diff --git a/data/db/slideshow.json.dist b/data/db/slideshow.json.dist index c2acc4a..7371348 100755 --- a/data/db/slideshow.json.dist +++ b/data/db/slideshow.json.dist @@ -6,7 +6,8 @@ "location", "name", "position", - "type" + "type", + "cron_schedule" ], "data": { "0": { @@ -15,7 +16,8 @@ "type": "picture", "enabled": true, "name": "Picture Sample", - "position": 0 + "position": 0, + "cron_schedule": null }, "1": { "location": "https://unix.org", @@ -23,7 +25,8 @@ "type": "url", "enabled": true, "name": "URL Sample", - "position": 1 + "position": 1, + "cron_schedule": null } } } \ No newline at end of file diff --git a/data/www/css/main.css b/data/www/css/main.css index 1b4cd4d..ff2f41a 100644 --- a/data/www/css/main.css +++ b/data/www/css/main.css @@ -512,6 +512,14 @@ form .form-group textarea { width: auto; } +form .form-group input[type=checkbox] { + flex: 0; +} + +form .form-group input[type=checkbox].trigger { + margin-right: 10px; +} + form .form-group span { margin-left: 10px; } diff --git a/data/www/js/slideshow.js b/data/www/js/slideshow.js index 68bf91f..3cd6ea0 100644 --- a/data/www/js/slideshow.js +++ b/data/www/js/slideshow.js @@ -49,7 +49,7 @@ jQuery(document).ready(function ($) { }); }; - $(document).on('change', 'input[type=checkbox]', function () { + $(document).on('change', '.slide-item input[type=checkbox]', function () { $.ajax({ url: '/slideshow/slide/toggle', headers: {'Content-Type': 'application/json'}, @@ -68,6 +68,16 @@ jQuery(document).ready(function ($) { updateTable(); }); + $(document).on('change', '.modal-slide input[type=checkbox]', function () { + const $target = $('#'+ $(this).attr('id').replace('-trigger', '')); + const hide = !$(this).is(':checked'); + $target.toggleClass('hidden', hide); + + if (hide) { + $target.val(''); + } + }); + $(document).on('change', '#slide-add-type', function () { const value = $(this).val(); const inputType = $(this).find('option').filter(function (i, el) { @@ -95,11 +105,14 @@ jQuery(document).ready(function ($) { $(document).on('click', '.slide-edit', function () { const slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity')); showModal('modal-slide-edit'); + const hasCron = slide.cron_schedule && slide.cron_schedule.length > 0; $('.modal-slide-edit input:visible:eq(0)').focus().select(); $('#slide-edit-name').val(slide.name); $('#slide-edit-type').val(slide.type); $('#slide-edit-location').val(slide.location); $('#slide-edit-duration').val(slide.duration); + $('#slide-edit-cron-schedule').val(slide.cron_schedule).toggleClass('hidden', !hasCron); + $('#slide-edit-cron-schedule-trigger').prop('checked', hasCron); $('#slide-edit-id').val(slide.id); }); diff --git a/docs/screenshot.png b/docs/screenshot.png old mode 100644 new mode 100755 index 0841636..335c218 Binary files a/docs/screenshot.png and b/docs/screenshot.png differ diff --git a/lang/en.json b/lang/en.json index 0b4f76d..3c9857f 100644 --- a/lang/en.json +++ b/lang/en.json @@ -8,7 +8,10 @@ "slideshow_slide_panel_th_duration": "Duration", "slideshow_slide_panel_th_duration_unit": "sec", "slideshow_slide_panel_th_enabled": "Enabled", + "slideshow_slide_panel_th_cron_scheduled": "Scheduled", "slideshow_slide_panel_th_activity": "Activity", + "slideshow_slide_panel_td_cron_scheduled_loop": "Loop", + "slideshow_slide_panel_td_cron_scheduled_bad_cron": "Bad cron value", "slideshow_slide_form_add_title": "Add Slide", "slideshow_slide_form_add_submit": "Add", "slideshow_slide_form_edit_title": "Edit Slide", @@ -22,6 +25,8 @@ "slideshow_slide_form_label_object": "Object", "slideshow_slide_form_label_duration": "Duration", "slideshow_slide_form_label_duration_unit": "seconds", + "slideshow_slide_form_label_cron_scheduled": "Scheduled", + "slideshow_slide_form_widget_cron_scheduled_placeholder": "Use crontab format: * * * * *", "slideshow_slide_form_button_cancel": "Cancel", "js_slideshow_slide_delete_confirmation": "Are you sure?", diff --git a/lang/fr.json b/lang/fr.json index 1af5037..1cc575c 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -8,7 +8,10 @@ "slideshow_slide_panel_th_duration": "Durée", "slideshow_slide_panel_th_duration_unit": "sec", "slideshow_slide_panel_th_enabled": "Activé", + "slideshow_slide_panel_th_cron_scheduled": "Programmation", "slideshow_slide_panel_th_activity": "Options", + "slideshow_slide_panel_td_cron_scheduled_loop": "En boucle", + "slideshow_slide_panel_td_cron_scheduled_bad_cron": "Mauvaise valeur cron", "slideshow_slide_form_add_title": "Ajouter d'une slide", "slideshow_slide_form_add_submit": "Ajouter", "slideshow_slide_form_edit_title": "Modification d'une slide", @@ -22,6 +25,8 @@ "slideshow_slide_form_label_object": "Objet", "slideshow_slide_form_label_duration": "Durée", "slideshow_slide_form_label_duration_unit": "secondes", + "slideshow_slide_form_label_cron_scheduled": "Programmer", + "slideshow_slide_form_widget_cron_scheduled_placeholder": "Utiliser le format crontab: * * * * *", "slideshow_slide_form_button_cancel": "Annuler", "js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?", diff --git a/requirements.txt b/requirements.txt index 70ccd58..7460098 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ flask==2.3.3 pysondb-v2==2.1.0 python-dotenv -croniter +cron-descriptor diff --git a/src/controller/SlideshowController.py b/src/controller/SlideshowController.py index 259aa3b..5c82f2a 100644 --- a/src/controller/SlideshowController.py +++ b/src/controller/SlideshowController.py @@ -8,7 +8,7 @@ from src.service.ModelStore import ModelStore from src.model.entity.Slide import Slide from src.model.enum.SlideType import SlideType from src.interface.ObController import ObController -from src.utils import str_to_enum +from src.utils import str_to_enum, get_optional_string class SlideshowController(ObController): @@ -40,6 +40,7 @@ class SlideshowController(ObController): name=request.form['name'], type=str_to_enum(request.form['type'], SlideType), duration=request.form['duration'], + cron_schedule=get_optional_string(request.form['cron_schedule']), ) if slide.has_file(): @@ -65,7 +66,7 @@ class SlideshowController(ObController): return redirect(url_for('slideshow_slide_list')) def slideshow_slide_edit(self): - self._model_store.slide().update_form(request.form['id'], request.form['name'], request.form['duration']) + self._model_store.slide().update_form(request.form['id'], request.form['name'], request.form['duration'], request.form['cron_schedule']) self._post_update() return redirect(url_for('slideshow_slide_list')) diff --git a/src/manager/LangManager.py b/src/manager/LangManager.py index 7aca3f9..1db0dbe 100644 --- a/src/manager/LangManager.py +++ b/src/manager/LangManager.py @@ -8,7 +8,7 @@ class LangManager: def __init__(self, lang: str): self._map = {} - self._lang = lang + self._lang = lang.lower() self.load() def load(self, directory: str = "", prefix: str = ""): @@ -23,3 +23,6 @@ class LangManager: def map(self) -> dict: return self._map + + def get_locale(self, local_with_country: bool = False) -> str: + return "{}_{}".format(self._lang, self._lang.upper()) if local_with_country else self._lang \ No newline at end of file diff --git a/src/manager/SlideManager.py b/src/manager/SlideManager.py index 7944af9..31fa397 100644 --- a/src/manager/SlideManager.py +++ b/src/manager/SlideManager.py @@ -2,7 +2,7 @@ import os from typing import Dict, Optional, List, Tuple, Union from src.model.entity.Slide import Slide -from src.utils import str_to_enum +from src.utils import str_to_enum, get_optional_string from pysondb import PysonDB from pysondb.errors import IdDoesNotExistError @@ -69,8 +69,12 @@ class SlideManager: for slide_id, slide_position in positions.items(): self._db.update_by_id(slide_id, {"position": slide_position}) - def update_form(self, id: str, name: str, duration: int) -> None: - self._db.update_by_id(id, {"name": name, "duration": duration}) + def update_form(self, id: str, name: str, duration: int, cron_schedule: Optional[str] = '') -> None: + self._db.update_by_id(id, { + "name": name, + "duration": duration, + "cron_schedule": get_optional_string(cron_schedule) + }) def add_form(self, slide: Union[Slide, Dict]) -> None: db_slide = slide diff --git a/src/model/entity/Slide.py b/src/model/entity/Slide.py index 0f2059d..3ba35cd 100644 --- a/src/model/entity/Slide.py +++ b/src/model/entity/Slide.py @@ -7,14 +7,14 @@ from src.utils import str_to_enum class Slide: - def __init__(self, location: str = '', duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[str] = None, cron_schedule: str = ''): + def __init__(self, location: str = '', duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[str] = None, cron_schedule: Optional[str] = None): self._id = id if id else None self._location = location self._duration = duration self._type = str_to_enum(type, SlideType) if isinstance(type, str) else type self._enabled = enabled self._name = name - self._position = position, + self._position = position self._cron_schedule = cron_schedule @property @@ -38,11 +38,11 @@ class Slide: self._type = value @property - def cron_schedule(self) -> str: + def cron_schedule(self) -> Optional[str]: return self._cron_schedule @cron_schedule.setter - def cron_schedule(self, value: str): + def cron_schedule(self, value: Optional[str]): self._cron_schedule = value @property diff --git a/src/service/TemplateRenderer.py b/src/service/TemplateRenderer.py index 6146242..97d47b2 100644 --- a/src/service/TemplateRenderer.py +++ b/src/service/TemplateRenderer.py @@ -1,6 +1,8 @@ import os from flask import Flask, send_from_directory, Markup +from cron_descriptor import ExpressionDescriptor +from cron_descriptor.Exception import FormatException, WrongArgumentException, MissingFieldException from typing import List from jinja2 import Environment, FileSystemLoader, select_autoescape from src.service.ModelStore import ModelStore @@ -18,6 +20,22 @@ class TemplateRenderer: self._model_store = model_store self._render_hook = render_hook + def cron_descriptor(self, expression: str, use_24hour_time_format=True) -> str: + try: + return str( + ExpressionDescriptor( + expression=expression, + use_24hour_time_format=use_24hour_time_format, + locale_code=self._model_store.lang().get_locale(local_with_country=True) + ) + ) + except FormatException: + return '' + except WrongArgumentException: + return '' + except MissingFieldException: + return '' + def get_view_globals(self) -> dict: globals = dict( STATIC_PREFIX="/{}/{}/".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS), @@ -25,6 +43,7 @@ class TemplateRenderer: VERSION=self._model_store.config().map().get('version'), LANG=self._model_store.variable().map().get('lang').as_string(), HOOK=self._render_hook, + cron_descriptor=self.cron_descriptor ) for hook in HookType: diff --git a/src/service/WebServer.py b/src/service/WebServer.py index 04c3aa0..f4b4948 100644 --- a/src/service/WebServer.py +++ b/src/service/WebServer.py @@ -33,7 +33,6 @@ class WebServer: def setup(self) -> None: self._setup_flask_app() self._setup_web_globals() - self._setup_web_extensions() self._setup_web_errors() self._setup_web_controllers() @@ -73,12 +72,8 @@ class WebServer: def inject_global_vars() -> dict: return self._template_renderer.get_view_globals() - def _setup_web_extensions(self) -> None: - @self._app.template_filter('ctime') - def time_ctime(s): - return time.ctime(s) - def _setup_web_errors(self) -> None: @self._app.errorhandler(404) def not_found(e): return send_from_directory(self._get_template_folder(), 'core/error404.html'), 404 + diff --git a/src/utils.py b/src/utils.py index 48720c7..992c8f1 100644 --- a/src/utils.py +++ b/src/utils.py @@ -6,6 +6,17 @@ from typing import Optional, List from enum import Enum +def get_optional_string(var: Optional[str]) -> Optional[str]: + if var is None: + return None + + var = var.strip() + + if var: + return var + + return None + def get_keys(dict_or_object, key_list_name: str, key_attr_name: str = 'key') -> Optional[List]: if dict_or_object is None: return None diff --git a/views/slideshow/component/table.jinja.html b/views/slideshow/component/table.jinja.html index 443b9d6..f6abb29 100644 --- a/views/slideshow/component/table.jinja.html +++ b/views/slideshow/component/table.jinja.html @@ -4,6 +4,7 @@ {{ l.slideshow_slide_panel_th_name }} {{ l.slideshow_slide_panel_th_duration }} {{ l.slideshow_slide_panel_th_enabled }} + {{ l.slideshow_slide_panel_th_cron_scheduled }} {{ l.slideshow_slide_panel_th_activity }} @@ -42,6 +43,18 @@ + + {% if slide.cron_schedule %} + {% set cron_desc = cron_descriptor(slide.cron_schedule) %} + {% if cron_desc %} + ⏳ {{ cron_desc }} + {% else %} + ⚠️ {{ l.slideshow_slide_panel_td_cron_scheduled_bad_cron }} + {% endif %} + {% else %} + 🔄 {{ l.slideshow_slide_panel_td_cron_scheduled_loop }} + {% endif %} + diff --git a/views/slideshow/modal/add.jinja.html b/views/slideshow/modal/add.jinja.html index 4e066a0..41859b1 100644 --- a/views/slideshow/modal/add.jinja.html +++ b/views/slideshow/modal/add.jinja.html @@ -1,4 +1,4 @@ - +
+ +
+ + +
+
+