diff --git a/CHANGELOG.md b/CHANGELOG.md index 459cb3f51..d6be3ed0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/alembic_db/alembic/versions/9bdb60d2a2cd_add_custom_layout.py b/alembic_db/alembic/versions/9bdb60d2a2cd_add_custom_layout.py new file mode 100644 index 000000000..8c53fe343 --- /dev/null +++ b/alembic_db/alembic/versions/9bdb60d2a2cd_add_custom_layout.py @@ -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') diff --git a/mycodo/config.py b/mycodo/config.py index c889a282c..488fb5679 100644 --- a/mycodo/config.py +++ b/mycodo/config.py @@ -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') diff --git a/mycodo/databases/models/misc.py b/mycodo/databases/models/misc.py index 628e44659..109e764d6 100644 --- a/mycodo/databases/models/misc.py +++ b/mycodo/databases/models/misc.py @@ -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 diff --git a/mycodo/mycodo_flask/app.py b/mycodo/mycodo_flask/app.py index 776a01154..0b2ef3fac 100755 --- a/mycodo/mycodo_flask/app.py +++ b/mycodo/mycodo_flask/app.py @@ -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): diff --git a/mycodo/mycodo_flask/forms/forms_settings.py b/mycodo/mycodo_flask/forms/forms_settings.py index c7d8ace29..6d390ef69 100644 --- a/mycodo/mycodo_flask/forms/forms_settings.py +++ b/mycodo/mycodo_flask/forms/forms_settings.py @@ -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')) diff --git a/mycodo/mycodo_flask/routes_dashboard.py b/mycodo/mycodo_flask/routes_dashboard.py index d07709ef6..e01a3b675 100644 --- a/mycodo/mycodo_flask/routes_dashboard.py +++ b/mycodo/mycodo_flask/routes_dashboard.py @@ -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) diff --git a/mycodo/mycodo_flask/routes_general.py b/mycodo/mycodo_flask/routes_general.py index 6286bc7df..6aedd6029 100644 --- a/mycodo/mycodo_flask/routes_general.py +++ b/mycodo/mycodo_flask/routes_general.py @@ -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') diff --git a/mycodo/mycodo_flask/templates/layout.html b/mycodo/mycodo_flask/templates/layout_default.html similarity index 100% rename from mycodo/mycodo_flask/templates/layout.html rename to mycodo/mycodo_flask/templates/layout_default.html diff --git a/mycodo/mycodo_flask/templates/settings/general.html b/mycodo/mycodo_flask/templates/settings/general.html index 1d445de86..79d066cfa 100644 --- a/mycodo/mycodo_flask/templates/settings/general.html +++ b/mycodo/mycodo_flask/templates/settings/general.html @@ -121,6 +121,12 @@