Skip to content

Commit

Permalink
Fix Restart Frontend returning an error, add ability to set custom la…
Browse files Browse the repository at this point in the history
…yout.html
kizniche committed Sep 3, 2024
1 parent 561b3a1 commit 984f5b1
Showing 15 changed files with 161 additions and 40 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ This release changes the install directory from ~/Mycodo to /opt/Mycodo. This ne
- Fix MCP23017 Pump Output KeyError
- Fix displaying Tags on Highcharts Widget
- Fix dependencies of Mijia LYWSD03MMC
- Fix Restart Frontend returning an error

### Features

@@ -27,6 +28,7 @@ This release changes the install directory from ~/Mycodo to /opt/Mycodo. This ne
- Add Output: GP8XXX (8403) 2-Channel DAC (0-10 VDC) ([#1354](https://github.com/kizniche/Mycodo/issues/1354))
- Add Output: XL9535 16-Channel On/Off IO-Expander
- Add Widget: Measurement (2 Values)
- Add ability to set custom layout.html
- Add API Endpoint: /notes/create to create a Note ([#1357](https://github.com/kizniche/Mycodo/issues/1357))
- Add ability to switch displaying hostname with custom text
- Add Step Line Series Type to Graph (Synchronous) Widget
40 changes: 40 additions & 0 deletions alembic_db/alembic/versions/9bdb60d2a2cd_add_custom_layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Add custom_layout
Revision ID: 9bdb60d2a2cd
Revises: d6b624da47f4
Create Date: 2024-09-03 13:44:20.678365
"""
import sys
import os

sys.path.append(os.path.abspath(os.path.join(__file__, "../../../..")))

from alembic_db.alembic_post_utils import write_revision_post_alembic

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '9bdb60d2a2cd'
down_revision = 'd6b624da47f4'
branch_labels = None
depends_on = None


def upgrade():
with op.batch_alter_table("misc") as batch_op:
batch_op.add_column(sa.Column('custom_layout', sa.Text))

op.execute(
'''
UPDATE misc
SET custom_layout=''
'''
)


def downgrade():
with op.batch_alter_table("misc") as batch_op:
batch_op.drop_column('custom_layout')
7 changes: 5 additions & 2 deletions mycodo/config.py
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
from config_translations import TRANSLATIONS as T

MYCODO_VERSION = '8.15.13'
ALEMBIC_VERSION = 'd6b624da47f4'
ALEMBIC_VERSION = '9bdb60d2a2cd'

# FORCE UPGRADE MASTER
# Set True to enable upgrading to the master branch of the Mycodo repository.
@@ -55,8 +55,11 @@
PATH_INPUTS_CUSTOM = os.path.join(PATH_INPUTS, 'custom_inputs')
PATH_OUTPUTS_CUSTOM = os.path.join(PATH_OUTPUTS, 'custom_outputs')
PATH_WIDGETS_CUSTOM = os.path.join(PATH_WIDGETS, 'custom_widgets')
PATH_TEMPLATE = os.path.join(INSTALL_DIRECTORY, 'mycodo/mycodo_flask/templates')
PATH_TEMPLATE_LAYOUT = os.path.join(PATH_TEMPLATE, 'layout.html')
PATH_TEMPLATE_LAYOUT_DEFAULT = os.path.join(PATH_TEMPLATE, 'layout_default.html')
PATH_TEMPLATE_USER = os.path.join(PATH_TEMPLATE, 'user_templates')
PATH_USER_SCRIPTS = os.path.join(INSTALL_DIRECTORY, 'mycodo/user_scripts')
PATH_HTML_USER = os.path.join(INSTALL_DIRECTORY, 'mycodo/mycodo_flask/templates/user_templates')
PATH_PYTHON_CODE_USER = os.path.join(INSTALL_DIRECTORY, 'mycodo/user_python_code')
PATH_MEASUREMENTS_BACKUP = os.path.join(INSTALL_DIRECTORY, 'mycodo/backup_measurements')
PATH_SETTINGS_BACKUP = os.path.join(INSTALL_DIRECTORY, 'mycodo/backup_settings')
1 change: 1 addition & 0 deletions mycodo/databases/models/misc.py
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ class Misc(CRUDMixin, db.Model):
brand_image = db.Column(db.BLOB, default=b'')
brand_image_height = db.Column(db.Integer, default=55)
custom_css = db.Column(db.String, default='')
custom_layout = db.Column(db.String, default='')

# Measurement database
db_name = 'influxdb' # Default
11 changes: 8 additions & 3 deletions mycodo/mycodo_flask/app.py
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
from mycodo.mycodo_flask.api import api_blueprint, init_api
from mycodo.mycodo_flask.extensions import db
from mycodo.mycodo_flask.utils.utils_general import get_ip_address
from mycodo.utils.layouts import update_layout
from mycodo.utils.widgets import parse_widget_information

logger = logging.getLogger(__name__)
@@ -78,9 +79,13 @@ def register_extensions(app):
if app.config['SQLALCHEMY_DATABASE_URI'] != 'sqlite://':
with session_scope(app.config['SQLALCHEMY_DATABASE_URI']) as new_session:
misc = new_session.query(Misc).first()
if misc and misc.force_https:
csp = {'default-src': ['*', '\'unsafe-inline\'', '\'unsafe-eval\'']}
Talisman(app, content_security_policy=csp)
if misc:
# Ensure layout.html is present, by generating it at startup
update_layout(misc.custom_layout)

if misc.force_https:
csp = {'default-src': ['*', '\'unsafe-inline\'', '\'unsafe-eval\'']}
Talisman(app, content_security_policy=csp)


def register_blueprints(app):
1 change: 1 addition & 0 deletions mycodo/mycodo_flask/forms/forms_settings.py
Original file line number Diff line number Diff line change
@@ -92,6 +92,7 @@ class SettingsGeneral(FlaskForm):
language = StringField(lazy_gettext('Language'))
rpyc_timeout = StringField(lazy_gettext('Pyro Timeout'))
custom_css = StringField(lazy_gettext('Custom CSS'), widget=TextArea())
custom_layout = StringField(lazy_gettext('Custom Layout'), widget=TextArea())
brand_display = StringField(lazy_gettext('Brand Display'))
title_display = StringField(lazy_gettext('Title Display'))
hostname_override = StringField(lazy_gettext('Brand Text'))
20 changes: 9 additions & 11 deletions mycodo/mycodo_flask/routes_dashboard.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
from sqlalchemy import and_

from mycodo.config import INSTALL_DIRECTORY
from mycodo.config import PATH_HTML_USER
from mycodo.config import PATH_TEMPLATE_USER
from mycodo.databases.models import (PID, Camera, Conditional, Conversion,
CustomController, Dashboard,
DeviceMeasurements, Input, Measurement,
@@ -206,37 +206,37 @@ def page_dashboard(dashboard_id):

for each_widget_type in widget_types_on_dashboard:
file_html_head = "widget_template_{}_head.html".format(each_widget_type)
path_html_head = os.path.join(PATH_HTML_USER, file_html_head)
path_html_head = os.path.join(PATH_TEMPLATE_USER, file_html_head)
if os.path.exists(path_html_head):
list_html_files_head[each_widget_type] = file_html_head

file_html_title_bar = "widget_template_{}_title_bar.html".format(each_widget_type)
path_html_title_bar = os.path.join(PATH_HTML_USER, file_html_title_bar)
path_html_title_bar = os.path.join(PATH_TEMPLATE_USER, file_html_title_bar)
if os.path.exists(path_html_title_bar):
list_html_files_title_bar[each_widget_type] = file_html_title_bar

file_html_body = "widget_template_{}_body.html".format(each_widget_type)
path_html_body = os.path.join(PATH_HTML_USER, file_html_body)
path_html_body = os.path.join(PATH_TEMPLATE_USER, file_html_body)
if os.path.exists(path_html_body):
list_html_files_body[each_widget_type] = file_html_body

file_html_configure_options = "widget_template_{}_configure_options.html".format(each_widget_type)
path_html_configure_options = os.path.join(PATH_HTML_USER, file_html_configure_options)
path_html_configure_options = os.path.join(PATH_TEMPLATE_USER, file_html_configure_options)
if os.path.exists(path_html_configure_options):
list_html_files_configure_options[each_widget_type] = file_html_configure_options

file_html_js = "widget_template_{}_js.html".format(each_widget_type)
path_html_js = os.path.join(PATH_HTML_USER, file_html_js)
path_html_js = os.path.join(PATH_TEMPLATE_USER, file_html_js)
if os.path.exists(path_html_js):
list_html_files_js[each_widget_type] = file_html_js

file_html_js_ready = "widget_template_{}_js_ready.html".format(each_widget_type)
path_html_js_ready = os.path.join(PATH_HTML_USER, file_html_js_ready)
path_html_js_ready = os.path.join(PATH_TEMPLATE_USER, file_html_js_ready)
if os.path.exists(path_html_js_ready):
list_html_files_js_ready[each_widget_type] = file_html_js_ready

file_html_js_ready_end = "widget_template_{}_js_ready_end.html".format(each_widget_type)
path_html_js_ready_end = os.path.join(PATH_HTML_USER, file_html_js_ready_end)
path_html_js_ready_end = os.path.join(PATH_TEMPLATE_USER, file_html_js_ready_end)
if os.path.exists(path_html_js_ready_end):
list_html_files_js_ready_end[each_widget_type] = file_html_js_ready_end

@@ -330,11 +330,9 @@ def page_dashboard(dashboard_id):
@flask_login.login_required
def restart_flask_auto_advance_page(dashboard_id=""):
"""Wait then automatically load next page"""
logger.info("Reloading frontend in 5 seconds")

logger.info("Reloading frontend in 10 seconds")
cmd = f"sleep 10 && {INSTALL_DIRECTORY}/mycodo/scripts/mycodo_wrapper frontend_reload 2>&1"
subprocess.Popen(cmd, shell=True)
logger.info("Rendering wait page")

return render_template('pages/wait_and_autoload.html',
dashboard_id=dashboard_id)
11 changes: 8 additions & 3 deletions mycodo/mycodo_flask/routes_general.py
Original file line number Diff line number Diff line change
@@ -843,8 +843,13 @@ def computer_command(action):
elif action == 'frontend_reload':
subprocess.Popen('docker restart mycodo_flask 2>&1', shell=True)
else:
cmd = f'{INSTALL_DIRECTORY}/mycodo/scripts/mycodo_wrapper {action} 2>&1'
subprocess.Popen(cmd, shell=True)
if action == 'frontend_reload':
logger.info("Reloading frontend in 10 seconds")
cmd = f"sleep 10 && {INSTALL_DIRECTORY}/mycodo/scripts/mycodo_wrapper frontend_reload 2>&1"
subprocess.Popen(cmd, shell=True)
else:
cmd = f'{INSTALL_DIRECTORY}/mycodo/scripts/mycodo_wrapper {action} 2>&1'
subprocess.Popen(cmd, shell=True)

if action == 'restart':
flash(gettext("System rebooting in 10 seconds"), "success")
@@ -853,7 +858,7 @@ def computer_command(action):
elif action == 'daemon_restart':
flash(gettext("Command to restart the daemon sent"), "success")
elif action == 'frontend_reload':
flash(gettext("Command to reload the frontend sent"), "success")
flash(gettext("Frontend reloading in 10 seconds"), "success")

return redirect('/settings')

File renamed without changes.
6 changes: 6 additions & 0 deletions mycodo/mycodo_flask/templates/settings/general.html
Original file line number Diff line number Diff line change
@@ -121,6 +121,12 @@ <h3 style="text-align: right; padding-bottom: 1.3em;"><a href="https://kizniche.
<textarea class="form-control" id="custom_css" name="custom_css" title="Set custom CSS" rows="4">{{settings.custom_css}}</textarea>
</div>
</div>
<div class="form-group">
{{form_settings_general.custom_layout.label(class_='col-sm-12 control-label checkbox-nopad')}}
<div class="col-sm-12">
<textarea class="form-control" id="custom_layout" name="custom_layout" title="Set custom Layout HTML" rows="4">{{settings.custom_layout}}</textarea>
</div>
</div>
<div class="form-group">
{{form_settings_general.daemon_debug_mode.label(class_='col-sm-12 control-label checkbox-nopad')}}
<div class="col-sm-12">
6 changes: 3 additions & 3 deletions mycodo/mycodo_flask/utils/utils_export.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
from mycodo.config import (ALEMBIC_VERSION, DATABASE_NAME, DOCKER_CONTAINER, IMPORT_LOG_FILE,
INSTALL_DIRECTORY, MYCODO_VERSION,
PATH_ACTIONS_CUSTOM, PATH_FUNCTIONS_CUSTOM,
PATH_HTML_USER, PATH_INPUTS_CUSTOM,
PATH_TEMPLATE_USER, PATH_INPUTS_CUSTOM,
PATH_OUTPUTS_CUSTOM, PATH_PYTHON_CODE_USER,
PATH_USER_SCRIPTS, PATH_WIDGETS_CUSTOM,
SQL_DATABASE_MYCODO, DATABASE_PATH)
@@ -310,7 +310,7 @@ def import_settings(form):
PATH_OUTPUTS_CUSTOM,
PATH_WIDGETS_CUSTOM,
PATH_USER_SCRIPTS,
PATH_HTML_USER,
PATH_TEMPLATE_USER,
PATH_PYTHON_CODE_USER
]

@@ -336,7 +336,7 @@ def import_settings(form):
(PATH_OUTPUTS_CUSTOM, "custom_outputs"),
(PATH_WIDGETS_CUSTOM, "custom_widgets"),
(PATH_USER_SCRIPTS, "user_scripts"),
(PATH_HTML_USER, "user_html"),
(PATH_TEMPLATE_USER, "user_html"),
(PATH_PYTHON_CODE_USER, "user_python_code")
]

26 changes: 19 additions & 7 deletions mycodo/mycodo_flask/utils/utils_settings.py
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
from mycodo.config import PATH_FUNCTIONS_CUSTOM
from mycodo.config import PATH_INPUTS_CUSTOM
from mycodo.config import PATH_OUTPUTS_CUSTOM
from mycodo.config import PATH_TEMPLATE_USER
from mycodo.config import PATH_WIDGETS_CUSTOM
from mycodo.config import UPGRADE_INIT_FILE
from mycodo.config_devices_units import MEASUREMENTS
@@ -65,6 +66,7 @@
from mycodo.utils.database import db_retrieve_table
from mycodo.utils.functions import parse_function_information
from mycodo.utils.inputs import parse_input_information
from mycodo.utils.layouts import update_layout
from mycodo.utils.modules import load_module_from_file
from mycodo.utils.outputs import parse_output_information
from mycodo.utils.send_data import send_email
@@ -363,6 +365,7 @@ def user_del(form):
# Settings modifications
#


def settings_general_mod(form):
"""Modify General settings."""
messages = {
@@ -382,12 +385,22 @@ def settings_general_mod(form):

if not messages["error"]:
try:
reload_frontend = False
mod_misc = Misc.query.first()

force_https = mod_misc.force_https
mod_misc.force_https = form.force_https.data
if mod_misc.force_https != form.force_https.data:
mod_misc.force_https = form.force_https.data
reload_frontend = True

mod_misc.rpyc_timeout = form.rpyc_timeout.data
mod_misc.custom_css = form.custom_css.data

if mod_misc.custom_layout != form.custom_layout.data:
mod_misc.custom_layout = form.custom_layout.data
assure_path_exists(PATH_TEMPLATE_USER)
update_layout(mod_misc.custom_layout)
reload_frontend = True

mod_misc.brand_display = form.brand_display.data
mod_misc.title_display = form.title_display.data
mod_misc.hostname_override = form.hostname_override.data
@@ -452,11 +465,10 @@ def settings_general_mod(form):
action=TRANSLATIONS['modify']['title'],
controller=gettext("General Settings")))

if force_https != form.force_https.data:
# Force HTTPS option changed.
# Reload web server with new settings.
cmd = '{path}/mycodo/scripts/mycodo_wrapper frontend_reload 2>&1'.format(
path=INSTALL_DIRECTORY)
if reload_frontend:
# Reload web server
logger.info("Reloading frontend in 10 seconds")
cmd = f"sleep 10 && {INSTALL_DIRECTORY}/mycodo/scripts/mycodo_wrapper frontend_reload 2>&1"
subprocess.Popen(cmd, shell=True)

except Exception as except_msg:
48 changes: 48 additions & 0 deletions mycodo/utils/layouts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
#
# layouts.py - Mycodo core utils
#
# Copyright (C) 2015-2020 Kyle T. Gabriel <mycodo@kylegabriel.com>
#
# This file is part of Mycodo
#
# Mycodo 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.
#
# Mycodo 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 Mycodo. If not, see <http://www.gnu.org/licenses/>.
#
# Contact at kylegabriel.com

import logging
import os
import shutil

from mycodo.config import PATH_TEMPLATE_LAYOUT
from mycodo.config import PATH_TEMPLATE_LAYOUT_DEFAULT

logger = logging.getLogger("mycodo.utils.layouts")


def update_layout(custom_layout):
try:
if custom_layout:
# Use custom layout
with open(PATH_TEMPLATE_LAYOUT, "w") as template:
template.write(custom_layout)
else:
# Use default layout
if (os.path.exists(PATH_TEMPLATE_LAYOUT) and
not os.path.samefile(PATH_TEMPLATE_LAYOUT, PATH_TEMPLATE_LAYOUT_DEFAULT)):
# Delete current layout if it's different from the default
os.remove(PATH_TEMPLATE_LAYOUT)
shutil.copy(PATH_TEMPLATE_LAYOUT_DEFAULT, PATH_TEMPLATE_LAYOUT)
except:
logger.exception("Generating layout")
4 changes: 2 additions & 2 deletions mycodo/utils/tools.py
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
from dateutil import relativedelta

from mycodo.config import (INSTALL_DIRECTORY, PATH_ACTIONS_CUSTOM,
PATH_FUNCTIONS_CUSTOM, PATH_HTML_USER,
PATH_FUNCTIONS_CUSTOM, PATH_TEMPLATE_USER,
PATH_INPUTS_CUSTOM, PATH_OUTPUTS_CUSTOM,
PATH_PYTHON_CODE_USER, PATH_USER_SCRIPTS,
PATH_WIDGETS_CUSTOM, SQL_DATABASE_MYCODO,
@@ -95,7 +95,7 @@ def create_settings_export(save_path=None):
(PATH_OUTPUTS_CUSTOM, "custom_outputs"),
(PATH_WIDGETS_CUSTOM, "custom_widgets"),
(PATH_USER_SCRIPTS, "user_scripts"),
(PATH_HTML_USER, "user_html"),
(PATH_TEMPLATE_USER, "user_html"),
(PATH_PYTHON_CODE_USER, "user_python_code")
]
for each_backup in export_directories:
18 changes: 9 additions & 9 deletions mycodo/utils/widget_generate_html.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@

sys.path.append(os.path.abspath(os.path.join(os.path.realpath(__file__), '../../..')))

from mycodo.config import PATH_HTML_USER
from mycodo.config import PATH_TEMPLATE_USER
from mycodo.utils.widgets import parse_widget_information

logger = logging.getLogger("mycodo.utils.widget_generate_html")
@@ -31,12 +31,12 @@ def assure_path_exists(path):
def generate_widget_html():
"""Generate all HTML files for all widgets."""
dict_widgets = parse_widget_information()
assure_path_exists(PATH_HTML_USER)
assure_path_exists(PATH_TEMPLATE_USER)

for widget_name in dict_widgets:
try:
filename_head = f"widget_template_{widget_name}_head.html"
path_head = os.path.join(PATH_HTML_USER, filename_head)
path_head = os.path.join(PATH_TEMPLATE_USER, filename_head)
with open(path_head, 'w') as fw:
if 'widget_dashboard_head' in dict_widgets[widget_name]:
html_head = dict_widgets[widget_name]['widget_dashboard_head']
@@ -47,7 +47,7 @@ def generate_widget_html():
set_user_grp(path_head, 'mycodo', 'mycodo')

filename_title_bar = f"widget_template_{widget_name}_title_bar.html"
path_title_bar = os.path.join(PATH_HTML_USER, filename_title_bar)
path_title_bar = os.path.join(PATH_TEMPLATE_USER, filename_title_bar)
with open(path_title_bar, 'w') as fw:
if 'widget_dashboard_title_bar' in dict_widgets[widget_name]:
html_title_bar = dict_widgets[widget_name]['widget_dashboard_title_bar']
@@ -58,7 +58,7 @@ def generate_widget_html():
set_user_grp(path_title_bar, 'mycodo', 'mycodo')

filename_body = f"widget_template_{widget_name}_body.html"
path_body = os.path.join(PATH_HTML_USER, filename_body)
path_body = os.path.join(PATH_TEMPLATE_USER, filename_body)
with open(path_body, 'w') as fw:
if 'widget_dashboard_body' in dict_widgets[widget_name]:
html_body = dict_widgets[widget_name]['widget_dashboard_body']
@@ -69,7 +69,7 @@ def generate_widget_html():
set_user_grp(path_body, 'mycodo', 'mycodo')

filename_configure_options = f"widget_template_{widget_name}_configure_options.html"
path_configure_options = os.path.join(PATH_HTML_USER, filename_configure_options)
path_configure_options = os.path.join(PATH_TEMPLATE_USER, filename_configure_options)
with open(path_configure_options, 'w') as fw:
if 'widget_dashboard_configure_options' in dict_widgets[widget_name]:
html_configure_options = dict_widgets[widget_name]['widget_dashboard_configure_options']
@@ -80,7 +80,7 @@ def generate_widget_html():
set_user_grp(path_configure_options, 'mycodo', 'mycodo')

filename_js = f"widget_template_{widget_name}_js.html"
path_js = os.path.join(PATH_HTML_USER, filename_js)
path_js = os.path.join(PATH_TEMPLATE_USER, filename_js)
with open(path_js, 'w') as fw:
if 'widget_dashboard_js' in dict_widgets[widget_name]:
html_js = dict_widgets[widget_name]['widget_dashboard_js']
@@ -91,7 +91,7 @@ def generate_widget_html():
set_user_grp(path_js, 'mycodo', 'mycodo')

filename_js_ready = f"widget_template_{widget_name}_js_ready.html"
path_js_ready = os.path.join(PATH_HTML_USER, filename_js_ready)
path_js_ready = os.path.join(PATH_TEMPLATE_USER, filename_js_ready)
with open(path_js_ready, 'w') as fw:
if 'widget_dashboard_js_ready' in dict_widgets[widget_name]:
html_js_ready = dict_widgets[widget_name]['widget_dashboard_js_ready']
@@ -102,7 +102,7 @@ def generate_widget_html():
set_user_grp(path_js_ready, 'mycodo', 'mycodo')

filename_js_ready_end = f"widget_template_{widget_name}_js_ready_end.html"
path_js_ready_end = os.path.join(PATH_HTML_USER, filename_js_ready_end)
path_js_ready_end = os.path.join(PATH_TEMPLATE_USER, filename_js_ready_end)
with open(path_js_ready_end, 'w') as fw:
if 'widget_dashboard_js_ready_end' in dict_widgets[widget_name]:
html_js_ready_end = dict_widgets[widget_name]['widget_dashboard_js_ready_end']

0 comments on commit 984f5b1

Please sign in to comment.