Fixes issue #6

This commit is contained in:
CasVT
2023-05-28 11:43:06 +02:00
parent c3ce909554
commit 29eba69f74
14 changed files with 535 additions and 264 deletions

View File

@@ -8,7 +8,7 @@ from typing import Union
from flask import g
__DATABASE_VERSION__ = 4
__DATABASE_VERSION__ = 5
class Singleton(type):
_instances = {}
@@ -100,6 +100,87 @@ def migrate_db(current_db_version: int) -> None:
""")
current_db_version = 4
if current_db_version == 4:
# V4 -> V5
cursor.executescript("""
BEGIN TRANSACTION;
PRAGMA defer_foreign_keys = ON;
-- Reminders
INSERT INTO reminder_services(reminder_id, notification_service_id)
SELECT id, notification_service
FROM reminders;
CREATE TEMPORARY TABLE temp_reminders AS
SELECT id, user_id, title, text, time, repeat_quantity, repeat_interval, original_time, color
FROM reminders;
DROP TABLE reminders;
CREATE TABLE reminders(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
text TEXT,
time INTEGER NOT NULL,
repeat_quantity VARCHAR(15),
repeat_interval INTEGER,
original_time INTEGER,
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO reminders
SELECT * FROM temp_reminders;
-- Static reminders
INSERT INTO reminder_services(static_reminder_id, notification_service_id)
SELECT id, notification_service
FROM static_reminders;
CREATE TEMPORARY TABLE temp_static_reminders AS
SELECT id, user_id, title, text, color
FROM static_reminders;
DROP TABLE static_reminders;
CREATE TABLE static_reminders(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
text TEXT,
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO static_reminders
SELECT * FROM temp_static_reminders;
-- Templates
INSERT INTO reminder_services(template_id, notification_service_id)
SELECT id, notification_service
FROM templates;
CREATE TEMPORARY TABLE temp_templates AS
SELECT id, user_id, title, text, color
FROM templates;
DROP TABLE templates;
CREATE TABLE templates(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
text TEXT,
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO templates
SELECT * FROM temp_templates;
COMMIT;
""")
current_db_version = 5
return
def setup_db() -> None:
@@ -128,7 +209,6 @@ def setup_db() -> None:
title VARCHAR(255) NOT NULL,
text TEXT,
time INTEGER NOT NULL,
notification_service INTEGER NOT NULL,
repeat_quantity VARCHAR(15),
repeat_interval INTEGER,
@@ -136,32 +216,41 @@ def setup_db() -> None:
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (notification_service) REFERENCES notification_services(id)
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS templates(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
text TEXT,
notification_service INTEGER NOT NULL,
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (notification_service) REFERENCES notification_services(id)
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS static_reminders(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
text TEXT,
notification_service INTEGER NOT NULL,
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (notification_service) REFERENCES notification_services(id)
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS reminder_services(
reminder_id INTEGER,
static_reminder_id INTEGER,
template_id INTEGER,
notification_service_id INTEGER NOT NULL,
FOREIGN KEY (reminder_id) REFERENCES reminders(id)
ON DELETE CASCADE,
FOREIGN KEY (static_reminder_id) REFERENCES static_reminders(id)
ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES templates(id)
ON DELETE CASCADE,
FOREIGN KEY (notification_service_id) REFERENCES notification_services(id)
);
CREATE TABLE IF NOT EXISTS config(
key VARCHAR(255) PRIMARY KEY,

View File

@@ -82,24 +82,39 @@ class NotificationService:
"""
# Check if no reminders exist with this service
cursor = get_db()
cursor.execute(
"SELECT 1 FROM reminders WHERE notification_service = ? LIMIT 1;",
cursor.execute("""
SELECT 1
FROM reminder_services
WHERE notification_service_id = ?
AND reminder_id IS NOT NULL
LIMIT 1;
""",
(self.id,)
)
if cursor.fetchone():
raise NotificationServiceInUse('reminder')
# Check if no templates exist with this service
cursor.execute(
"SELECT 1 FROM templates WHERE notification_service = ? LIMIT 1;",
cursor.execute("""
SELECT 1
FROM reminder_services
WHERE notification_service_id = ?
AND template_id IS NOT NULL
LIMIT 1;
""",
(self.id,)
)
if cursor.fetchone():
raise NotificationServiceInUse('template')
# Check if no static reminders exist with this service
cursor.execute(
"SELECT 1 FROM static_reminders WHERE notification_service = ? LIMIT 1;",
cursor.execute("""
SELECT 1
FROM reminder_services
WHERE notification_service_id = ?
AND static_reminder_id IS NOT NULL
LIMIT 1;
""",
(self.id,)
)
if cursor.fetchone():

View File

@@ -56,25 +56,31 @@ class ReminderHandler:
SELECT
r.id,
r.title, r.text,
r.repeat_quantity, r.repeat_interval, r.original_time,
ns.url
r.repeat_quantity, r.repeat_interval, r.original_time
FROM reminders r
INNER JOIN notification_services ns
ON r.notification_service = ns.id
WHERE time = ?;
""", (time,))
reminders = list(map(dict, cursor))
for reminder in reminders:
cursor.execute("""
SELECT url
FROM reminder_services rs
INNER JOIN notification_services ns
ON rs.notification_service_id = ns.id
WHERE rs.reminder_id = ?;
""", (reminder['id'],))
# Send of reminder
a = Apprise()
a.add(reminder['url'])
for url in cursor:
a.add(url['url'])
a.notify(title=reminder["title"], body=reminder["text"])
if reminder['repeat_quantity'] is None:
# Delete the reminder from the database
cursor.execute(
"DELETE FROM reminders WHERE id = ?",
"DELETE FROM reminders WHERE id = ?;",
(reminder['id'],)
)
else:
@@ -155,30 +161,33 @@ class Reminder:
"""
reminder = get_db(dict).execute("""
SELECT
r.id,
r.title, r.text,
r.time,
r.notification_service,
ns.title AS notification_service_title,
r.repeat_quantity,
r.repeat_interval,
r.color
FROM reminders r
INNER JOIN notification_services ns
ON r.notification_service = ns.id
WHERE r.id = ?
id,
title, text,
time,
repeat_quantity,
repeat_interval,
color
FROM reminders
WHERE id = ?
LIMIT 1;
""",
(self.id,)
).fetchone()
reminder = dict(reminder)
return dict(reminder)
reminder['notification_services'] = list(map(lambda r: r[0], get_db().execute("""
SELECT notification_service_id
FROM reminder_services
WHERE reminder_id = ?;
""", (self.id,))))
return reminder
def update(
self,
title: str = None,
time: int = None,
notification_service: int = None,
notification_services: List[int] = None,
text: str = None,
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"] = None,
repeat_interval: int = None,
@@ -189,12 +198,15 @@ class Reminder:
Args:
title (str): The new title of the entry. Defaults to None.
time (int): The new UTC epoch timestamp the the reminder should be send. Defaults to None.
notification_service (int): The new id of the notification service to use to send the reminder. Defaults to None.
notification_services (List[int]): The new list of id's of the notification services to use to send the reminder. Defaults to None.
text (str, optional): The new body of the reminder. Defaults to None.
repeat_quantity (Literal["years", "months", "weeks", "days", "hours", "minutes"], optional): The new quantity of the repeat specified for the reminder. Defaults to None.
repeat_interval (int, optional): The new amount of repeat_quantity, like "5" (hours). Defaults to None.
color (str, optional): The new hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.
Raises:
NotificationServiceNotFound: One of the notification services was not found
Returns:
dict: The new reminder info
"""
@@ -218,7 +230,6 @@ class Reminder:
new_values = {
'title': title,
'time': time,
'notification_service': notification_service,
'text': text,
'repeat_quantity': repeat_quantity,
'repeat_interval': repeat_interval,
@@ -229,53 +240,63 @@ class Reminder:
data[k] = v
# Update database
try:
if not repeated_reminder:
next_time = data["time"]
cursor.execute("""
UPDATE reminders
SET
title=?, text=?,
time=?, notification_service=?,
repeat_quantity=?, repeat_interval=?,
color=?
WHERE id = ?;
""", (
data["title"],
data["text"],
data["time"],
data["notification_service"],
data["repeat_quantity"],
data["repeat_interval"],
data["color"],
self.id
))
else:
next_time = _find_next_time(
if not repeated_reminder:
next_time = data["time"]
cursor.execute("""
UPDATE reminders
SET
title=?, text=?,
time=?,
repeat_quantity=?, repeat_interval=?,
color=?
WHERE id = ?;
""", (
data["title"],
data["text"],
data["time"],
data["repeat_quantity"], data["repeat_interval"]
data["repeat_quantity"],
data["repeat_interval"],
data["color"],
self.id
))
else:
next_time = _find_next_time(
data["time"],
data["repeat_quantity"], data["repeat_interval"]
)
cursor.execute("""
UPDATE reminders
SET
title=?, text=?,
time=?,
repeat_quantity=?, repeat_interval=?, original_time=?,
color=?
WHERE id = ?;
""", (
data["title"],
data["text"],
next_time,
data["repeat_quantity"],
data["repeat_interval"],
data["time"],
data["color"],
self.id
))
if notification_services:
cursor.connection.isolation_level = None
cursor.execute("BEGIN TRANSACTION;")
cursor.execute("DELETE FROM reminder_services WHERE reminder_id = ?", (self.id,))
try:
cursor.executemany(
"INSERT INTO reminder_services(reminder_id, notification_service_id) VALUES (?,?)",
((self.id, s) for s in notification_services)
)
cursor.execute("""
UPDATE reminders
SET
title=?, text=?,
time=?, notification_service=?,
repeat_quantity=?, repeat_interval=?, original_time=?,
color=?
WHERE id = ?;
""", (
data["title"],
data["text"],
next_time,
data["notification_service"],
data["repeat_quantity"],
data["repeat_interval"],
data["time"],
data["color"],
self.id
))
except IntegrityError:
raise NotificationServiceNotFound
cursor.execute("COMMIT;")
except IntegrityError:
raise NotificationServiceNotFound
cursor.connection.isolation_level = ""
reminder_handler.find_next_reminder(next_time)
return self.get()
@@ -306,7 +327,7 @@ class Reminders:
sort_by (Literal["time", "time_reversed", "title", "title_reversed"], optional): How to sort the result. Defaults to "time".
Returns:
List[dict]: The id, title, text, time, notification_service, notification_service_title and color of each reminder
List[dict]: The id, title, text, time and color of each reminder
"""
sort_function = self.sort_functions.get(
sort_by,
@@ -316,18 +337,14 @@ class Reminders:
# Fetch all reminders
reminders: list = list(map(dict, get_db(dict).execute("""
SELECT
r.id,
r.title, r.text,
r.time,
r.notification_service,
ns.title AS notification_service_title,
r.repeat_quantity,
r.repeat_interval,
r.color
FROM reminders r
INNER JOIN notification_services ns
ON r.notification_service = ns.id
WHERE r.user_id = ?;
id,
title, text,
time,
repeat_quantity,
repeat_interval,
color
FROM reminders
WHERE user_id = ?;
""",
(self.user_id,)
)))
@@ -368,7 +385,7 @@ class Reminders:
self,
title: str,
time: int,
notification_service: int,
notification_services: List[int],
text: str = '',
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"] = None,
repeat_interval: int = None,
@@ -379,12 +396,15 @@ class Reminders:
Args:
title (str): The title of the entry
time (int): The UTC epoch timestamp the the reminder should be send.
notification_service (int): The id of the notification service to use to send the reminder.
notification_services (List[int]): The id's of the notification services to use to send the reminder.
text (str, optional): The body of the reminder. Defaults to ''.
repeat_quantity (Literal["years", "months", "weeks", "days", "hours", "minutes"], optional): The quantity of the repeat specified for the reminder. Defaults to None.
repeat_interval (int, optional): The amount of repeat_quantity, like "5" (hours). Defaults to None.
color (str, optional): The hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.
Raises:
NotificationServiceNotFound: One of the notification services was not found
Returns:
dict: The info about the reminder
"""
@@ -397,21 +417,28 @@ class Reminders:
elif repeat_quantity is not None and repeat_interval is None:
raise InvalidKeyValue('repeat_interval', repeat_interval)
cursor = get_db()
if repeat_quantity is None and repeat_interval is None:
id = cursor.execute("""
INSERT INTO reminders(user_id, title, text, time, color)
VALUES (?, ?, ?, ?, ?);
""", (self.user_id, title, text, time, color)
).lastrowid
else:
id = cursor.execute("""
INSERT INTO reminders(user_id, title, text, time, repeat_quantity, repeat_interval, original_time, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
""", (self.user_id, title, text, time, repeat_quantity, repeat_interval, time, color)
).lastrowid
try:
if repeat_quantity is None and repeat_interval is None:
id = get_db().execute("""
INSERT INTO reminders(user_id, title, text, time, notification_service, color)
VALUES (?,?,?,?,?, ?);
""", (self.user_id, title, text, time, notification_service, color)
).lastrowid
else:
id = get_db().execute("""
INSERT INTO reminders(user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, original_time, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
""", (self.user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, time, color)
).lastrowid
cursor.executemany(
"INSERT INTO reminder_services(reminder_id, notification_service_id) VALUES (?, ?);",
((id, service) for service in notification_services)
)
except IntegrityError:
raise NotificationServiceNotFound
reminder_handler.find_next_reminder(time)
# Return info
@@ -419,7 +446,7 @@ class Reminders:
def test_reminder(
title: str,
notification_service: int,
notification_services: List[int],
text: str = ''
) -> None:
"""Test send a reminder draft
@@ -430,12 +457,14 @@ def test_reminder(
text (str, optional): The body of the reminder. Defaults to ''.
"""
a = Apprise()
url = get_db(dict).execute(
"SELECT url FROM notification_services WHERE id = ? LIMIT 1;",
(notification_service,)
).fetchone()
if not url:
raise NotificationServiceNotFound
a.add(url[0])
cursor = get_db(dict)
for service in notification_services:
url = cursor.execute(
"SELECT url FROM notification_services WHERE id = ? LIMIT 1;",
(service,)
).fetchone()
if not url:
raise NotificationServiceNotFound
a.add(url[0])
a.notify(title=title, body=text)
return

View File

@@ -5,7 +5,8 @@ from typing import List
from apprise import Apprise
from backend.custom_exceptions import NotificationServiceNotFound, ReminderNotFound
from backend.custom_exceptions import (NotificationServiceNotFound,
ReminderNotFound)
from backend.db import get_db
@@ -30,26 +31,29 @@ class StaticReminder:
"""
reminder = get_db(dict).execute("""
SELECT
r.id,
r.title, r.text,
r.notification_service,
ns.title AS notification_service_title,
r.color
FROM static_reminders r
INNER JOIN notification_services ns
ON r.notification_service = ns.id
WHERE r.id = ?
id,
title, text,
color
FROM static_reminders
WHERE id = ?
LIMIT 1;
""",
(self.id,)
).fetchone()
reminder = dict(reminder)
return dict(reminder)
reminder['notification_services'] = list(map(lambda r: r[0], get_db().execute("""
SELECT notification_service_id
FROM reminder_services
WHERE static_reminder_id = ?;
""", (self.id,))))
return reminder
def update(
self,
title: str = None,
notification_service: int = None,
notification_services: List[int] = None,
text: str = None,
color: str = None
) -> dict:
@@ -57,12 +61,12 @@ class StaticReminder:
Args:
title (str, optional): The new title of the entry. Defaults to None.
notification_service (int, optional): The new id of the notification service to use to send the reminder. Defaults to None.
notification_services (List[int], optional): The new id's of the notification services to use to send the reminder. Defaults to None.
text (str, optional): The new body of the reminder. Defaults to None.
color (str, optional): The new hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.
Raises:
NotificationServiceNotFound: The notification service with the given id was not found
NotificationServiceNotFound: One of the notification services was not found
Returns:
dict: The new static reminder info
@@ -71,7 +75,6 @@ class StaticReminder:
data = self.get()
new_values = {
'title': title,
'notification_service': notification_service,
'text': text,
'color': color
}
@@ -80,22 +83,32 @@ class StaticReminder:
data[k] = v
# Update database
try:
get_db().execute("""
UPDATE static_reminders
SET
title = ?, text = ?,
notification_service = ?,
color = ?
WHERE id = ?;
""",
(data['title'], data['text'],
data['notification_service'],
data['color'],
self.id)
)
except IntegrityError:
raise NotificationServiceNotFound
cursor = get_db()
cursor.execute("""
UPDATE static_reminders
SET
title = ?, text = ?,
color = ?
WHERE id = ?;
""",
(data['title'], data['text'],
data['color'],
self.id)
)
if notification_services:
cursor.connection.isolation_level = None
cursor.execute("BEGIN TRANSACTION;")
cursor.execute("DELETE FROM reminder_services WHERE static_reminder_id = ?", (self.id,))
try:
cursor.executemany(
"INSERT INTO reminder_services(static_reminder_id, notification_service_id) VALUES (?,?)",
((self.id, s) for s in notification_services)
)
cursor.execute("COMMIT;")
except IntegrityError:
raise NotificationServiceNotFound
cursor.connection.isolation_level = ""
return self.get()
@@ -116,22 +129,18 @@ class StaticReminders:
"""Get all static reminders
Returns:
List[dict]: The id, title, text, notification_service, notification_service_title and color of each static reminder
List[dict]: The id, title, text and color of each static reminder
"""
reminders: list = list(map(
dict,
get_db(dict).execute("""
SELECT
r.id,
r.title, r.text,
r.notification_service,
ns.title AS notification_service_title,
r.color
FROM static_reminders r
INNER JOIN notification_services ns
ON r.notification_service = ns.id
WHERE r.user_id = ?
ORDER BY r.title, r.id;
id,
title, text,
color
FROM static_reminders
WHERE user_id = ?
ORDER BY title, id;
""",
(self.user_id,)
)
@@ -153,7 +162,7 @@ class StaticReminders:
def add(
self,
title: str,
notification_service: int,
notification_services: List[int],
text: str = '',
color: str = None
) -> StaticReminder:
@@ -161,23 +170,29 @@ class StaticReminders:
Args:
title (str): The title of the entry
notification_service (int): The id of the notification service to use to send the reminder.
notification_services (List[int]): The id's of the notification services to use to send the reminder.
text (str, optional): The body of the reminder. Defaults to ''.
color (str, optional): The hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.
Raises:
NotificationServiceNotFound: The notification service with the given id was not found
NotificationServiceNotFound: One of the notification services was not found
Returns:
StaticReminder: A StaticReminder instance representing the newly created static reminder
"""
cursor = get_db()
id = cursor.execute("""
INSERT INTO static_reminders(user_id, title, text, color)
VALUES (?,?,?,?);
""",
(self.user_id, title, text, color)
).lastrowid
try:
id = get_db().execute("""
INSERT INTO static_reminders(user_id, title, text, notification_service, color)
VALUES (?,?,?,?,?);
""",
(self.user_id, title, text, notification_service, color)
).lastrowid
cursor.executemany(
"INSERT INTO reminder_services(static_reminder_id, notification_service_id) VALUES (?, ?);",
((id, service) for service in notification_services)
)
except IntegrityError:
raise NotificationServiceNotFound
@@ -192,18 +207,26 @@ class StaticReminders:
Raises:
ReminderNotFound: The static reminder with the given id was not found
"""
reminder = get_db(dict).execute("""
SELECT r.title, r.text, ns.url
FROM static_reminders r
INNER JOIN notification_services ns
ON r.notification_service = ns.id
WHERE r.id = ?;
cursor = get_db(dict)
reminder = cursor.execute("""
SELECT title, text
FROM static_reminders
WHERE id = ?
LIMIT 1;
""", (id,)).fetchone()
if not reminder:
raise ReminderNotFound
reminder = dict(reminder)
a = Apprise()
a.add(reminder['url'])
cursor.execute("""
SELECT url
FROM reminder_services rs
INNER JOIN notification_services ns
ON rs.notification_service_id = ns.id
WHERE rs.static_reminder_id = ?;
""", (id,))
for url in cursor:
a.add(url['url'])
a.notify(title=reminder['title'], body=reminder['text'])
return

View File

@@ -31,7 +31,6 @@ class Template:
SELECT
id,
title, text,
notification_service,
color
FROM templates
WHERE id = ?
@@ -39,12 +38,19 @@ class Template:
""",
(self.id,)
).fetchone()
template = dict(template)
return dict(template)
template['notification_services'] = list(map(lambda r: r[0], get_db().execute("""
SELECT notification_service_id
FROM reminder_services
WHERE template_id = ?;
""", (self.id,))))
return template
def update(self,
title: str = None,
notification_service: int = None,
notification_services: List[int] = None,
text: str = None,
color: str = None
) -> dict:
@@ -52,7 +58,7 @@ class Template:
Args:
title (str): The new title of the entry. Defaults to None.
notification_service (int): The new id of the notification service to use to send the reminder. Defaults to None.
notification_services (List[int]): The new id's of the notification services to use to send the reminder. Defaults to None.
text (str, optional): The new body of the template. Defaults to None.
color (str, optional): The new hex code of the color of the template, which is shown in the web-ui. Defaults to None.
@@ -64,7 +70,6 @@ class Template:
data = self.get()
new_values = {
'title': title,
'notification_service': notification_service,
'text': text,
'color': color
}
@@ -72,20 +77,30 @@ class Template:
if k in ('color',) or v is not None:
data[k] = v
try:
cursor.execute("""
UPDATE templates
SET title=?, notification_service=?, text=?, color=?
WHERE id = ?;
""", (
data['title'],
data['notification_service'],
data['text'],
data['color'],
self.id
))
except IntegrityError:
raise NotificationServiceNotFound
cursor.execute("""
UPDATE templates
SET title=?, text=?, color=?
WHERE id = ?;
""", (
data['title'],
data['text'],
data['color'],
self.id
))
if notification_services:
cursor.connection.isolation_level = None
cursor.execute("BEGIN TRANSACTION;")
cursor.execute("DELETE FROM reminder_services WHERE template_id = ?", (self.id,))
try:
cursor.executemany(
"INSERT INTO reminder_services(template_id, notification_service_id) VALUES (?,?)",
((self.id, s) for s in notification_services)
)
cursor.execute("COMMIT;")
except IntegrityError:
raise NotificationServiceNotFound
cursor.connection.isolation_level = ""
return self.get()
@@ -105,13 +120,12 @@ class Templates:
"""Get all templates
Returns:
List[dict]: The id, title, text, notification_service and color
List[dict]: The id, title, text and color
"""
templates: list = list(map(dict, get_db(dict).execute("""
SELECT
id,
title, text,
notification_service,
color
FROM templates
WHERE user_id = ?
@@ -136,7 +150,7 @@ class Templates:
def add(
self,
title: str,
notification_service: int,
notification_services: List[int],
text: str = '',
color: str = None
) -> Template:
@@ -144,20 +158,26 @@ class Templates:
Args:
title (str): The title of the entry
notification_service (int): The id of the notification service to use to send the reminder.
notification_services (List[int]): The id's of the notification services to use to send the reminder.
text (str, optional): The body of the reminder. Defaults to ''.
color (str, optional): The hex code of the color of the template, which is shown in the web-ui. Defaults to None.
Returns:
Template: The info about the template
"""
cursor = get_db()
id = cursor.execute("""
INSERT INTO templates(user_id, title, text, color)
VALUES (?,?,?,?);
""",
(self.user_id, title, text, color)
).lastrowid
try:
id = get_db().execute("""
INSERT INTO templates(user_id, title, text, notification_service, color)
VALUES (?,?,?,?,?);
""",
(self.user_id, title, text, notification_service, color)
).lastrowid
cursor.executemany(
"INSERT INTO reminder_services(template_id, notification_service_id) VALUES (?, ?);",
((id, service) for service in notification_services)
)
except IntegrityError:
raise NotificationServiceNotFound

View File

@@ -97,6 +97,8 @@ class User:
"""
cursor = get_db()
cursor.execute("DELETE FROM reminders WHERE user_id = ?", (self.user_id,))
cursor.execute("DELETE FROM templates WHERE user_id = ?", (self.user_id,))
cursor.execute("DELETE FROM static_reminders WHERE user_id = ?", (self.user_id,))
cursor.execute("DELETE FROM notification_services WHERE user_id = ?", (self.user_id,))
cursor.execute("DELETE FROM users WHERE id = ?", (self.user_id,))
return

View File

@@ -81,7 +81,7 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
if value is not None:
# Check value and optionally convert
if key in ('time', 'notification_service'):
if key == 'time':
try:
value = int(value)
except (ValueError, TypeError):
@@ -112,6 +112,15 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
if not color_regex.search(value):
raise InvalidKeyValue(key, value)
elif key == 'notification_services':
if not value:
raise KeyNotFound(key)
if not isinstance(value, list):
raise InvalidKeyValue(key, value)
for v in value:
if not isinstance(v, int):
raise InvalidKeyValue(key, value)
else:
if key == 'sort_by':
value = 'time'
@@ -403,13 +412,13 @@ def api_reminders_list():
sort_by: how to sort the result. Allowed values are 'title', 'title_reversed', 'time' and 'time_reversed'
Returns:
200:
The id, title, text, time, notification_service, notification_service_title, repeat_quantity, repeat_interval and color of each reminder
The id, title, text, time, repeat_quantity, repeat_interval and color of each reminder
POST:
Description: Add a reminder
Parameters (body):
title (required): the title of the reminder
time (required): the UTC epoch timestamp that the reminder should be sent at
notification_service (required): the id of the notification service to use to send the notification
notification_services (required): array of the id's of the notification services to use to send the notification
text: the body of the reminder
repeat_quantity ('years', 'months', 'weeks', 'days', 'hours', 'minutes'): The quantity of the repeat_interval
repeat_interval: The number of the interval
@@ -419,6 +428,8 @@ def api_reminders_list():
The info about the new reminder entry
400:
KeyNotFound: One of the required parameters was not given
404:
NotificationServiceNotFound: One of the notification services was not found
"""
reminders: Reminders = g.user_data.reminders
@@ -431,7 +442,7 @@ def api_reminders_list():
data = request.get_json()
title = extract_key(data, 'title')
time = extract_key(data, 'time')
notification_service = extract_key(data, 'notification_service')
notification_services = extract_key(data, 'notification_services')
text = extract_key(data, 'text', check_existence=False)
repeat_quantity = extract_key(data, 'repeat_quantity', check_existence=False)
repeat_interval = extract_key(data, 'repeat_interval', check_existence=False)
@@ -439,7 +450,7 @@ def api_reminders_list():
result = reminders.add(title=title,
time=time,
notification_service=notification_service,
notification_services=notification_services,
text=text,
repeat_quantity=repeat_quantity,
repeat_interval=repeat_interval,
@@ -493,10 +504,10 @@ def api_test_reminder():
"""
data = request.get_json()
title = extract_key(data, 'title')
notification_service = extract_key(data, 'notification_service')
notification_services = extract_key(data, 'notification_services')
text = extract_key(data, 'text', check_existence=False)
test_reminder(title, notification_service, text)
test_reminder(title, notification_services, text)
return return_api({}, code=201)
@api.route('/reminders/<int:r_id>', methods=['GET', 'PUT', 'DELETE'])
@@ -522,7 +533,7 @@ def api_get_reminder(r_id: int):
Parameters (body):
title: The new title of the entry.
time: The new UTC epoch timestamp the the reminder should be send.
notification_service: The new id of the notification service to use to send the reminder.
notification_services: Array of the new id's of the notification services to use to send the reminder.
text: The new body of the reminder.
repeat_quantity ('years', 'months', 'weeks', 'days', 'hours', 'minutes'): The new quantity of the repeat_interval.
repeat_interval: The new number of the interval.
@@ -531,7 +542,8 @@ def api_get_reminder(r_id: int):
200:
Reminder updated successfully
404:
No reminder found with the given id
ReminderNotFound: No reminder found with the given id
NotificationServiceNotFound: One of the notification services was not found
DELETE:
Description: Delete the reminder
Returns:
@@ -549,15 +561,15 @@ def api_get_reminder(r_id: int):
data = request.get_json()
title = extract_key(data, 'title', check_existence=False)
time = extract_key(data, 'time', check_existence=False)
notification_service = extract_key(data, 'notification_service', check_existence=False)
notification_services = extract_key(data, 'notification_services', check_existence=False)
text = extract_key(data, 'text', check_existence=False)
repeat_quantity = extract_key(data, 'repeat_quantity', check_existence=False)
repeat_interval = extract_key(data, 'repeat_interval', check_existence=False)
color = extract_key(data, 'color', check_existence=False)
result = reminders.fetchone(r_id).update(title=title,
time=time,
notification_service=notification_service,
notification_services=notification_services,
text=text,
repeat_quantity=repeat_quantity,
repeat_interval=repeat_interval,
@@ -585,12 +597,12 @@ def api_get_templates():
Description: Get a list of all templates
Returns:
200:
The id, title, notification_service, text and color of every template
The id, title, text and color of every template
POST:
Description: Add a template
Parameters (body):
title (required): the title of the template
notification_service (required): the id of the notification service to use to send the notification
notification_services (required): array of the id's of the notification services to use to send the notification
text: the body of the template
color: the hex code of the color of the template, which is shown in the web-ui
Returns:
@@ -598,6 +610,8 @@ def api_get_templates():
The info about the new template entry
400:
KeyNotFound: One of the required parameters was not given
404:
NotificationServiceNotFound: One of the notification services was not found
"""
templates: Templates = g.user_data.templates
@@ -608,12 +622,12 @@ def api_get_templates():
elif request.method == 'POST':
data = request.get_json()
title = extract_key(data, 'title')
notification_service = extract_key(data, 'notification_service')
notification_services = extract_key(data, 'notification_services')
text = extract_key(data, 'text', check_existence=False)
color = extract_key(data, 'color', check_existence=False)
result = templates.add(title=title,
notification_service=notification_service,
notification_services=notification_services,
text=text,
color=color)
return return_api(result.get(), code=201)
@@ -640,14 +654,15 @@ def api_get_template(t_id: int):
Description: Edit the template
Parameters (body):
title: The new title of the entry.
notification_service: The new id of the notification service to use to send the reminder.
notification_services: The new array of id's of the notification services to use to send the reminder.
text: The new body of the template.
color: The new hex code of the color of the template.
Returns:
200:
Template updated successfully
404:
No template found with the given id
TemplateNotFound: No template found with the given id
NotificationServiceNotFound: One of the notification services was not found
DELETE:
Description: Delete the template
Returns:
@@ -665,12 +680,12 @@ def api_get_template(t_id: int):
elif request.method == 'PUT':
data = request.get_json()
title = extract_key(data, 'title', check_existence=False)
notification_service = extract_key(data, 'notification_service', check_existence=False)
notification_services = extract_key(data, 'notification_services', check_existence=False)
text = extract_key(data, 'text', check_existence=False)
color = extract_key(data, 'color', check_existence=False)
result = template.update(title=title,
notification_service=notification_service,
notification_services=notification_services,
text=text,
color=color)
return return_api(result)
@@ -696,12 +711,12 @@ def api_static_reminders_list():
Description: Get a list of all static reminders
Returns:
200:
The id, title, text, notification_service, notification_service_title and color of each static reminder
The id, title, text and color of each static reminder
POST:
Description: Add a static reminder
Parameters (body):
title (required): the title of the static reminder
notification_service (required): the id of the notification service to use to send the notification
notification_services (required): array of the id's of the notification services to use to send the notification
text: the body of the static reminder
color: The hex code of the color of the static reminder, which is shown in the web-ui
Returns:
@@ -709,6 +724,8 @@ def api_static_reminders_list():
The info about the new static reminder entry
400:
KeyNotFound: One of the required parameters was not given
404:
NotificationServiceNotFound: One of the notification services was not found
"""
reminders: StaticReminders = g.user_data.static_reminders
@@ -719,12 +736,12 @@ def api_static_reminders_list():
elif request.method == 'POST':
data = request.get_json()
title = extract_key(data, 'title')
notification_service = extract_key(data, 'notification_service')
notification_services = extract_key(data, 'notification_services')
text = extract_key(data, 'text', check_existence=False)
color = extract_key(data, 'color', check_existence=False)
result = reminders.add(title=title,
notification_service=notification_service,
notification_services=notification_services,
text=text,
color=color)
return return_api(result.get(), code=201)
@@ -756,14 +773,15 @@ def api_get_static_reminder(r_id: int):
Description: Edit the static reminder
Parameters (body):
title: The new title of the static reminder.
notification_service: The new id of the notification service to use to send the reminder.
notification_services: The new array of id's of the notification services to use to send the reminder.
text: The new body of the static reminder.
color: The new hex code of the color of the static reminder, which is shown in the web-ui.
Returns:
200:
Static reminder updated successfully
404:
No static reminder found with the given id
ReminderNotFound: No static reminder found with the given id
NotificationServiceNotFound: One of the notification services was not found
DELETE:
Description: Delete the static reminder
Returns:
@@ -784,12 +802,12 @@ def api_get_static_reminder(r_id: int):
elif request.method == 'PUT':
data = request.get_json()
title = extract_key(data, 'title', check_existence=False)
notification_service = extract_key(data, 'notification_service', check_existence=False)
notification_services = extract_key(data, 'notification_services', check_existence=False)
text = extract_key(data, 'text', check_existence=False)
color = extract_key(data, 'color', check_existence=False)
result = reminders.fetchone(r_id).update(title=title,
notification_service=notification_service,
notification_services=notification_services,
text=text,
color=color)
return return_api(result)

View File

@@ -81,6 +81,34 @@
border-color: var(--color-white);
}
.notification-service-list {
width: 100%;
max-height: 10rem;
overflow-y: auto;
display: flex;
flex-direction: column;
border: 2px solid var(--color-gray);
border-radius: 4px;
box-shadow: var(--default-shadow);
}
.notification-service-list > div {
display: flex;
gap: 1rem;
padding: .5rem .75rem;
}
.notification-service-list > div:not(:first-child) {
border-top: 1px solid var(--color-gray);
}
.notification-service-list > div > input {
width: fit-content;
}
.repeat-bar,
.repeat-edit-bar {
display: flex;
@@ -188,7 +216,7 @@ div.options > button {
#info.show-add-static-reminder #template-selection,
#info.show-add-static-reminder #color-toggle,
#info.show-add-static-reminder #notification-service-input {
#info.show-add-static-reminder #toggle-notification-service-list {
width: 100%;
}
@@ -207,7 +235,7 @@ div.options > button {
}
#info.show-add-template #color-toggle,
#info.show-add-template #notification-service-input {
#info.show-add-template #toggle-notification-service-list {
width: 100%;
}
@@ -236,7 +264,7 @@ div.options > button {
}
#info.show-edit-static-reminder #color-toggle,
#info.show-edit-static-reminder #notification-service-input {
#info.show-edit-static-reminder #toggle-notification-service-list {
width: 100%;
}
@@ -254,6 +282,6 @@ div.options > button {
}
#info.show-edit-template #color-toggle,
#info.show-edit-template #notification-service-input {
#info.show-edit-template #toggle-notification-service-list {
width: 100%;
}

View File

@@ -14,15 +14,22 @@ function fillNotificationSelection() {
if (json.result.length) {
document.getElementById('add-reminder').classList.remove('error', 'error-icon');
const options = document.getElementById('notification-service-input');
options.innerHTML = '';
inputs.notification_service.innerHTML = '';
json.result.forEach(service => {
const entry = document.createElement('option');
entry.value = service.id;
entry.innerText = service.title;
options.appendChild(entry);
const entry = document.createElement('div');
const select = document.createElement('input');
select.dataset.id = service.id;
select.type = 'checkbox';
entry.appendChild(select);
const title = document.createElement('p');
title.innerText = service.title;
entry.appendChild(title);
inputs.notification_service.appendChild(entry);
});
options.querySelector(':nth-child(1)').setAttribute('selected', '');
inputs.notification_service.querySelector(':first-child input').checked = true;
const table = document.getElementById('services-list');
table.querySelectorAll('tr:not(#add-row)').forEach(e => e.remove());

View File

@@ -3,7 +3,8 @@ function showAdd(type) {
inputs.title.value = '';
inputs.text.value = '';
inputs.time.value = '';
inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value;
inputs.notification_service.querySelectorAll('input[type="checkbox"]').forEach(c => c.checked = false);
inputs.notification_service.querySelector('input[type="checkbox"]:first-child').checked = true;
toggleNormal();
toggleColor(true);
document.getElementById('test-reminder').classList.remove('show-sent');
@@ -71,7 +72,9 @@ function showEdit(id, type) {
);
inputs.time.value = trigger_date.toLocaleString('en-CA').slice(0,10) + 'T' + trigger_date.toTimeString().slice(0,5);
};
inputs.notification_service.value = json.result.notification_service;
inputs.notification_service.querySelectorAll('input[type="checkbox"]').forEach(
c => c.checked = json.result.notification_services.includes(parseInt(c.dataset.id))
);
if (type == types.reminder) {
if (json.result.repeat_interval === null) {

View File

@@ -26,7 +26,7 @@ function loadTemplateSelection() {
function applyTemplate() {
if (inputs.template.value === '0') {
inputs.title.value = '';
inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value;
inputs.notification_service.querySelectorAll('input[type="checkbox"]:checked').forEach(c => c.checked = false)
inputs.text.value = '';
toggleColor(true);
} else {
@@ -37,7 +37,9 @@ function applyTemplate() {
})
.then(json => {
inputs.title.value = json.result.title;
inputs.notification_service.value = json.result.notification_service;
inputs.notification_service.querySelectorAll('input[type="checkbox"]').forEach(
c => c.checked = json.result.notification_services.includes(parseInt(c.dataset.id))
);
inputs.text.value = json.result.text;
if (json.result.color !== null) {
if (inputs.color.classList.contains('hidden'))

View File

@@ -4,7 +4,7 @@ const inputs = {
'template': document.getElementById('template-selection'),
'title': document.getElementById('title-input'),
'time': document.getElementById('time-input'),
'notification_service': document.getElementById('notification-service-input'),
'notification_service': document.querySelector('.notification-service-list'),
'text': document.getElementById('text-input'),
'color': document.querySelector('.color-list')
};
@@ -44,6 +44,13 @@ function toggleColor(hide=false) {
inputs.color.classList.add('hidden');
};
function toggleNotificationService(hide=false) {
if (!hide)
inputs.notification_service.classList.toggle('hidden');
else
inputs.notification_service.classList.add('hidden');
};
function toggleNormal() {
type_buttons.normal_button.dataset.selected = 'true';
type_buttons.repeat_button.dataset.selected = 'false';
@@ -83,9 +90,22 @@ function testReminder() {
input.classList.remove('error-input');
input.removeAttribute('title');
};
const ns = [...
document.querySelectorAll('.notification-service-list input[type="checkbox"]:checked')
].map(c => parseInt(c.dataset.id))
if (!ns) {
input.classList.add('error-input');
input.title = 'No notification service set';
return
} else {
input.classList.remove('error-input');
input.removeAttribute('title');
};
const data = {
'title': inputs.title.value,
'notification_service': inputs.notification_service.value,
'notification_services': ns,
'text': inputs.text.value
};
headers.body = JSON.stringify(data);
@@ -140,19 +160,30 @@ function deleteInfo() {
function submitInfo() {
inputs.time.classList.remove('error-input');
inputs.time.removeAttribute('title');
inputs.notification_service.classList.remove('error-input');
inputs.notification_service.removeAttribute('title');
let fetch_data = {
url: null,
method: null
method: null,
call_back: null
};
const data = {
'title': inputs.title.value,
'notification_service': inputs.notification_service.value,
'notification_services': [...
document.querySelectorAll('.notification-service-list input[type="checkbox"]:checked')
].map(c => parseInt(c.dataset.id)),
'text': inputs.text.value,
'color': null
};
if (!inputs.color.classList.contains('hidden')) {
data['color'] = inputs.color.querySelector('button[data-selected="true"]').dataset.color;
};
if (data.notification_services.length === 0) {
inputs.notification_service.classList.add('error-input');
inputs.notification_service.title = 'No notification service set';
return
};
const e_id = document.getElementById('info').dataset.id;
const cl = document.getElementById('info').classList;
@@ -165,16 +196,22 @@ function submitInfo() {
};
fetch_data.url = `${url_prefix}/api/reminders?api_key=${api_key}`;
fetch_data.method = 'POST';
fetch_data.call_back = fillReminders;
} else if (cl.contains('show-add-template')) {
// Add template
fetch_data.url = `${url_prefix}/api/templates?api_key=${api_key}`;
fetch_data.method = 'POST';
fetch_data.call_back = () => {
loadTemplateSelection();
fillTemplates();
};
} else if (cl.contains('show-add-static-reminder')) {
// Add static reminder
fetch_data.url = `${url_prefix}/api/staticreminders?api_key=${api_key}`;
fetch_data.method = 'POST';
fetch_data.call_back = fillStaticReminders;
} else if (cl.contains('show-edit-reminder')) {
// Edit reminder
@@ -185,16 +222,22 @@ function submitInfo() {
};
fetch_data.url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`;
fetch_data.method = 'PUT';
fetch_data.call_back = fillReminders;
} else if (cl.contains('show-edit-template')) {
// Edit template
fetch_data.url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`;
fetch_data.method = 'PUT';
fetch_data.call_back = () => {
loadTemplateSelection();
fillTemplates();
};
} else if (cl.contains('show-edit-static-reminder')) {
// Edit a static reminder
fetch_data.url = `${url_prefix}/api/staticreminders/${e_id}?api_key=${api_key}`;
fetch_data.method = 'PUT';
fetch_data.call_back = fillStaticReminders;
} else return;
@@ -206,9 +249,7 @@ function submitInfo() {
.then(response => {
if (!response.ok) return Promise.reject(response.status);
fillReminders();
fillStaticReminders();
fillTemplates();
fetch_data.call_back()
hideWindow();
})
.catch(e => {
@@ -228,6 +269,7 @@ loadColor();
document.getElementById('template-selection').addEventListener('change', e => applyTemplate());
document.getElementById('color-toggle').addEventListener('click', e => toggleColor());
document.getElementById('toggle-notification-service-list').addEventListener('click', e => toggleNotificationService());
document.getElementById('normal-button').addEventListener('click', e => toggleNormal());
document.getElementById('repeat-button').addEventListener('click', e => toggleRepeated());
document.getElementById('close-info').addEventListener('click', e => hideWindow());

View File

@@ -163,8 +163,9 @@
<input type="text" id="title-input" placeholder="Title" required>
<div class="sub-inputs">
<input type="datetime-local" id="time-input" required>
<select id="notification-service-input" required></select>
<button type="button" id="toggle-notification-service-list">Notification Services</button>
</div>
<div class="notification-service-list hidden"></div>
<div class="sub-inputs">
<button type="button" id="normal-button" data-selected="true">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">

View File

@@ -1,5 +1,4 @@
import unittest
from threading import Thread
from backend.reminders import filter_function, ReminderHandler
@@ -9,13 +8,6 @@ class Test_Reminder_Handler(unittest.TestCase):
instance = ReminderHandler(context)
self.assertIs(context, instance.context)
self.assertIsInstance(instance.thread, Thread)
self.assertFalse(instance.stop)
with self.assertRaises(RuntimeError):
instance.stop_handling()
self.assertTrue(instance.stop)
def test_filter_function(self):
p = {
'title': 'TITLE',