From cd400bb5595b9645b88dc617fea46153de798ea9 Mon Sep 17 00:00:00 2001 From: CasVT Date: Thu, 24 Apr 2025 21:10:28 +0200 Subject: [PATCH] Added option to enable/disable reminders (#77) --- backend/base/definitions.py | 1 + backend/features/reminders.py | 34 +++++++++++++++++------- backend/internals/db.py | 1 + backend/internals/db_migration.py | 15 +++++++++++ backend/internals/db_models.py | 44 ++++++++++++++++++++++--------- frontend/api.py | 9 ++++--- frontend/input_validation.py | 23 ++++++++++++++-- frontend/static/css/info.css | 21 +++++++++++++-- frontend/static/js/library.js | 3 +++ frontend/static/js/show.js | 2 ++ frontend/static/js/window.js | 5 ++++ frontend/templates/reminders.html | 4 +++ 12 files changed, 133 insertions(+), 29 deletions(-) diff --git a/backend/base/definitions.py b/backend/base/definitions.py index 5377707..5be5537 100644 --- a/backend/base/definitions.py +++ b/backend/base/definitions.py @@ -242,6 +242,7 @@ class ReminderData(GeneralReminderData): repeat_quantity: Union[str, None] repeat_interval: Union[int, None] _weekdays: Union[str, None] + enabled: bool def __post_init__(self) -> None: if self._weekdays is not None: diff --git a/backend/features/reminders.py b/backend/features/reminders.py index 7bbf673..ecc516a 100644 --- a/backend/features/reminders.py +++ b/backend/features/reminders.py @@ -57,7 +57,8 @@ class Reminder: repeat_quantity: Union[None, RepeatQuantity] = None, repeat_interval: Union[None, int] = None, weekdays: Union[None, List[WEEKDAY_NUMBER]] = None, - color: Union[None, str] = None + color: Union[None, str] = None, + enabled: Union[None, bool] = None ) -> ReminderData: """Edit the reminder. @@ -92,6 +93,10 @@ class Reminder: of the reminder, which is shown in the web-ui. Defaults to None. + enabled (Union[None, bool], optional): Whether the reminder should + be enabled. + Defaults to None. + Note about args: Either repeat_quantity and repeat_interval are given, weekdays is given or neither, but not both. @@ -107,7 +112,8 @@ class Reminder: LOGGER.info( f'Updating notification service {self.id}: ' + f'{title=}, {time=}, {notification_services=}, {text=}, ' - + f'{repeat_quantity=}, {repeat_interval=}, {weekdays=}, {color=}' + + f'{repeat_quantity=}, {repeat_interval=}, {weekdays=}, {color=}, ' + + f"{enabled=}" ) # Validate data @@ -151,7 +157,8 @@ class Reminder: lambda w: ",".join(map(str, sorted(w))) ), 'color': color, - 'notification_services': notification_services + 'notification_services': notification_services, + 'enabled': enabled } for k, v in new_values.items(): if ( @@ -163,7 +170,7 @@ class Reminder: if repeated_reminder: next_time = find_next_time( data["time"], - data["repeat_quantity"], + repeat_quantity, data["repeat_interval"], weekdays ) @@ -177,7 +184,8 @@ class Reminder: data["weekdays"], data["time"], data["color"], - data["notification_services"] + data["notification_services"], + data["enabled"] ) else: @@ -192,7 +200,8 @@ class Reminder: data["weekdays"], data["original_time"], data["color"], - data["notification_services"] + data["notification_services"], + data["enabled"] ) REMINDER_HANDLER.find_next_reminder(next_time) @@ -281,7 +290,8 @@ class Reminders: repeat_quantity: Union[None, RepeatQuantity] = None, repeat_interval: Union[None, int] = None, weekdays: Union[None, List[WEEKDAY_NUMBER]] = None, - color: Union[None, str] = None + color: Union[None, str] = None, + enabled: bool = True ) -> Reminder: """Add a reminder. @@ -312,6 +322,10 @@ class Reminders: reminder, which is shown in the web-ui. Defaults to None. + enabled (Union[None, bool], optional): Whether the reminder should + be enabled. + Defaults to None. + Note about args: Either repeat_quantity and repeat_interval are given, weekdays is given or neither, but not both. @@ -327,7 +341,8 @@ class Reminders: """ LOGGER.info( f'Adding reminder with {title=}, {time=}, {notification_services=}, ' + - f'{text=}, {repeat_quantity=}, {repeat_interval=}, {weekdays=}, {color=}') + f'{text=}, {repeat_quantity=}, {repeat_interval=}, {weekdays=}, {color=}' + + f'{enabled=}') # Validate data if time < datetime.utcnow().timestamp(): @@ -377,7 +392,8 @@ class Reminders: weekdays_str, original_time, color, - notification_services + notification_services, + enabled ) REMINDER_HANDLER.find_next_reminder(time) diff --git a/backend/internals/db.py b/backend/internals/db.py index cb2c4bf..5c2cc83 100644 --- a/backend/internals/db.py +++ b/backend/internals/db.py @@ -314,6 +314,7 @@ def setup_db() -> None: weekdays VARCHAR(13), color VARCHAR(7), + enabled BOOL NOT NULL DEFAULT 1, FOREIGN KEY (user_id) REFERENCES users(id) ); diff --git a/backend/internals/db_migration.py b/backend/internals/db_migration.py index d53a1b9..5ad7a9b 100644 --- a/backend/internals/db_migration.py +++ b/backend/internals/db_migration.py @@ -303,3 +303,18 @@ class MigrateUpdateManifest(DBMigrator): # So the migration doesn't do anything anymore, and a function used # doesn't exist anymore, so the whole migration is just removed. return + + +class MigrateAddEnabled(DBMigrator): + start_version = 10 + + def run(self) -> None: + # V10 -> V11 + + from backend.internals.db import get_db + + get_db().execute(""" + ALTER TABLE reminders + ADD enabled BOOL NOT NULL DEFAULT 1; + """) + return diff --git a/backend/internals/db_models.py b/backend/internals/db_models.py index 22fbd1c..5cc3659 100644 --- a/backend/internals/db_models.py +++ b/backend/internals/db_models.py @@ -547,7 +547,7 @@ class RemindersDB: id, title, text, color, time, original_time, repeat_quantity, repeat_interval, - weekdays AS _weekdays + weekdays AS _weekdays, enabled FROM reminders WHERE user_id = :user_id {id_filter}; @@ -576,7 +576,8 @@ class RemindersDB: weekdays: Union[str, None], original_time: Union[int, None], color: Union[str, None], - notification_services: List[int] + notification_services: List[int], + enabled: bool ) -> int: new_id = get_db().execute(""" INSERT INTO reminders( @@ -586,7 +587,8 @@ class RemindersDB: repeat_quantity, repeat_interval, weekdays, original_time, - color + color, + enabled ) VALUES ( :user_id, @@ -595,7 +597,8 @@ class RemindersDB: :rq, :ri, :wd, :ot, - :color + :color, + :enabled ); """, { @@ -607,7 +610,8 @@ class RemindersDB: "ri": repeat_interval, "wd": weekdays, "ot": original_time, - "color": color + "color": color, + "enabled": enabled } ).lastrowid @@ -628,7 +632,8 @@ class RemindersDB: weekdays: Union[str, None], original_time: Union[int, None], color: Union[str, None], - notification_services: List[int] + notification_services: List[int], + enabled: bool ) -> None: get_db().execute(""" UPDATE reminders @@ -640,7 +645,8 @@ class RemindersDB: repeat_interval = :ri, weekdays = :wd, original_time = :ot, - color = :color + color = :color, + enabled = :enabled WHERE id = :r_id; """, { @@ -652,6 +658,7 @@ class RemindersDB: "wd": weekdays, "ot": original_time, "color": color, + "enabled": enabled, "r_id": reminder_id } ) @@ -701,7 +708,14 @@ class UserlessRemindersDB: ).exists() or -1 def get_soonest_time(self) -> Union[int, None]: - return get_db().execute("SELECT MIN(time) FROM reminders;").exists() + """Get the earliest time a reminder goes off that is enabled. + + Returns: + Union[int, None]: The time, or None if there are no reminders. + """ + return get_db().execute( + "SELECT MIN(time) FROM reminders WHERE enabled = 1;" + ).exists() def fetch( self, @@ -717,7 +731,7 @@ class UserlessRemindersDB: title, text, color, time, original_time, repeat_quantity, repeat_interval, - weekdays AS _weekdays + weekdays AS _weekdays, enabled FROM reminders {time_filter}; """, @@ -745,7 +759,8 @@ class UserlessRemindersDB: weekdays: Union[str, None], original_time: Union[int, None], color: Union[str, None], - notification_services: List[int] + notification_services: List[int], + enabled: bool ) -> int: new_id = get_db().execute(""" INSERT INTO reminders( @@ -755,7 +770,8 @@ class UserlessRemindersDB: repeat_quantity, repeat_interval, weekdays, original_time, - color + color, + enabled ) VALUES ( :user_id, @@ -764,7 +780,8 @@ class UserlessRemindersDB: :rq, :ri, :wd, :ot, - :color + :color, + :enabled ); """, { @@ -776,7 +793,8 @@ class UserlessRemindersDB: "ri": repeat_interval, "wd": weekdays, "ot": original_time, - "color": color + "color": color, + "enabled": enabled } ).lastrowid diff --git a/frontend/api.py b/frontend/api.py index 397bab0..e82f7fb 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -306,7 +306,8 @@ def api_reminders_list(inputs: Dict[str, Any]): repeat_quantity=inputs['repeat_quantity'], repeat_interval=inputs['repeat_interval'], weekdays=inputs['weekdays'], - color=inputs['color'] + color=inputs['color'], + enabled=inputs['enabled'] ) return return_api(result.get().todict(), code=201) @@ -344,6 +345,7 @@ def api_get_reminder(inputs: Dict[str, Any], r_id: int): return return_api(result.todict()) elif request.method == 'PUT': + print(inputs) result = reminders.fetchone(r_id).update( title=inputs['title'], time=inputs['time'], @@ -352,7 +354,8 @@ def api_get_reminder(inputs: Dict[str, Any], r_id: int): repeat_quantity=inputs['repeat_quantity'], repeat_interval=inputs['repeat_interval'], weekdays=inputs['weekdays'], - color=inputs['color'] + color=inputs['color'], + enabled=inputs['enabled'] ) return return_api(result.todict()) @@ -453,7 +456,7 @@ def api_static_reminders_query(inputs: Dict[str, Any]): return return_api([r.todict() for r in result]) -@api.route('staticreminders/', StaticReminderData) +@api.route('/staticreminders/', StaticReminderData) @endpoint_wrapper def api_get_static_reminder(inputs: Dict[str, Any], s_id: int): reminders = StaticReminders( diff --git a/frontend/input_validation.py b/frontend/input_validation.py index ddff504..bc32f73 100644 --- a/frontend/input_validation.py +++ b/frontend/input_validation.py @@ -306,6 +306,23 @@ class ColorVariable(NonRequiredInputVariable): ) +class EnabledVariable(NonRequiredInputVariable): + name = "enabled" + description = "Whether the reminder should be enabled" + data_type = [DataType.BOOL] + default = True + + def validate(self) -> bool: + return isinstance(self.value, bool) + + +class EditEnabledVariable(EnabledVariable): + default = None + + def validate(self) -> bool: + return self.value is None or super().validate() + + class QueryVariable(InputVariable): name = "query" description = "The search term" @@ -538,7 +555,8 @@ class RemindersData(EndpointData): RepeatQuantityVariable, RepeatIntervalVariable, WeekDaysVariable, - ColorVariable + ColorVariable, + EnabledVariable ] ) ) @@ -570,7 +588,8 @@ class ReminderData(EndpointData): RepeatQuantityVariable, RepeatIntervalVariable, WeekDaysVariable, - ColorVariable + ColorVariable, + EditEnabledVariable ] ), delete=( diff --git a/frontend/static/css/info.css b/frontend/static/css/info.css index da01f60..9a60103 100644 --- a/frontend/static/css/info.css +++ b/frontend/static/css/info.css @@ -21,7 +21,7 @@ --gap: 1rem; display: flex; justify-content: center; - align-items: center; + align-items: stretch; flex-wrap: wrap; gap: var(--gap); } @@ -29,7 +29,7 @@ .sub-inputs > :where( input, select, button, label ) { - width: calc(50% - (var(--gap) / 2)); + flex: 1 0; } .form-container > form > button, @@ -54,6 +54,19 @@ opacity: 0; } +#enabled-container { + height: 4rem; + flex: 1 0; + + display: flex; + justify-content: center; + align-items: center; + flex-wrap: nowrap; + gap: 1rem; + + padding: 1rem; +} + #color-button { --color: var(--color-dark); background-color: var(--color); @@ -254,6 +267,7 @@ div.options > button { display: none; } +#info.show-add-static-reminder #enabled-container, #info.show-add-static-reminder #time-input, #info.show-add-static-reminder #normal-button, #info.show-add-static-reminder #repeat-button, @@ -274,6 +288,7 @@ div.options > button { width: 100%; } +#info.show-add-template #enabled-container, #info.show-add-template #template-selection, #info.show-add-template #time-input, #info.show-add-template #normal-button, @@ -307,6 +322,7 @@ div.options > button { width: 100%; } +#info.show-edit-static-reminder #enabled-container, #info.show-edit-static-reminder #template-selection, #info.show-edit-static-reminder #time-input, #info.show-edit-static-reminder #normal-button, @@ -326,6 +342,7 @@ div.options > button { width: 100%; } +#info.show-edit-template #enabled-container, #info.show-edit-template #template-selection, #info.show-edit-template #time-input, #info.show-edit-template #normal-button, diff --git a/frontend/static/js/library.js b/frontend/static/js/library.js index 01bb3d3..6c67d4f 100644 --- a/frontend/static/js/library.js +++ b/frontend/static/js/library.js @@ -81,6 +81,9 @@ function fillTable(table, results) { } else if (r.weekdays !== null) formatted_date += ` (each ${r.weekdays.map(d => week_days[d]).join(', ')})`; + if (!r.enabled) + formatted_date += ' (Disabled)'; + time.innerText = formatted_date; entry.appendChild(time); }; diff --git a/frontend/static/js/show.js b/frontend/static/js/show.js index 7486135..2004a21 100644 --- a/frontend/static/js/show.js +++ b/frontend/static/js/show.js @@ -1,5 +1,6 @@ function showAdd(type) { const default_service = getLocalStorage('default_service')['default_service']; + inputs.enabled.checked = true; inputs.template.value = '0'; inputs.title.value = ''; inputs.text.value = ''; @@ -69,6 +70,7 @@ function showEdit(id, type) { inputs.title.value = json.result.title; if (type === Types.reminder) { + inputs.enabled.checked = json.result.enabled; var trigger_date = new Date( (json.result.time + new Date(json.result.time * 1000).getTimezoneOffset() diff --git a/frontend/static/js/window.js b/frontend/static/js/window.js index a4785b0..84d12b5 100644 --- a/frontend/static/js/window.js +++ b/frontend/static/js/window.js @@ -1,6 +1,7 @@ const colors = ["#3c3c3c", "#49191e", "#171a42", "#083b06", "#3b3506", "#300e40"]; const inputs = { + 'enabled': document.querySelector('#enabled-input'), 'template': document.querySelector('#template-selection'), 'color_toggle': document.querySelector('#color-toggle'), 'color_button': document.querySelector('#color-button'), @@ -219,6 +220,8 @@ function submitInfo() { const cl = document.getElementById('info').classList; if (cl.contains('show-add-reminder')) { // Add reminder + data['enabled'] = inputs.enabled.checked; + data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60); @@ -262,6 +265,8 @@ function submitInfo() { } else if (cl.contains('show-edit-reminder')) { // Edit reminder + data['enabled'] = inputs.enabled.checked; + data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60); diff --git a/frontend/templates/reminders.html b/frontend/templates/reminders.html index 21cec88..8ed97e6 100644 --- a/frontend/templates/reminders.html +++ b/frontend/templates/reminders.html @@ -228,6 +228,10 @@ +
+ + +