mirror of
https://github.com/Casvt/MIND.git
synced 2026-02-19 11:54:46 -05:00
@@ -28,7 +28,13 @@ class NotificationServiceNotFound(Exception):
|
||||
|
||||
class NotificationServiceInUse(Exception):
|
||||
"""The notification service is wished to be deleted but a reminder is still using it"""
|
||||
api_response = {'error': 'NotificationServiceInUse', 'result': {}, 'code': 400}
|
||||
def __init__(self, type: str=''):
|
||||
self.type = type
|
||||
super().__init__(self.type)
|
||||
|
||||
@property
|
||||
def api_response(self) -> Dict[str, Any]:
|
||||
return {'error': 'NotificationServiceInUse', 'result': {'type': self.type}, 'code': 400}
|
||||
|
||||
class InvalidTime(Exception):
|
||||
"""The time given is in the past"""
|
||||
@@ -58,3 +64,7 @@ class InvalidKeyValue(Exception):
|
||||
@property
|
||||
def api_response(self) -> Dict[str, Any]:
|
||||
return {'error': 'InvalidKeyValue', 'result': {'key': self.key, 'value': self.value}, 'code': 400}
|
||||
|
||||
class TemplateNotFound(Exception):
|
||||
"""The template was not found"""
|
||||
api_response = {'error': 'TemplateNotFound', 'result': {}, 'code': 404}
|
||||
|
||||
@@ -105,6 +105,16 @@ def setup_db() -> None:
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (notification_service) REFERENCES notification_services(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,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (notification_service) REFERENCES notification_services(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS config(
|
||||
key VARCHAR(255) PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
|
||||
@@ -80,11 +80,19 @@ class NotificationService:
|
||||
# Check if no reminders exist with this service
|
||||
cursor = get_db()
|
||||
cursor.execute(
|
||||
"SELECT id FROM reminders WHERE notification_service = ? LIMIT 1",
|
||||
"SELECT 1 FROM reminders WHERE notification_service = ? LIMIT 1;",
|
||||
(self.id,)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise NotificationServiceInUse
|
||||
raise NotificationServiceInUse('reminder')
|
||||
|
||||
# Check if no templates exist with this service
|
||||
cursor.execute(
|
||||
"SELECT 1 FROM templates WHERE notification_service = ? LIMIT 1;",
|
||||
(self.id,)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise NotificationServiceInUse('template')
|
||||
|
||||
cursor.execute(
|
||||
"DELETE FROM notification_services WHERE id = ?",
|
||||
|
||||
151
backend/templates.py
Normal file
151
backend/templates.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from sqlite3 import IntegrityError
|
||||
from typing import List
|
||||
|
||||
from backend.custom_exceptions import (NotificationServiceNotFound,
|
||||
TemplateNotFound)
|
||||
from backend.db import get_db
|
||||
|
||||
class Template:
|
||||
"""Represents a template
|
||||
"""
|
||||
def __init__(self, template_id: int):
|
||||
self.id = template_id
|
||||
|
||||
exists = get_db().execute("SELECT 1 FROM templates WHERE id = ? LIMIT 1;", (self.id,)).fetchone()
|
||||
if not exists:
|
||||
raise TemplateNotFound
|
||||
|
||||
def get(self) -> dict:
|
||||
"""Get info about the template
|
||||
|
||||
Returns:
|
||||
dict: The info about the template
|
||||
"""
|
||||
template = get_db(dict).execute("""
|
||||
SELECT
|
||||
id,
|
||||
title, text,
|
||||
notification_service
|
||||
FROM templates
|
||||
WHERE id = ?;
|
||||
""",
|
||||
(self.id,)
|
||||
).fetchone()
|
||||
|
||||
return dict(template)
|
||||
|
||||
def update(self,
|
||||
title: str = None,
|
||||
notification_service: int = None,
|
||||
text: str = None
|
||||
) -> dict:
|
||||
"""Edit the 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.
|
||||
text (str, optional): The new body of the template. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: The new template info
|
||||
"""
|
||||
cursor = get_db()
|
||||
|
||||
data = self.get()
|
||||
new_values = {
|
||||
'title': title,
|
||||
'notification_service': notification_service,
|
||||
'text': text
|
||||
}
|
||||
for k, v in new_values.items():
|
||||
if v is not None:
|
||||
data[k] = v
|
||||
|
||||
try:
|
||||
cursor.execute("""
|
||||
UPDATE templates
|
||||
SET title=?, notification_service=?, text=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data['title'],
|
||||
data['notification_service'],
|
||||
data['text'],
|
||||
self.id
|
||||
))
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
|
||||
return self.get()
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the template
|
||||
"""
|
||||
get_db().execute("DELETE FROM templates WHERE id = ?;", (self.id,))
|
||||
return
|
||||
|
||||
class Templates:
|
||||
"""Represents the template library of the user account
|
||||
"""
|
||||
def __init__(self, user_id: int):
|
||||
self.user_id = user_id
|
||||
|
||||
def fetchall(self) -> List[dict]:
|
||||
"""Get all templates
|
||||
|
||||
Returns:
|
||||
List[dict]: The id, title, text and notification_service
|
||||
"""
|
||||
templates: list = list(map(dict, get_db(dict).execute("""
|
||||
SELECT
|
||||
id,
|
||||
title, text,
|
||||
notification_service
|
||||
FROM templates
|
||||
WHERE user_id = ?
|
||||
ORDER BY title, id;
|
||||
""",
|
||||
(self.user_id,)
|
||||
).fetchall()))
|
||||
|
||||
return templates
|
||||
|
||||
def fetchone(self, id: int) -> Template:
|
||||
"""Get one template
|
||||
|
||||
Args:
|
||||
id (int): The id of the template to fetch
|
||||
|
||||
Returns:
|
||||
Template: A Template instance
|
||||
"""
|
||||
return Template(id)
|
||||
|
||||
def add(
|
||||
self,
|
||||
title: str,
|
||||
notification_service: int,
|
||||
text: str = ''
|
||||
) -> Template:
|
||||
"""Add a template
|
||||
|
||||
Args:
|
||||
title (str): The title of the entry
|
||||
notification_service (int): The id of the notification service to use to send the reminder.
|
||||
text (str, optional): The body of the reminder. Defaults to ''.
|
||||
|
||||
Returns:
|
||||
Template: The info about the template
|
||||
"""
|
||||
try:
|
||||
id = get_db().execute("""
|
||||
INSERT INTO templates(user_id, title, text, notification_service)
|
||||
VALUES (?,?,?,?);
|
||||
""",
|
||||
(self.user_id, title, text, notification_service)
|
||||
).lastrowid
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
|
||||
return self.fetchone(id)
|
||||
@@ -6,6 +6,7 @@ from backend.db import get_db
|
||||
from backend.notification_service import NotificationServices
|
||||
from backend.reminders import Reminders
|
||||
from backend.security import generate_salt_hash, get_hash
|
||||
from backend.templates import Templates
|
||||
|
||||
ONEPASS_USERNAME_CHARACTERS = 'abcedfghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.!@$'
|
||||
ONEPASS_INVALID_USERNAMES = ['reminders','api']
|
||||
@@ -52,6 +53,17 @@ class User:
|
||||
self.notification_services_instance = NotificationServices(self.user_id)
|
||||
return self.notification_services_instance
|
||||
|
||||
@property
|
||||
def templates(self) -> Templates:
|
||||
"""Get access to the templates of the user account
|
||||
|
||||
Returns:
|
||||
Templates: Templates instance that can be used to access the templates of the user account
|
||||
"""
|
||||
if not hasattr(self, 'templates_instance'):
|
||||
self.templates_instance = Templates(self.user_id)
|
||||
return self.templates_instance
|
||||
|
||||
def edit_password(self, new_password: str) -> None:
|
||||
"""Change the password of the account
|
||||
|
||||
|
||||
108
frontend/api.py
108
frontend/api.py
@@ -15,6 +15,7 @@ from backend.custom_exceptions import (AccessUnauthorized, InvalidKeyValue,
|
||||
from backend.notification_service import (NotificationService,
|
||||
NotificationServices)
|
||||
from backend.reminders import Reminders, reminder_handler
|
||||
from backend.templates import Template, Templates
|
||||
from backend.users import User, register_user
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
@@ -451,7 +452,7 @@ def api_reminders_query():
|
||||
result = g.user_data.reminders.search(query)
|
||||
return return_api(result)
|
||||
|
||||
@api.route('/reminders/<r_id>', methods=['GET','PUT','DELETE'])
|
||||
@api.route('/reminders/<int:r_id>', methods=['GET','PUT','DELETE'])
|
||||
@error_handler
|
||||
@auth
|
||||
def api_get_reminder(r_id: int):
|
||||
@@ -515,3 +516,108 @@ def api_get_reminder(r_id: int):
|
||||
elif request.method == 'DELETE':
|
||||
reminders.fetchone(r_id).delete()
|
||||
return return_api({})
|
||||
|
||||
#===================
|
||||
# Template endpoints
|
||||
#===================
|
||||
|
||||
@api.route('/templates', methods=['GET', 'POST'])
|
||||
@error_handler
|
||||
@auth
|
||||
def api_get_templates():
|
||||
"""
|
||||
Endpoint: /templates
|
||||
Description: Manage the templates
|
||||
Requires being logged in: Yes
|
||||
Methods:
|
||||
GET:
|
||||
Description: Get a list of all templates
|
||||
Returns:
|
||||
200:
|
||||
The id, title, notification_service and text of every template
|
||||
POST:
|
||||
Description: Add a template
|
||||
Parameters (body (content-type: application/json)):
|
||||
title (required): the title of the template
|
||||
notification_service (required): the id of the notification service to use to send the notification
|
||||
text: the body of the template
|
||||
Returns:
|
||||
200:
|
||||
The info about the new template entry
|
||||
400:
|
||||
KeyNotFound: One of the required parameters was not given
|
||||
"""
|
||||
templates: Templates = g.user_data.templates
|
||||
|
||||
if request.method == 'GET':
|
||||
result = templates.fetchall()
|
||||
return return_api(result)
|
||||
|
||||
elif request.method == 'POST':
|
||||
data = request.get_json()
|
||||
title = extract_key(data, 'title')
|
||||
notification_service = extract_key(data, 'notification_service')
|
||||
text = extract_key(data, 'text', check_existence=False)
|
||||
|
||||
result = templates.add(title=title,
|
||||
notification_service=notification_service,
|
||||
text=text)
|
||||
return return_api(result.get(), code=201)
|
||||
|
||||
@api.route('/templates/<int:t_id>', methods=['GET', 'PUT', 'DELETE'])
|
||||
@error_handler
|
||||
@auth
|
||||
def api_get_template(t_id: int):
|
||||
"""
|
||||
Endpoint: /templates/<t_id>
|
||||
Description: Manage a specific template
|
||||
Requires being logged in: Yes
|
||||
URL Parameters:
|
||||
<t_id>:
|
||||
The id of the template
|
||||
Methods:
|
||||
GET:
|
||||
Returns:
|
||||
200:
|
||||
All info about the template
|
||||
404:
|
||||
No template found with the given id
|
||||
PUT:
|
||||
Description: Edit the template
|
||||
Parameters (body (content-type: application/json)):
|
||||
title: The new title of the entry.
|
||||
notification_service: The new id of the notification service to use to send the reminder.
|
||||
text: The new body of the template.
|
||||
Returns:
|
||||
200:
|
||||
Template updated successfully
|
||||
404:
|
||||
No template found with the given id
|
||||
DELETE:
|
||||
Description: Delete the template
|
||||
Returns:
|
||||
200:
|
||||
Template deleted successfully
|
||||
404:
|
||||
No template found with the given id
|
||||
"""
|
||||
template: Template = g.user_data.templates.fetchone(t_id)
|
||||
|
||||
if request.method == 'GET':
|
||||
result = template.get()
|
||||
return return_api(result)
|
||||
|
||||
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)
|
||||
text = extract_key(data, 'text', check_existence=False)
|
||||
|
||||
result = template.update(title=title,
|
||||
notification_service=notification_service,
|
||||
text=text)
|
||||
return return_api(result)
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
template.delete()
|
||||
return return_api({})
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
gap: .75rem;
|
||||
|
||||
border: 2px solid var(--color-gray);
|
||||
box-shadow: var(--default-shadow);
|
||||
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
@@ -62,6 +63,7 @@
|
||||
|
||||
border: 2px solid var(--color-gray);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--default-shadow);
|
||||
}
|
||||
|
||||
.repeat-bar > *,
|
||||
|
||||
@@ -264,6 +264,27 @@ nav > div > button svg {
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
.tab-selector {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.tab-selector > button {
|
||||
border: 2px solid var(--color-gray);
|
||||
padding: .5rem 1rem;
|
||||
transition: background-color .3s ease-in-out;
|
||||
}
|
||||
|
||||
.tab-selector > button[data-selected="true"] {
|
||||
background-color: var(--color-gray);
|
||||
}
|
||||
|
||||
@media (max-width: 543px) {
|
||||
.window-container {
|
||||
margin-left: 0;
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
}
|
||||
|
||||
/* REMINDER LIST */
|
||||
#reminder-list {
|
||||
#reminder-list,
|
||||
#template-list {
|
||||
--gap: 1rem;
|
||||
--entry-width: 13rem;
|
||||
max-width: 43rem;
|
||||
@@ -60,13 +61,15 @@
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#add-entry {
|
||||
#add-entry,
|
||||
#add-template {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
#add-entry svg {
|
||||
#add-entry svg,
|
||||
#add-template svg {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
@@ -95,7 +98,8 @@
|
||||
background-color: var(--color-gray);
|
||||
}
|
||||
|
||||
div.entry.fit {
|
||||
div.entry.fit,
|
||||
button.entry.fit {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -160,6 +164,11 @@ div.entry.fit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#delete-template {
|
||||
border-color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
@media (max-width: 543px) {
|
||||
header > div {
|
||||
transform: translateX(0);
|
||||
@@ -52,6 +52,7 @@ function addReminder() {
|
||||
|
||||
function showAdd() {
|
||||
if (!document.getElementById('add-entry').classList.contains('error')) {
|
||||
loadTemplates(force=false);
|
||||
showWindow('add');
|
||||
} else {
|
||||
showWindow('notification');
|
||||
@@ -61,6 +62,7 @@ function showAdd() {
|
||||
function closeAdd() {
|
||||
hideWindow();
|
||||
setTimeout(() => {
|
||||
document.getElementById('template-selection').value = document.querySelector('#template-selection option[selected]').value;
|
||||
inputs.title.value = '';
|
||||
inputs.time.value = '';
|
||||
inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value;
|
||||
@@ -89,6 +91,7 @@ function toggleRepeated() {
|
||||
// code run on load
|
||||
|
||||
document.getElementById('add-form').setAttribute('action', 'javascript:addReminder();');
|
||||
document.getElementById('template-selection').addEventListener('change', e => loadTemplate());
|
||||
document.getElementById('normal-button').addEventListener('click', e => toggleNormal());
|
||||
document.getElementById('repeat-button').addEventListener('click', e => toggleRepeated());
|
||||
document.getElementById('close-add').addEventListener('click', e => closeAdd());
|
||||
|
||||
@@ -30,6 +30,23 @@ function hideWindow() {
|
||||
});
|
||||
};
|
||||
|
||||
function showTab(tab_id, button_id, load_function=null) {
|
||||
document.querySelectorAll(`.tab-selector > button`).forEach(e => {
|
||||
if (e.id === button_id) {
|
||||
e.dataset.selected = "true"
|
||||
} else {
|
||||
e.dataset.selected = "false"
|
||||
};
|
||||
});
|
||||
document.querySelectorAll(`#home > div:not(.tab-selector)`).forEach(e => {
|
||||
e.classList.add('hidden');
|
||||
});
|
||||
document.getElementById(tab_id).classList.remove('hidden');
|
||||
if (load_function !== null) {
|
||||
load_function(force=false);
|
||||
};
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
const api_key = sessionStorage.getItem('api_key');
|
||||
@@ -44,6 +61,9 @@ document.getElementById('notification-services-button').addEventListener('click'
|
||||
document.getElementById('settings-button').addEventListener('click', e => showWindow('settings'));
|
||||
document.getElementById('logout-button').addEventListener('click', e => logout());
|
||||
|
||||
document.getElementById('reminders-selector').addEventListener('click', e => showTab('reminder-tab', 'reminders-selector'));
|
||||
document.getElementById('templates-selector').addEventListener('click', e => showTab('template-tab', 'templates-selector', loadTemplates));
|
||||
|
||||
const edit_icon = '<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 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22.94,1.06a3.626,3.626,0,0,0-5.124,0L0,18.876V24H5.124L22.94,6.184A3.627,3.627,0,0,0,22.94,1.06ZM4.3,22H2V19.7L15.31,6.4l2.3,2.3ZM21.526,4.77,19.019,7.277l-2.295-2.3L19.23,2.474a1.624,1.624,0,0,1,2.3,2.3Z"></path></g></g></svg>';
|
||||
const delete_icon = '<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 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22,4H17V2a2,2,0,0,0-2-2H9A2,2,0,0,0,7,2V4H2V6H4V21a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V6h2ZM9,2h6V4H9Zm9,19a1,1,0,0,1-1,1H7a1,1,0,0,1-1-1V6H18Z"></path><rect x="9" y="10" width="2" height="8"></rect><rect x="13" y="10" width="2" height="8"></rect></g></g></svg>';
|
||||
const save_icon = '<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 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><path d="M12,10a4,4,0,1,0,4,4A4,4,0,0,0,12,10Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,12,16Z"></path><path d="M22.536,4.122,19.878,1.464A4.966,4.966,0,0,0,16.343,0H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H19a5.006,5.006,0,0,0,5-5V7.657A4.966,4.966,0,0,0,22.536,4.122ZM17,2.08V3a3,3,0,0,1-3,3H10A3,3,0,0,1,7,3V2h9.343A2.953,2.953,0,0,1,17,2.08ZM22,19a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2V3a5.006,5.006,0,0,0,5,5h4a4.991,4.991,0,0,0,4.962-4.624l2.16,2.16A3.02,3.02,0,0,1,22,7.657Z"></path></g></svg>';
|
||||
|
||||
@@ -11,8 +11,12 @@ function fillNotificationSelection() {
|
||||
.then(json => {
|
||||
if (json.result.length) {
|
||||
document.getElementById('add-entry').classList.remove('error', 'error-icon');
|
||||
[document.getElementById('notification-service-input'),
|
||||
document.getElementById('notification-service-edit-input')].forEach(options => {
|
||||
[
|
||||
document.getElementById('notification-service-input'),
|
||||
document.getElementById('notification-service-edit-input'),
|
||||
document.getElementById('notification-service-template-input'),
|
||||
document.getElementById('notification-service-template-edit-input')
|
||||
].forEach(options => {
|
||||
options.innerHTML = '';
|
||||
json.result.forEach(service => {
|
||||
const entry = document.createElement('option');
|
||||
@@ -95,10 +99,11 @@ function deleteService(id) {
|
||||
fetch(`/api/notificationservices/${id}?api_key=${api_key}`, {
|
||||
'method': 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
if (json.error !== null) {
|
||||
return Promise.reject(json);
|
||||
};
|
||||
|
||||
row.remove();
|
||||
@@ -107,12 +112,12 @@ function deleteService(id) {
|
||||
};
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid') {
|
||||
window.location.href = '/';
|
||||
} else if (e === 400) {
|
||||
} else if (e.error === 'NotificationServiceInUse') {
|
||||
const delete_button = row.querySelector('button[title="Delete"]');
|
||||
delete_button.classList.add('error-icon');
|
||||
delete_button.title = 'The notification service is still in use by a reminder';
|
||||
delete_button.title = `The notification service is still in use by a ${e.result.type}`;
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
|
||||
@@ -134,3 +134,4 @@ setInterval(fillList, 60000);
|
||||
document.getElementById('search-form').setAttribute('action', 'javascript:search();');
|
||||
document.getElementById('clear-button').addEventListener('click', e => clearSearch());
|
||||
document.getElementById('add-entry').addEventListener('click', e => showAdd());
|
||||
document.getElementById('add-template').addEventListener('click', e => showWindow('add-template'));
|
||||
|
||||
217
frontend/static/js/templates.js
Normal file
217
frontend/static/js/templates.js
Normal file
@@ -0,0 +1,217 @@
|
||||
const template_inputs = {
|
||||
'title': document.getElementById('title-template-input'),
|
||||
'notification-service': document.getElementById('notification-service-template-input'),
|
||||
'text': document.getElementById('text-template-input')
|
||||
};
|
||||
|
||||
const edit_template_inputs = {
|
||||
'title': document.getElementById('title-template-edit-input'),
|
||||
'notification-service': document.getElementById('notification-service-template-edit-input'),
|
||||
'text': document.getElementById('text-template-edit-input')
|
||||
};
|
||||
|
||||
function loadTemplates(force=true) {
|
||||
const table = document.getElementById('template-list');
|
||||
if (!force && !!table.querySelector('button:not(#add-template)')) {
|
||||
return
|
||||
};
|
||||
|
||||
fetch(`/api/templates?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
const select_list = document.getElementById('template-selection');
|
||||
select_list.querySelectorAll('option:not([selected])').forEach(e => e.remove());
|
||||
json.result.forEach(template => {
|
||||
const entry = document.createElement('option');
|
||||
entry.value = template.id;
|
||||
entry.innerText = template.title;
|
||||
|
||||
select_list.appendChild(entry);
|
||||
});
|
||||
|
||||
table.querySelectorAll('button:not(#add-template)').forEach(e => e.remove());
|
||||
json.result.forEach(template => {
|
||||
const entry = document.createElement('button');
|
||||
entry.classList.add('entry');
|
||||
entry.addEventListener('click', e => showEditTemplate(template.id));
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.innerText = template.title;
|
||||
entry.appendChild(title);
|
||||
|
||||
table.appendChild(entry);
|
||||
|
||||
if (title.clientHeight < title.scrollHeight) {
|
||||
entry.classList.add('expand');
|
||||
};
|
||||
});
|
||||
table.querySelectorAll('button:not(#add-template)').forEach(template => template.classList.add('fit'));
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function loadTemplate() {
|
||||
const id = document.getElementById('template-selection').value;
|
||||
if (id === "0") {
|
||||
inputs.title.value = '';
|
||||
inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value;
|
||||
inputs.text.value = '';
|
||||
} else {
|
||||
fetch(`/api/templates/${id}?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
inputs.title.value = json.result.title;
|
||||
inputs.notification_service.value = json.result.notification_service;
|
||||
inputs.text.value = json.result.text;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function addTemplate() {
|
||||
const data = {
|
||||
'title': template_inputs.title.value,
|
||||
'notification_service': template_inputs["notification-service"].value,
|
||||
'text': template_inputs.text.value
|
||||
};
|
||||
fetch(`/api/templates?api_key=${api_key}`, {
|
||||
'method': 'POST',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
|
||||
loadTemplates();
|
||||
closeAddTemplate();
|
||||
return
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function closeAddTemplate() {
|
||||
hideWindow();
|
||||
setTimeout(() => {
|
||||
template_inputs.title.value = '';
|
||||
template_inputs['notification-service'].value = document.querySelector('#notification-service-template-input option[selected]').value;
|
||||
template_inputs.text.value = '';
|
||||
}, 500);
|
||||
};
|
||||
|
||||
function showEditTemplate(id) {
|
||||
fetch(`/api/templates/${id}?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
document.getElementById('template-edit-form').dataset.id = id;
|
||||
edit_template_inputs.title.value = json.result.title;
|
||||
edit_template_inputs['notification-service'].value = json.result.notification_service;
|
||||
edit_template_inputs.text.value = json.result.text;
|
||||
showWindow('edit-template');
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function saveTemplate() {
|
||||
const id = document.getElementById('template-edit-form').dataset.id;
|
||||
const data = {
|
||||
'title': edit_template_inputs.title.value,
|
||||
'notification_service': edit_template_inputs['notification-service'].value,
|
||||
'text': edit_template_inputs.text.value
|
||||
};
|
||||
fetch(`/api/templates/${id}?api_key=${api_key}`, {
|
||||
'method': 'PUT',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
loadTemplates();
|
||||
hideWindow();
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function deleteTemplate() {
|
||||
const id = document.getElementById('template-edit-form').dataset.id;
|
||||
fetch(`/api/templates/${id}?api_key=${api_key}`, {
|
||||
'method': 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
|
||||
loadTemplates();
|
||||
hideWindow();
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
document.getElementById('template-form').setAttribute('action', 'javascript:addTemplate();');
|
||||
document.getElementById('close-template').addEventListener('click', e => closeAddTemplate());
|
||||
document.getElementById('template-edit-form').setAttribute('action', 'javascript:saveTemplate()');
|
||||
document.getElementById('close-edit-template').addEventListener('click', e => hideWindow());
|
||||
document.getElementById('delete-template').addEventListener('click', e => deleteTemplate());
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="/static/css/general.css">
|
||||
<link rel="stylesheet" href="/static/css/reminders.css">
|
||||
<link rel="stylesheet" href="/static/css/reminders_templates.css">
|
||||
<link rel="stylesheet" href="/static/css/add_edit.css">
|
||||
<link rel="stylesheet" href="/static/css/notification.css">
|
||||
<link rel="stylesheet" href="/static/css/settings.css">
|
||||
@@ -16,6 +16,7 @@
|
||||
<script src="/static/js/edit.js" defer></script>
|
||||
<script src="/static/js/notification.js" defer></script>
|
||||
<script src="/static/js/settings.js" defer></script>
|
||||
<script src="/static/js/templates.js" defer></script>
|
||||
|
||||
<title>Reminders - Noted</title>
|
||||
</head>
|
||||
@@ -74,49 +75,71 @@
|
||||
</nav>
|
||||
<main class="window-container">
|
||||
<div id="home">
|
||||
<div class="search-container">
|
||||
<noscript>Javascript is disabled. The web-ui of Noted does not work with JavaScript disabled.</noscript>
|
||||
<form id="search-form">
|
||||
<div class="search-bar">
|
||||
<button type="submit">
|
||||
<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 513.749 513.749" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M504.352,459.061l-99.435-99.477c74.402-99.427,54.115-240.344-45.312-314.746S119.261-9.277,44.859,90.15 S-9.256,330.494,90.171,404.896c79.868,59.766,189.565,59.766,269.434,0l99.477,99.477c12.501,12.501,32.769,12.501,45.269,0 c12.501-12.501,12.501-32.769,0-45.269L504.352,459.061z M225.717,385.696c-88.366,0-160-71.634-160-160s71.634-160,160-160 s160,71.634,160,160C385.623,314.022,314.044,385.602,225.717,385.696z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="text" id="search-input" required placeholder="Search..." aria-placeholder="Search for reminders">
|
||||
<button type="button" id="clear-button">
|
||||
<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 512.021 512.021" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M301.258,256.01L502.645,54.645c12.501-12.501,12.501-32.769,0-45.269c-12.501-12.501-32.769-12.501-45.269,0l0,0 L256.01,210.762L54.645,9.376c-12.501-12.501-32.769-12.501-45.269,0s-12.501,32.769,0,45.269L210.762,256.01L9.376,457.376 c-12.501,12.501-12.501,32.769,0,45.269s32.769,12.501,45.269,0L256.01,301.258l201.365,201.387 c12.501,12.501,32.769,12.501,45.269,0c12.501-12.501,12.501-32.769,0-45.269L301.258,256.01z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="tab-selector">
|
||||
<button id="reminders-selector" data-selected="true">Reminders</button>
|
||||
<button id="templates-selector" data-selected="false">Templates</button>
|
||||
</div>
|
||||
<div id="reminder-list">
|
||||
<button class="entry" id="add-entry" aria-label="Add reminder" title="Add reminder">
|
||||
<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 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<div id="reminder-tab">
|
||||
<div class="search-container">
|
||||
<noscript>Javascript is disabled. The web-ui of Noted does not work with JavaScript disabled.</noscript>
|
||||
<form id="search-form">
|
||||
<div class="search-bar">
|
||||
<button type="submit">
|
||||
<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 513.749 513.749" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M504.352,459.061l-99.435-99.477c74.402-99.427,54.115-240.344-45.312-314.746S119.261-9.277,44.859,90.15 S-9.256,330.494,90.171,404.896c79.868,59.766,189.565,59.766,269.434,0l99.477,99.477c12.501,12.501,32.769,12.501,45.269,0 c12.501-12.501,12.501-32.769,0-45.269L504.352,459.061z M225.717,385.696c-88.366,0-160-71.634-160-160s71.634-160,160-160 s160,71.634,160,160C385.623,314.022,314.044,385.602,225.717,385.696z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="text" id="search-input" required placeholder="Search..." aria-placeholder="Search for reminders">
|
||||
<button type="button" id="clear-button">
|
||||
<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 512.021 512.021" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M301.258,256.01L502.645,54.645c12.501-12.501,12.501-32.769,0-45.269c-12.501-12.501-32.769-12.501-45.269,0l0,0 L256.01,210.762L54.645,9.376c-12.501-12.501-32.769-12.501-45.269,0s-12.501,32.769,0,45.269L210.762,256.01L9.376,457.376 c-12.501,12.501-12.501,32.769,0,45.269s32.769,12.501,45.269,0L256.01,301.258l201.365,201.387 c12.501,12.501,32.769,12.501,45.269,0c12.501-12.501,12.501-32.769,0-45.269L301.258,256.01z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="reminder-list">
|
||||
<button class="entry" id="add-entry" aria-label="Add reminder" title="Add reminder">
|
||||
<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 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
<g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<p>Add a notification service first!</p>
|
||||
</button>
|
||||
</svg>
|
||||
<p>Add a notification service first!</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="template-tab" class="hidden">
|
||||
<div id="template-list">
|
||||
<button class="entry" id="add-template" aria-label="Add template" title="Add template">
|
||||
<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 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add">
|
||||
<h2>Add a reminder</h2>
|
||||
<div class="form-container">
|
||||
<form id="add-form">
|
||||
<select id="template-selection">
|
||||
<option value="0" selected>No template</option>
|
||||
</select>
|
||||
<input type="text" id="title-input" placeholder="Title" required>
|
||||
<div class="sub-inputs">
|
||||
<input type="datetime-local" id="time-input" required>
|
||||
@@ -270,6 +293,35 @@
|
||||
<button id="delete-account-button">Delete Account</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-template">
|
||||
<h2>Add a template</h2>
|
||||
<div class="form-container">
|
||||
<form id="template-form">
|
||||
<input type="text" id="title-template-input" placeholder="Title" required>
|
||||
<select id="notification-service-template-input" required></select>
|
||||
<textarea id="text-template-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
|
||||
<div class="options">
|
||||
<button type="button" id="close-template">Cancel</button>
|
||||
<button type="submit">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="edit-template">
|
||||
<h2>Edit a template</h2>
|
||||
<div class="form-container">
|
||||
<form id="template-edit-form">
|
||||
<input type="text" id="title-template-edit-input" placeholder="Title" required>
|
||||
<select id="notification-service-template-edit-input" required></select>
|
||||
<textarea id="text-template-edit-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
|
||||
<div class="options">
|
||||
<button type="button" id="close-edit-template">Cancel</button>
|
||||
<button type="button" id="delete-template">Delete</button>
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -40,9 +40,9 @@ class Test_API(unittest.TestCase):
|
||||
self.assertEqual(result(AccessUnauthorized), return_api(**AccessUnauthorized.api_response))
|
||||
self.assertEqual(result(ReminderNotFound), return_api(**ReminderNotFound.api_response))
|
||||
self.assertEqual(result(NotificationServiceNotFound), return_api(**NotificationServiceNotFound.api_response))
|
||||
self.assertEqual(result(NotificationServiceInUse), return_api(**NotificationServiceInUse.api_response))
|
||||
self.assertEqual(result(InvalidTime), return_api(**InvalidTime.api_response))
|
||||
self.assertEqual(result(InvalidURL), return_api(**InvalidURL.api_response))
|
||||
self.assertEqual(result(NotificationServiceInUse, 'test'), return_api(**NotificationServiceInUse('test').api_response))
|
||||
self.assertEqual(result(KeyNotFound, 'test'), return_api(**KeyNotFound('test').api_response))
|
||||
self.assertEqual(result(InvalidKeyValue, 'test', 'value'), return_api(**InvalidKeyValue('test', 'value').api_response))
|
||||
with self.assertRaises(TypeError):
|
||||
|
||||
Reference in New Issue
Block a user