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__)
#
# foris-subordinates-plugin
# 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
import bottle
import os
import typing
from foris import fapi
from foris.config import ConfigPageMixin, add_config_page, JoinedPages
from foris.plugins import ForisPlugin
from foris.state import current_state
from foris.utils import messages
from foris.utils.routing import reverse
from foris.utils.translators import gettext_dummy as gettext, gettext as _
from . import handlers
class CommonPage(ConfigPageMixin):
def _prepare_render_args(self, args):
args['PLUGIN_NAME'] = SubordinatesPlugin.PLUGIN_NAME
args['PLUGIN_STYLES'] = SubordinatesPlugin.PLUGIN_STYLES
args['PLUGIN_STATIC_SCRIPTS'] = SubordinatesPlugin.PLUGIN_STATIC_SCRIPTS
args['PLUGIN_DYNAMIC_SCRIPTS'] = SubordinatesPlugin.PLUGIN_DYNAMIC_SCRIPTS
def render(self, **kwargs):
self._prepare_render_args(kwargs)
return super().render(**kwargs)
class SubordinatesSetupPage(CommonPage, handlers.SubordinatesConfigHandler):
slug = "subordinates-setup"
menu_order = 1 # submenu
template = "subordinates/subordinates_setup"
menu_title = gettext("Set up")
userfriendly_title = gettext("Managed devices: Set up")
template_type = "jinja2"
def render(self, **kwargs):
data = current_state.backend.perform("subordinates", "list")
kwargs["subordinates"] = data["subordinates"]
return super().render(**kwargs)
def save(self, *args, **kwargs):
super(SubordinatesSetupPage, self).save(no_messages=True, *args, **kwargs)
data = self.form.callback_results
if data["result"]:
messages.success(_(
"Token was successfully added and client '%(controller_id)s' "
"should be visible in a moment."
) % dict(controller_id=data["controller_id"]))
else:
messages.error(_("Failed to add token."))
return data["result"]
def _check_and_get_controller_id(self):
if bottle.request.method != 'POST':
messages.error(_("Wrong HTTP method."))
bottle.redirect(reverse("config_page", page_name="remote"))
form = self.get_controller_id_form(bottle.request.POST.decode())
if not form.data["controller_id"]:
raise bottle.HTTPError(404, "controller_id not found")
return form.data["controller_id"]
def _ajax_list_subordinates(self):
data = current_state.backend.perform("subordinates", "list")
return bottle.template(
"subordinates/_subordinates_list_setup.html.j2",
subordinates=data["subordinates"],
template_adapter=bottle.Jinja2Template,
)
def _ajax_delete(self):
controller_id = self._check_and_get_controller_id()
res = current_state.backend.perform(
"subordinates", "del", {"controller_id": controller_id})
if res["result"]:
return bottle.template(
"subordinates/_subordinates_message.html.j2",
message={
"classes": ['success'],
"text": _("Subordinate '%(controller_id)s' was successfully deleted.")
% dict(controller_id=controller_id)
},
template_adapter=bottle.Jinja2Template,
)
else:
return bottle.template(
"subordinates/_subordinates_message.html.j2",
message={
"classes": ['error'],
"text": _("Failed to delete subordinate '%(controller_id)s'.")
% dict(controller_id=controller_id)
},
template_adapter=bottle.Jinja2Template,
)
def _ajax_set_enabled(self, enabled):
controller_id = self._check_and_get_controller_id()
res = current_state.backend.perform("subordinates", "set_enabled", {
"controller_id": controller_id,
"enabled": enabled,
})
if res["result"]:
if enabled:
message = {
"classes": ['success'],
"text": _("Subordinate '%(controller_id)s' was sucessfuly enabled.")
% dict(controller_id=controller_id)
}
else:
message = {
"classes": ['success'],
"text": _("Subordinate '%(controller_id)s' was sucessfuly disabled.")
% dict(controller_id=controller_id)
}
else:
if enabled:
message = {
"classes": ['error'],
"text": _("Failed to enable subordinate '%(controller_id)s'.")
% dict(controller_id=controller_id)
}
else:
message = {
"classes": ['error'],
"text": _("Failed to disable subordinate '%(controller_id)s'.")
% dict(controller_id=controller_id)
}
return bottle.template(
"subordinates/_subordinates_message.html.j2",
message=message,
template_adapter=bottle.Jinja2Template,
)
def call_ajax_action(self, action):
if action == "list":
return self._ajax_list_subordinates()
elif action == "disable":
return self._ajax_set_enabled(False)
elif action == "enable":
return self._ajax_set_enabled(True)
elif action == "delete":
return self._ajax_delete()
raise ValueError("Unknown AJAX action.")
@classmethod
def is_visible(cls):
if current_state.backend.name != "mqtt":
return False
return ConfigPageMixin.is_visible_static(cls)
@classmethod
def is_enabled(cls):
if current_state.backend.name != "mqtt":
return False
return ConfigPageMixin.is_enabled_static(cls)
def get_page_form(self, form_name: str, data: dict, controller_id: str) -> typing.Tuple[
fapi.ForisAjaxForm, typing.Callable[[dict], typing.Tuple['str', 'str']]
]:
"""Returns appropriate foris form and handler to generate response
"""
form: fapi.ForisAjaxForm
if form_name == "sub-form":
form = handlers.SubordinatesEditForm(data)
def prepare_message(results: dict) -> dict:
if results["result"]:
message = {
"classes": ['success'],
"text": _("Device '%(controller_id)s' was sucessfully updated.")
% dict(controller_id=data["controller_id"])
}
else:
message = {
"classes": ['error'],
"text": _("Failed to update subordinate '%(controller_id)s'.")
% dict(controller_id=data["controller_id"])
}
return message
form.url = reverse(
"config_ajax_form", page_name="subordinates-setup", form_name="sub-form"
)
return form, prepare_message
elif form_name == "subsub-form":
form = handlers.SubsubordinatesEditForm(data)
def prepare_message(results: dict) -> dict:
if results["result"]:
message = {
"classes": ['success'],
"text": _("Subsubordinate '%(controller_id)s' was sucessfully updated.")
% dict(controller_id=data["controller_id"])
}
else:
message = {
"classes": ['error'],
"text": _("Failed to update subsubordinate '%(controller_id)s'.")
% dict(controller_id=data["controller_id"])
}
return message
form.url = reverse(
"config_ajax_form", page_name="subordinates-setup", form_name="subsub-form")
return form, prepare_message
raise bottle.HTTPError(404, "No form '%s' not found." % form_name)
class SubordinatesWifiPage(CommonPage):
slug = "subordinates-wifi"
menu_order = 2 # submenu
template = "subordinates/subordinates_wifi"
menu_title = gettext("Wi-Fi")
userfriendly_title = gettext("Managed devices: Wi-Fi")
template_type = "jinja2"
def render(self, **kwargs):
data = current_state.backend.perform("subordinates", "list")
kwargs["subordinates"] = data["subordinates"]
return super().render(**kwargs)
@classmethod
def is_visible(cls):
if current_state.backend.name != "mqtt":
return False
return ConfigPageMixin.is_visible_static(cls)
@classmethod
def is_enabled(cls):
if current_state.backend.name != "mqtt":
return False
return ConfigPageMixin.is_enabled_static(cls)
def _ajax_list_subordinates(self):
data = current_state.backend.perform("subordinates", "list")
return bottle.template(
"subordinates/_subordinates_list_wifi.html.j2",
subordinates=data["subordinates"],
template_adapter=bottle.Jinja2Template,
)
def call_ajax_action(self, action):
if action == "list":
return self._ajax_list_subordinates()
raise ValueError("Unknown AJAX action.")
def get_page_form(self, form_name: str, data: dict, controller_id: str) -> typing.Tuple[
fapi.ForisAjaxForm, typing.Callable[[dict], typing.Tuple['str', 'str']]
]:
"""Returns appropriate foris form and handler to generate response
"""
if form_name == "wifi-form":
form = handlers.WifiEditForm(data, controller_id=controller_id)
def prepare_message(results: dict) -> dict:
if results["result"]:
message = {
"classes": ['success'],
"text": _("Wifi settings was sucessfully updated.")
}
else:
message = {
"classes": ['error'],
"text": _("Failed to update Wifi settings.")
}
return message
form.url = reverse(
"config_ajax_form", page_name="subordinates-wifi", form_name="wifi-form"
)
return form, prepare_message
raise bottle.HTTPError(404, "No form '%s' not found." % form_name)
class SubordinatesJoinedPage(JoinedPages):
userfriendly_title = gettext("Managed devices")
slug = "subordinates"
no_url = True
subpages: typing.Iterable[typing.Type['ConfigPageMixin']] = [
SubordinatesSetupPage,
SubordinatesWifiPage,
]
@classmethod
def is_visible(cls):
if current_state.backend.name != "mqtt":
return False
return ConfigPageMixin.is_visible_static(cls)
@classmethod
def is_enabled(cls):
if current_state.backend.name != "mqtt":
return False
return ConfigPageMixin.is_enabled_static(cls)
# plugin definition
class SubordinatesPlugin(ForisPlugin):
PLUGIN_NAME = "subordinates"
DIRNAME = os.path.dirname(os.path.abspath(__file__))
PLUGIN_STYLES = [
]
PLUGIN_STATIC_SCRIPTS = [
"js/subordinates.js", # static js file
]
PLUGIN_DYNAMIC_SCRIPTS = [
"subordinates.js", # dynamic js file (a template which will be rendered to javascript)
]
def __init__(self, app):
super(SubordinatesPlugin, self).__init__(app)
add_config_page(SubordinatesJoinedPage)
# 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 %}