mirror of
https://github.com/Casvt/MIND.git
synced 2026-02-19 11:54:46 -05:00
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: casvt
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug, enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Description of the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Clear and concise steps to reproduce the behaviour. -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behaviour**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Version info**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser: [e.g. chrome, safari]
|
||||
- Noted Version: [e.g. V1.0.0]
|
||||
- Python Version: [e.g. V3.7]
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
12
.github/ISSUE_TEMPLATE/contribute-request.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/contribute-request.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Contribute request
|
||||
about: Announce that you want to contribute to the project
|
||||
title: ''
|
||||
labels: contributing
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What do you want to contribute? (e.g. add a feature, fix a bug, improve documentation)**
|
||||
|
||||
**How long do you think it will take to finish it? (e.g. one week, one month, three months)**
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -133,4 +133,6 @@ dmypy.json
|
||||
|
||||
# VS code
|
||||
*.code-workspace
|
||||
.vscode/
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"python.testing.unittestArgs": [
|
||||
"-v",
|
||||
"-s",
|
||||
"./tests",
|
||||
"-p",
|
||||
"*_test.py"
|
||||
],
|
||||
"python.testing.pytestEnabled": false,
|
||||
"python.testing.unittestEnabled": true
|
||||
}
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
casvantijn@gmail.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
34
CONTRIBUTING.md
Normal file
34
CONTRIBUTING.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Contributing to Noted
|
||||
## General steps
|
||||
Contributing to Noted consists of 5 steps, listed hereunder.
|
||||
|
||||
1. Make a [contributing request](https://github.com/Casvt/Noted/issues/new?template=contribute-request.md), where you describe what you plan on doing. This request needs to get approved before you can start, or your pull request won't be accepted. This is to avoid multiple people from doing the same thing and to avoid you wasting your time if we do not wish the changes. This is also where discussions can be held about how something will be implemented.
|
||||
2. When the request is accepted, start your local development (more info about this below).
|
||||
3. When done, create a pull request to the Development branch, where you mention again what you've changed/added and give a link to the original contributing request issue.
|
||||
4. The PR will be reviewed and if requested, changes will need to be made before it is accepted.
|
||||
5. When everything is okay, the PR will be accepted and you'll be done!
|
||||
|
||||
## Local development steps
|
||||
Once your request is accepted, you can start your local development.
|
||||
|
||||
1. Clone the repository onto your computer and open it using your prefered IDE (Visual Studio Code is used by us).
|
||||
2. Make the changes needed and write accompanying tests.
|
||||
3. Check if the code written follows the styling guide below.
|
||||
4. Run the finished version, using python 3.7, to check if you've made any errors.
|
||||
5. Run the tests (unittest is used). This can be done with a button click within VS Code, or with the following command where you need to be inside the root folder of the project:
|
||||
```bash
|
||||
python3 -m unittest discover -v -s './tests' -p '*_test.py'
|
||||
```
|
||||
6. Test your version thoroughly to catch as many bugs as possible (if any).
|
||||
|
||||
## Styling guide
|
||||
The code of Noted is written in such way that it follows the following rules. Your code should too.
|
||||
|
||||
1. Compatible with python 3.7 .
|
||||
2. Tabs (4 space size) are used for indentation.
|
||||
3. Use type hints as much as possible, though don't if it requires importing extra functions or classes (except for the `typing` library).
|
||||
4. Each function in the backend needs a doc string describing the function, what the inputs are, what errors could be raised from within the function and what the output is.
|
||||
5. Each function in the `api.py` file should have a doc string describing the api endpoint. For the formatting, look at the doc strings of the other functions in the file.
|
||||
6. The imports need to be sorted (the extension `isort` is used in VS Code).
|
||||
7. The code needs to be compatible with Linux, MacOS and Windows.
|
||||
8. The code should, though not strictly enforced, reasonably comply with the rule of 80 characters per line.
|
||||
10
Noted.py
10
Noted.py
@@ -2,32 +2,33 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from os import urandom
|
||||
from os.path import dirname, join
|
||||
from os.path import dirname, join, abspath
|
||||
from sys import version_info
|
||||
|
||||
from flask import Flask, render_template, request
|
||||
from waitress.server import create_server
|
||||
|
||||
from backend.db import close_db, setup_db
|
||||
from backend.db import close_db, setup_db, DBConnection
|
||||
from frontend.api import api, reminder_handler
|
||||
from frontend.ui import ui
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = '8080'
|
||||
THREADS = 10
|
||||
DB_FILENAME = 'Noted.db'
|
||||
|
||||
def _folder_path(*folders) -> str:
|
||||
"""Turn filepaths relative to the project folder into absolute paths
|
||||
Returns:
|
||||
str: The absolute filepath
|
||||
"""
|
||||
return join(dirname(__file__), *folders)
|
||||
return join(dirname(abspath(__file__)), *folders)
|
||||
|
||||
def _create_app() -> Flask:
|
||||
"""Create a Flask app instance
|
||||
Returns:
|
||||
Flask: The created app instance
|
||||
"""
|
||||
"""
|
||||
app = Flask(
|
||||
__name__,
|
||||
template_folder=_folder_path('frontend','templates'),
|
||||
@@ -78,6 +79,7 @@ def Noted() -> None:
|
||||
# Register web server
|
||||
app = _create_app()
|
||||
with app.app_context():
|
||||
DBConnection.file = _folder_path(DB_FILENAME)
|
||||
setup_db()
|
||||
reminder_handler._find_next_reminder()
|
||||
|
||||
|
||||
32
README.md
32
README.md
@@ -1,4 +1,32 @@
|
||||
# Noted
|
||||
# Noted Reminders
|
||||
A simple self hosted reminder platform that uses push to send notifications to your device. Set the reminder and forget about it! 📢
|
||||
|
||||
See the [wiki](https://github.com/Casvt/Noted/wiki) for instructions on how to install and setup Noted.
|
||||
Noted is a simple self hosted application for creating reminders that get pushed to your device using the [Apprise](https://github.com/caronc/apprise) API. You can send messages to just about every platform, including scheduled emails!
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Core Features
|
||||
* Basic auth
|
||||
* Utilizes Apprise
|
||||
* Create and delete reminders
|
||||
* Schedule reminders
|
||||
|
||||
## Planned Features
|
||||
You can see our planned features in our [Project board](https://github.com/users/Casvt/projects/3).
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
sudo apt-get install git python3-pip
|
||||
sudo git clone https://github.com/Casvt/Noted.git /opt/Noted
|
||||
cd /opt/Noted
|
||||
python3 -m pip install -r requirements.txt
|
||||
python3 Noted.py
|
||||
```
|
||||
You can see the [wiki](https://github.com/Casvt/Noted/wiki) for instructions on how to install using other OS.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
class UsernameTaken(Exception):
|
||||
"""The username is already taken"""
|
||||
api_response = {'error': 'UsernameTaken', 'result': {}, 'code': 400}
|
||||
@@ -26,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"""
|
||||
@@ -43,7 +51,7 @@ class KeyNotFound(Exception):
|
||||
super().__init__(self.key)
|
||||
|
||||
@property
|
||||
def api_response(self):
|
||||
def api_response(self) -> Dict[str, Any]:
|
||||
return {'error': 'KeyNotFound', 'result': {'key': self.key}, 'code': 400}
|
||||
|
||||
class InvalidKeyValue(Exception):
|
||||
@@ -54,5 +62,9 @@ class InvalidKeyValue(Exception):
|
||||
super().__init__(self.key)
|
||||
|
||||
@property
|
||||
def api_response(self):
|
||||
return {'error': 'KeyNotFound', 'result': {'key': self.key, 'value': self.value}, 'code': 400}
|
||||
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}
|
||||
|
||||
@@ -6,21 +6,24 @@ from typing import Union
|
||||
|
||||
from flask import g
|
||||
|
||||
NOTED_DB_FILE = 'Noted.db'
|
||||
__DATABASE_VERSION__ = 1
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
def __call__(cls, *args, **kwargs):
|
||||
i = f'{cls}{current_thread()}'
|
||||
if i not in cls._instances:
|
||||
cls._instances[i] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
_instances = {}
|
||||
def __call__(cls, *args, **kwargs):
|
||||
i = f'{cls}{current_thread()}'
|
||||
if i not in cls._instances:
|
||||
cls._instances[i] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
|
||||
return cls._instances[i]
|
||||
return cls._instances[i]
|
||||
|
||||
class DBConnection(Connection, metaclass=Singleton):
|
||||
def __init__(self, file: str, timeout: float):
|
||||
super().__init__(file, timeout=timeout)
|
||||
super().cursor().execute("PRAGMA foreign_keys = ON;")
|
||||
file = ''
|
||||
|
||||
def __init__(self, timeout: float) -> None:
|
||||
super().__init__(self.file, timeout=timeout)
|
||||
super().cursor().execute("PRAGMA foreign_keys = ON;")
|
||||
return
|
||||
|
||||
def get_db(output_type: Union[dict, tuple]=tuple):
|
||||
"""Get a database cursor instance. Coupled to Flask's g.
|
||||
@@ -34,7 +37,7 @@ def get_db(output_type: Union[dict, tuple]=tuple):
|
||||
try:
|
||||
cursor = g.cursor
|
||||
except AttributeError:
|
||||
db = DBConnection(NOTED_DB_FILE, timeout=20.0)
|
||||
db = DBConnection(timeout=20.0)
|
||||
cursor = g.cursor = db.cursor()
|
||||
|
||||
if output_type is dict:
|
||||
@@ -57,6 +60,16 @@ def close_db(e=None) -> None:
|
||||
pass
|
||||
return
|
||||
|
||||
def migrate_db(current_db_version: int) -> None:
|
||||
"""
|
||||
Migrate a Noted database from it's current version
|
||||
to the newest version supported by the Noted version installed.
|
||||
"""
|
||||
print('Migrating database to newer version...')
|
||||
cursor = get_db()
|
||||
|
||||
return
|
||||
|
||||
def setup_db() -> None:
|
||||
"""Setup the database
|
||||
"""
|
||||
@@ -84,10 +97,43 @@ def setup_db() -> None:
|
||||
text TEXT,
|
||||
time INTEGER NOT NULL,
|
||||
notification_service INTEGER NOT NULL,
|
||||
|
||||
repeat_quantity VARCHAR(15),
|
||||
repeat_interval INTEGER,
|
||||
original_time INTEGER,
|
||||
|
||||
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
|
||||
);
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO config(key, value)
|
||||
VALUES ('database_version', ?);
|
||||
""",
|
||||
(__DATABASE_VERSION__,)
|
||||
)
|
||||
current_db_version = int(cursor.execute("SELECT value FROM config WHERE key = 'database_version' LIMIT 1;").fetchone()[0])
|
||||
|
||||
if current_db_version < __DATABASE_VERSION__:
|
||||
migrate_db(current_db_version)
|
||||
cursor.execute(
|
||||
"UPDATE config SET value = ? WHERE key = 'database_version' LIMIT 1;",
|
||||
(__DATABASE_VERSION__,)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -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 = ?",
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from sqlite3 import IntegrityError
|
||||
from threading import Thread
|
||||
from time import sleep, time as epoch_time
|
||||
from time import sleep
|
||||
from time import time as epoch_time
|
||||
from typing import List, Literal
|
||||
|
||||
from apprise import Apprise
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from flask import Flask
|
||||
|
||||
from backend.custom_exceptions import (InvalidTime,
|
||||
from backend.custom_exceptions import (InvalidKeyValue, InvalidTime,
|
||||
NotificationServiceNotFound,
|
||||
ReminderNotFound)
|
||||
from backend.db import close_db, get_db
|
||||
|
||||
filter_function = lambda query, p: (
|
||||
query in p["title"].lower()
|
||||
or query in p["text"].lower()
|
||||
or query in p["notification_service_title"].lower()
|
||||
)
|
||||
|
||||
def _find_next_time(
|
||||
original_time: int,
|
||||
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"],
|
||||
repeat_interval: int
|
||||
) -> int:
|
||||
td = relativedelta(**{repeat_quantity: repeat_interval})
|
||||
new_time = datetime.fromtimestamp(original_time)
|
||||
current_time = datetime.fromtimestamp(epoch_time())
|
||||
while new_time <= current_time:
|
||||
new_time += td
|
||||
return int(new_time.timestamp())
|
||||
|
||||
class ReminderHandler():
|
||||
"""Run in a thread to handle the set reminders
|
||||
"""
|
||||
@@ -26,7 +47,6 @@ class ReminderHandler():
|
||||
def _find_next_reminder(self) -> None:
|
||||
"""Note when next reminder is (could be in the past) or otherwise None
|
||||
"""
|
||||
|
||||
with self.context():
|
||||
next_timestamp = get_db().execute(
|
||||
"SELECT time FROM reminders ORDER BY time LIMIT 1;"
|
||||
@@ -58,26 +78,45 @@ class ReminderHandler():
|
||||
while not self.stop:
|
||||
if self.next_reminder and self.next_reminder <= epoch_time():
|
||||
with self.context():
|
||||
cursor = get_db()
|
||||
cursor = get_db(dict)
|
||||
# Get all reminders for the timestamp
|
||||
reminders = cursor.execute(
|
||||
"SELECT notification_service, title, text FROM reminders WHERE time = ?",
|
||||
reminders = cursor.execute("""
|
||||
SELECT
|
||||
id,
|
||||
notification_service, title, text,
|
||||
repeat_quantity, repeat_interval, original_time
|
||||
FROM reminders
|
||||
WHERE time = ?;
|
||||
""",
|
||||
(self.next_reminder,)
|
||||
).fetchall()
|
||||
|
||||
# Send of each reminder
|
||||
for reminder in reminders:
|
||||
# Send of reminder
|
||||
a = Apprise()
|
||||
url = cursor.execute(
|
||||
"SELECT url FROM notification_services WHERE id = ?",
|
||||
(reminder[0],)
|
||||
).fetchone()[0]
|
||||
(reminder["notification_service"],)
|
||||
).fetchone()["url"]
|
||||
a.add(url)
|
||||
a.notify(title=reminder[1], body=reminder[2])
|
||||
a.notify(title=reminder["title"], body=reminder["text"])
|
||||
|
||||
if reminder['repeat_quantity'] is None:
|
||||
# Delete the reminders from the database
|
||||
cursor.execute("DELETE FROM reminders WHERE id = ?", (reminder['id'],))
|
||||
else:
|
||||
# Set next time
|
||||
new_time = _find_next_time(
|
||||
reminder['original_time'],
|
||||
reminder['repeat_quantity'],
|
||||
reminder['repeat_interval']
|
||||
)
|
||||
self.submit_next_reminder(new_time)
|
||||
cursor.execute(
|
||||
"UPDATE reminders SET time = ? WHERE id = ?;",
|
||||
(new_time, reminder['id'])
|
||||
)
|
||||
|
||||
# Delete the reminders from the database
|
||||
cursor.execute("DELETE FROM reminders WHERE time = ?", (self.next_reminder,))
|
||||
|
||||
# Note when next reminder is (could be in the past) or otherwise None
|
||||
self._find_next_reminder()
|
||||
|
||||
@@ -115,7 +154,9 @@ class Reminder:
|
||||
r.title, r.text,
|
||||
r.time,
|
||||
r.notification_service,
|
||||
ns.title AS notification_service_title
|
||||
ns.title AS notification_service_title,
|
||||
r.repeat_quantity,
|
||||
r.repeat_interval
|
||||
FROM
|
||||
reminders r
|
||||
INNER JOIN notification_services ns
|
||||
@@ -133,7 +174,9 @@ class Reminder:
|
||||
title: str = None,
|
||||
time: int = None,
|
||||
notification_service: int = None,
|
||||
text: str = None
|
||||
text: str = None,
|
||||
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None,
|
||||
repeat_interval: int = None
|
||||
) -> dict:
|
||||
"""Edit the reminder
|
||||
|
||||
@@ -144,11 +187,20 @@ class Reminder:
|
||||
text (str, optional): The new body of the reminder. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: The new password info
|
||||
dict: The new reminder info
|
||||
"""
|
||||
cursor = get_db()
|
||||
|
||||
# Validate data
|
||||
if time < epoch_time():
|
||||
raise InvalidTime
|
||||
if repeat_quantity is None and repeat_interval is not None:
|
||||
raise InvalidKeyValue('repeat_quantity', repeat_quantity)
|
||||
elif repeat_quantity is not None and repeat_interval is None:
|
||||
raise InvalidKeyValue('repeat_interval', repeat_interval)
|
||||
repeated_reminder = repeat_quantity is not None and repeat_interval is not None
|
||||
|
||||
if not repeated_reminder:
|
||||
if time < epoch_time():
|
||||
raise InvalidTime
|
||||
time = round(time)
|
||||
|
||||
# Get current data and update it with new values
|
||||
@@ -157,28 +209,50 @@ class Reminder:
|
||||
'title': title,
|
||||
'time': time,
|
||||
'notification_service': notification_service,
|
||||
'text': text
|
||||
'text': text,
|
||||
'repeat_quantity': repeat_quantity,
|
||||
'repeat_interval': repeat_interval
|
||||
}
|
||||
for k, v in new_values.items():
|
||||
if v is not None:
|
||||
if k in ('repeat_quantity', 'repeat_interval') or v is not None:
|
||||
data[k] = v
|
||||
|
||||
# Update database
|
||||
try:
|
||||
get_db().execute("""
|
||||
UPDATE reminders
|
||||
SET title=?, text=?, time=?, notification_service=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
data["time"],
|
||||
data["notification_service"],
|
||||
self.id
|
||||
))
|
||||
if not repeated_reminder:
|
||||
next_time = data["time"]
|
||||
cursor.execute("""
|
||||
UPDATE reminders
|
||||
SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
data["time"],
|
||||
data["notification_service"],
|
||||
data["repeat_quantity"],
|
||||
data["repeat_interval"],
|
||||
self.id
|
||||
))
|
||||
else:
|
||||
next_time = _find_next_time(data["time"], data["repeat_quantity"], data["repeat_interval"])
|
||||
cursor.execute("""
|
||||
UPDATE reminders
|
||||
SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?, original_time=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
next_time,
|
||||
data["notification_service"],
|
||||
data["repeat_quantity"],
|
||||
data["repeat_interval"],
|
||||
data["time"],
|
||||
self.id
|
||||
))
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
reminder_handler.submit_next_reminder(time)
|
||||
reminder_handler.submit_next_reminder(next_time)
|
||||
|
||||
return self.get()
|
||||
|
||||
@@ -190,7 +264,7 @@ class Reminder:
|
||||
return
|
||||
|
||||
class Reminders:
|
||||
"""Represents the reminder vault of the user account
|
||||
"""Represents the reminder library of the user account
|
||||
"""
|
||||
sort_functions = {
|
||||
'title': (lambda r: (r['title'], r['time']), False),
|
||||
@@ -223,7 +297,9 @@ class Reminders:
|
||||
r.title, r.text,
|
||||
r.time,
|
||||
r.notification_service,
|
||||
ns.title AS notification_service_title
|
||||
ns.title AS notification_service_title,
|
||||
r.repeat_quantity,
|
||||
r.repeat_interval
|
||||
FROM
|
||||
reminders r
|
||||
INNER JOIN notification_services ns
|
||||
@@ -249,16 +325,11 @@ class Reminders:
|
||||
List[dict]: All reminders that match. Similar output to self.fetchall
|
||||
"""
|
||||
query = query.lower()
|
||||
passwords = self.fetchall()
|
||||
passwords = list(filter(
|
||||
lambda p: (
|
||||
query in p["title"].lower()
|
||||
or query in p["text"].lower()
|
||||
or query in p["notification_service_title"].lower()
|
||||
),
|
||||
passwords
|
||||
reminders = list(filter(
|
||||
lambda p: filter_function(query, p),
|
||||
self.fetchall()
|
||||
))
|
||||
return passwords
|
||||
return reminders
|
||||
|
||||
def fetchone(self, id: int) -> Reminder:
|
||||
"""Get one reminder
|
||||
@@ -276,7 +347,9 @@ class Reminders:
|
||||
title: str,
|
||||
time: int,
|
||||
notification_service: int,
|
||||
text: str = ''
|
||||
text: str = '',
|
||||
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None,
|
||||
repeat_interval: int = None
|
||||
) -> Reminder:
|
||||
"""Add a reminder
|
||||
|
||||
@@ -285,22 +358,34 @@ class Reminders:
|
||||
time (int): The epoch timestamp the the reminder should be send.
|
||||
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 ''.
|
||||
repeat_quantity (Literal["year", "month", "week", "day", "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.
|
||||
|
||||
Returns:
|
||||
dict: The info about the reminder
|
||||
"""
|
||||
# Validate data
|
||||
if time < epoch_time():
|
||||
raise InvalidTime
|
||||
time = round(time)
|
||||
|
||||
# Insert into db
|
||||
|
||||
if repeat_quantity is None and repeat_interval is not None:
|
||||
raise InvalidKeyValue('repeat_quantity', repeat_quantity)
|
||||
elif repeat_quantity is not None and repeat_interval is None:
|
||||
raise InvalidKeyValue('repeat_interval', repeat_interval)
|
||||
|
||||
try:
|
||||
id = get_db().execute("""
|
||||
INSERT INTO reminders(user_id, title, text, time, notification_service)
|
||||
VALUES (?,?,?,?,?);
|
||||
""", (self.user_id, title, text, time, notification_service,)
|
||||
).lastrowid
|
||||
if repeat_quantity is None and repeat_interval is None:
|
||||
id = get_db().execute("""
|
||||
INSERT INTO reminders(user_id, title, text, time, notification_service)
|
||||
VALUES (?,?,?,?,?);
|
||||
""", (self.user_id, title, text, time, notification_service)
|
||||
).lastrowid
|
||||
else:
|
||||
id = get_db().execute("""
|
||||
INSERT INTO reminders(user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, original_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
""", (self.user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, time)
|
||||
).lastrowid
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
reminder_handler.submit_next_reminder(time)
|
||||
|
||||
@@ -9,7 +9,7 @@ def get_hash(salt: bytes, data: str) -> bytes:
|
||||
"""Hash a string using the supplied salt
|
||||
|
||||
Args:
|
||||
salt (bytes): The salt to use wwhen hashing
|
||||
salt (bytes): The salt to use when hashing
|
||||
data (str): The data to hash
|
||||
|
||||
Returns:
|
||||
|
||||
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,9 +6,10 @@ 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 = ['users','api']
|
||||
ONEPASS_INVALID_USERNAMES = ['reminders','api']
|
||||
|
||||
class User:
|
||||
"""Represents an user account
|
||||
@@ -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
|
||||
|
||||
@@ -106,7 +118,7 @@ def register_user(username: str, password: str) -> int:
|
||||
Returns:
|
||||
user_id (int): The id of the new user. User registered successful
|
||||
"""
|
||||
#check if username is valid
|
||||
# Check if username is valid
|
||||
_check_username(username)
|
||||
|
||||
cursor = get_db()
|
||||
|
||||
160
frontend/api.py
160
frontend/api.py
@@ -6,13 +6,16 @@ from typing import Any, Tuple
|
||||
|
||||
from flask import Blueprint, g, request
|
||||
|
||||
from backend.custom_exceptions import (AccessUnauthorized, InvalidKeyValue, InvalidTime, InvalidURL,
|
||||
KeyNotFound, NotificationServiceInUse, NotificationServiceNotFound, ReminderNotFound,
|
||||
UsernameInvalid, UsernameTaken,
|
||||
UserNotFound)
|
||||
from backend.custom_exceptions import (AccessUnauthorized, InvalidKeyValue,
|
||||
InvalidTime, InvalidURL, KeyNotFound,
|
||||
NotificationServiceInUse,
|
||||
NotificationServiceNotFound,
|
||||
ReminderNotFound, UsernameInvalid,
|
||||
UsernameTaken, UserNotFound)
|
||||
from backend.notification_service import (NotificationService,
|
||||
NotificationServices)
|
||||
from backend.reminders import reminder_handler, Reminders
|
||||
from backend.reminders import Reminders, reminder_handler
|
||||
from backend.templates import Template, Templates
|
||||
from backend.users import User, register_user
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
@@ -80,10 +83,27 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
elif key == 'repeat_interval':
|
||||
try:
|
||||
value = int(value)
|
||||
if value <= 0:
|
||||
raise ValueError
|
||||
except (ValueError, TypeError):
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
elif key == 'sort_by':
|
||||
if not value in Reminders.sort_functions:
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
elif key == 'repeat_quantity':
|
||||
if not value in ("year", "month", "week", "day", "hours", "minutes"):
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
elif key in ('username', 'password', 'new_password', 'title', 'url',
|
||||
'text', 'query'):
|
||||
if not isinstance(value, str):
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
else:
|
||||
if key == 'sort_by':
|
||||
@@ -358,7 +378,7 @@ def api_notification_service(n_id: int):
|
||||
return return_api({})
|
||||
|
||||
#===================
|
||||
# Vault endpoints
|
||||
# Library endpoints
|
||||
#===================
|
||||
|
||||
@api.route('/reminders', methods=['GET','POST'])
|
||||
@@ -376,7 +396,7 @@ 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, url and username of every reminder
|
||||
The id, title, text, time, notification_service, notification_service_title, repeat_quantity and repeat_interval of each reminder
|
||||
POST:
|
||||
Description: Add a reminder
|
||||
Parameters (body (content-type: application/json)):
|
||||
@@ -384,9 +404,11 @@ def api_reminders_list():
|
||||
time (required): the epoch timestamp that the reminder should be sent at
|
||||
notification_service (required): the id of the notification service to use to send the notification
|
||||
text: the body of the reminder
|
||||
repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The quantity of the repeat_interval
|
||||
repeat_interval: The number of the interval
|
||||
Returns:
|
||||
200:
|
||||
The id of the new reminder entry
|
||||
The info about the new reminder entry
|
||||
400:
|
||||
KeyNotFound: One of the required parameters was not given
|
||||
"""
|
||||
@@ -403,11 +425,15 @@ def api_reminders_list():
|
||||
time = extract_key(data, 'time')
|
||||
notification_service = extract_key(data, 'notification_service')
|
||||
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)
|
||||
|
||||
result = reminders.add(title=title,
|
||||
time=time,
|
||||
notification_service=notification_service,
|
||||
text=text)
|
||||
text=text,
|
||||
repeat_quantity=repeat_quantity,
|
||||
repeat_interval=repeat_interval)
|
||||
return return_api(result.get(), code=201)
|
||||
|
||||
@api.route('/reminders/search', methods=['GET'])
|
||||
@@ -433,7 +459,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):
|
||||
@@ -458,6 +484,8 @@ def api_get_reminder(r_id: int):
|
||||
time: The new epoch timestamp the the reminder should be send.
|
||||
notification_service: The new id of the notification service to use to send the reminder.
|
||||
text: The new body of the reminder.
|
||||
repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The quantity of the repeat_interval
|
||||
repeat_interval: The number of the interval
|
||||
Returns:
|
||||
200:
|
||||
Reminder updated successfully
|
||||
@@ -482,13 +510,123 @@ def api_get_reminder(r_id: int):
|
||||
time = extract_key(data, 'time', check_existence=False)
|
||||
notification_service = extract_key(data, 'notification_service', 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)
|
||||
|
||||
|
||||
result = reminders.fetchone(r_id).update(title=title,
|
||||
time=time,
|
||||
notification_service=notification_service,
|
||||
text=text)
|
||||
text=text,
|
||||
repeat_quantity=repeat_quantity,
|
||||
repeat_interval=repeat_interval)
|
||||
return return_api(result)
|
||||
|
||||
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({})
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
.form-container > form input,
|
||||
.form-container > form select,
|
||||
.form-container > form textarea {
|
||||
.form-container > form textarea,
|
||||
.form-container > form button {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -27,11 +28,71 @@
|
||||
}
|
||||
|
||||
.sub-inputs > input,
|
||||
.sub-inputs > select {
|
||||
.sub-inputs > select,
|
||||
.sub-inputs > button {
|
||||
width: calc(50% - (var(--gap) / 2));
|
||||
}
|
||||
|
||||
.options > button {
|
||||
.sub-inputs > button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
|
||||
border: 2px solid var(--color-gray);
|
||||
box-shadow: var(--default-shadow);
|
||||
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.sub-inputs > button > svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.sub-inputs > button[data-selected="false"] > svg {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.repeat-bar,
|
||||
.repeat-edit-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
|
||||
border: 2px solid var(--color-gray);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--default-shadow);
|
||||
}
|
||||
|
||||
.repeat-bar > *,
|
||||
.repeat-edit-bar > * {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.repeat-bar > p,
|
||||
.repeat-edit-bar > p {
|
||||
min-width: 7rem;
|
||||
|
||||
padding: 1rem;
|
||||
padding-right: .25rem;
|
||||
}
|
||||
|
||||
.repeat-bar > input[type="number"],
|
||||
.repeat-edit-bar > input[type="number"] {
|
||||
min-width: 5rem;
|
||||
|
||||
padding-left: .25rem;
|
||||
}
|
||||
|
||||
.repeat-bar > select,
|
||||
.repeat-edit-bar > select {
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
div.options > button {
|
||||
width: 6rem;
|
||||
|
||||
border: 2px solid var(--color-gray);
|
||||
@@ -46,6 +107,11 @@
|
||||
background-color: var(--color-gray);
|
||||
}
|
||||
|
||||
#delete-reminder {
|
||||
border-color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
.sub-inputs > input,
|
||||
.sub-inputs > select {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -262,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;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#add-service-button {
|
||||
@@ -18,66 +17,63 @@
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.table-container > table {
|
||||
.overflow-container {
|
||||
margin-inline: auto;
|
||||
|
||||
width: 100%;
|
||||
max-width: 50rem;
|
||||
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.overflow-container > table {
|
||||
border-spacing: 0px;
|
||||
}
|
||||
|
||||
.table-container > table th,
|
||||
.table-container > table td {
|
||||
.overflow-container > table th,
|
||||
.overflow-container > table td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table-container > table th {
|
||||
padding-block: .5rem;
|
||||
.overflow-container > table th {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
#services-list > tr > td {
|
||||
border-top: 1px solid var(--color-gray);
|
||||
}
|
||||
|
||||
#services-list > tr.edit > td {
|
||||
padding-block: .5rem;
|
||||
padding: .25rem;
|
||||
}
|
||||
|
||||
.title-column {
|
||||
width: 8rem;
|
||||
min-width: 9.5rem;
|
||||
width: 25%;
|
||||
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.url-column {
|
||||
width: 21rem;
|
||||
min-width: 26rem;
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
#services-list input {
|
||||
min-width: 6rem;
|
||||
|
||||
border-radius: 2px;
|
||||
padding: .5rem;
|
||||
padding: .25rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.url-column > input {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
#services-list input:read-only {
|
||||
border-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#services-list > tr.edit > td.action-column {
|
||||
padding-block: 1rem;
|
||||
}
|
||||
|
||||
.action-column {
|
||||
#services-list > tr > td.action-column {
|
||||
min-width: 4rem;
|
||||
width: 20%;
|
||||
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
|
||||
padding: .5rem;
|
||||
padding: calc(.5rem + 2px);
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -92,6 +88,6 @@
|
||||
}
|
||||
|
||||
#services-list > tr.edit > td.action-column > button[data-type="edit"],
|
||||
#services-list > tr:not(.edit) > td.action-column > button[data-type="save"] {
|
||||
#services-list > tr:not(.edit):not(#add-row) > td.action-column > button[data-type="save"] {
|
||||
display: none;
|
||||
}
|
||||
@@ -46,25 +46,30 @@
|
||||
}
|
||||
|
||||
/* REMINDER LIST */
|
||||
#reminder-list {
|
||||
#reminder-list,
|
||||
#template-list {
|
||||
--gap: 1rem;
|
||||
--entry-width: 13rem;
|
||||
max-width: 43rem;
|
||||
margin-inline: auto;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
justify-content: left;
|
||||
gap: var(--gap);
|
||||
flex-wrap: wrap;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -79,67 +84,41 @@
|
||||
}
|
||||
|
||||
.entry {
|
||||
width: 13rem;
|
||||
width: var(--entry-width);
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: .1rem;
|
||||
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
padding: .75rem;
|
||||
background-color: var(--color-gray);
|
||||
}
|
||||
|
||||
button.entry.fit {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.entry.expand {
|
||||
width: calc(var(--entry-width) * 2 + var(--gap));
|
||||
}
|
||||
|
||||
.entry h2 {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.entry-overlay {
|
||||
--height: 1.7rem;
|
||||
height: 100%;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--height);
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
padding: .5rem;
|
||||
background-color: rgb(0 0 0 / 25%);
|
||||
|
||||
transition: opacity .2s ease-in;
|
||||
text-align: left;
|
||||
font-size: 1.25em;
|
||||
line-height: 1.18;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entry-overlay::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 25px;
|
||||
width: 100%;
|
||||
background: linear-gradient(transparent, rgb(0 0 0 / 25%));
|
||||
left: 0;
|
||||
bottom: var(--height);
|
||||
}
|
||||
|
||||
.entry-overlay > button {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.entry-overlay > button > svg {
|
||||
width: .9rem;
|
||||
height: .9rem;
|
||||
}
|
||||
|
||||
.entry:hover > .entry-overlay {
|
||||
opacity: 1;
|
||||
#delete-template {
|
||||
border-color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
@media (max-width: 543px) {
|
||||
@@ -5,13 +5,30 @@ const inputs = {
|
||||
'text': document.getElementById('text-input')
|
||||
};
|
||||
|
||||
const type_buttons = {
|
||||
'normal-button': document.getElementById('normal-button'),
|
||||
'repeat-button': document.getElementById('repeat-button'),
|
||||
|
||||
'repeat-bar': document.querySelector('.repeat-bar'),
|
||||
'repeat-interval': document.getElementById('repeat-interval'),
|
||||
'repeat-quantity': document.getElementById('repeat-quantity')
|
||||
};
|
||||
|
||||
function addReminder() {
|
||||
inputs.time.classList.remove('error-input');
|
||||
inputs.time.removeAttribute('title');
|
||||
|
||||
const data = {
|
||||
'title': inputs.title.value,
|
||||
'time': new Date(inputs.time.value).getTime() / 1000,
|
||||
'notification_service': inputs.notification_service.value,
|
||||
'text': inputs.text.value
|
||||
};
|
||||
if (type_buttons['repeat-button'].dataset.selected === 'true') {
|
||||
data['repeat_quantity'] = type_buttons['repeat-quantity'].value;
|
||||
data['repeat_interval'] = type_buttons['repeat-interval'].value
|
||||
};
|
||||
|
||||
fetch(`/api/reminders?api_key=${api_key}`, {
|
||||
'method': 'POST',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
@@ -30,6 +47,9 @@ function addReminder() {
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = '/';
|
||||
} else if (e === 400) {
|
||||
inputs.time.classList.add('error-input');
|
||||
inputs.time.title = 'Time is in the past';
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
@@ -38,21 +58,46 @@ function addReminder() {
|
||||
|
||||
function showAdd() {
|
||||
if (!document.getElementById('add-entry').classList.contains('error')) {
|
||||
loadTemplates(force=false);
|
||||
showWindow('add');
|
||||
} else {
|
||||
showWindow('notification');
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
toggleNormal();
|
||||
inputs.text.value = '';
|
||||
}, 500);
|
||||
};
|
||||
|
||||
function toggleNormal() {
|
||||
type_buttons['normal-button'].dataset.selected = 'true';
|
||||
type_buttons['repeat-button'].dataset.selected = 'false';
|
||||
|
||||
type_buttons['repeat-bar'].classList.add('hidden');
|
||||
type_buttons['repeat-interval'].removeAttribute('required');
|
||||
type_buttons['repeat-interval'].value = '';
|
||||
};
|
||||
|
||||
function toggleRepeated() {
|
||||
type_buttons['normal-button'].dataset.selected = 'false';
|
||||
type_buttons['repeat-button'].dataset.selected = 'true';
|
||||
|
||||
type_buttons['repeat-bar'].classList.remove('hidden');
|
||||
type_buttons['repeat-interval'].setAttribute('required', '');
|
||||
};
|
||||
|
||||
// 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());
|
||||
|
||||
@@ -5,14 +5,30 @@ const edit_inputs = {
|
||||
'text': document.getElementById('text-edit-input')
|
||||
};
|
||||
|
||||
const edit_type_buttons = {
|
||||
'normal-edit-button': document.getElementById('normal-edit-button'),
|
||||
'repeat-edit-button': document.getElementById('repeat-edit-button'),
|
||||
|
||||
'repeat-edit-bar': document.querySelector('.repeat-edit-bar'),
|
||||
'repeat-edit-interval': document.getElementById('repeat-edit-interval'),
|
||||
'repeat-edit-quantity': document.getElementById('repeat-edit-quantity')
|
||||
};
|
||||
|
||||
function editReminder() {
|
||||
const id = document.getElementById('edit-form').dataset.id;
|
||||
const data = {
|
||||
'title': edit_inputs.title.value,
|
||||
'time': new Date(edit_inputs.time.value).getTime() / 1000,
|
||||
'notification_service': edit_inputs.notification_service.value,
|
||||
'text': edit_inputs.text.value
|
||||
'text': edit_inputs.text.value,
|
||||
'repeat_quantity': null,
|
||||
'repeat_interval': null
|
||||
};
|
||||
if (edit_type_buttons['repeat-edit-button'].dataset.selected === 'true') {
|
||||
data['repeat_quantity'] = edit_type_buttons['repeat-edit-quantity'].value;
|
||||
data['repeat_interval'] = edit_type_buttons['repeat-edit-interval'].value;
|
||||
};
|
||||
|
||||
fetch(`/api/reminders/${id}?api_key=${api_key}`, {
|
||||
'method': 'PUT',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
@@ -50,11 +66,22 @@ function showEdit(id) {
|
||||
})
|
||||
.then(json => {
|
||||
edit_inputs.title.value = json.result.title;
|
||||
|
||||
edit_inputs.time.value = new Date(
|
||||
(json.result.time + new Date(json.result.time * 1000).getTimezoneOffset() * -60) * 1000
|
||||
).toISOString().slice(0, 16);
|
||||
edit_inputs.notification_service.value = json.result.notification_service;
|
||||
|
||||
if (json.result['repeat_interval'] === null) {
|
||||
toggleEditNormal();
|
||||
} else {
|
||||
toggleEditRepeated();
|
||||
edit_type_buttons['repeat-edit-interval'].value = json.result['repeat_interval'];
|
||||
edit_type_buttons['repeat-edit-quantity'].value = json.result['repeat_quantity'];
|
||||
};
|
||||
|
||||
edit_inputs.text.value = json.result.text !== null ? json.result.text : '';
|
||||
|
||||
showWindow('edit');
|
||||
})
|
||||
.catch(e => {
|
||||
@@ -68,7 +95,53 @@ function showEdit(id) {
|
||||
});
|
||||
};
|
||||
|
||||
function toggleEditNormal() {
|
||||
edit_type_buttons['normal-edit-button'].dataset.selected = 'true';
|
||||
edit_type_buttons['repeat-edit-button'].dataset.selected = 'false';
|
||||
|
||||
edit_type_buttons['repeat-edit-bar'].classList.add('hidden');
|
||||
edit_type_buttons['repeat-edit-interval'].removeAttribute('required');
|
||||
edit_type_buttons['repeat-edit-interval'].value = '';
|
||||
};
|
||||
|
||||
function toggleEditRepeated() {
|
||||
edit_type_buttons['normal-edit-button'].dataset.selected = 'false';
|
||||
edit_type_buttons['repeat-edit-button'].dataset.selected = 'true';
|
||||
|
||||
edit_type_buttons['repeat-edit-bar'].classList.remove('hidden');
|
||||
edit_type_buttons['repeat-edit-interval'].setAttribute('required', '');
|
||||
};
|
||||
|
||||
function deleteReminder() {
|
||||
const id = document.getElementById('edit-form').dataset.id;
|
||||
fetch(`/api/reminders/${id}?api_key=${api_key}`, {
|
||||
'method': 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
|
||||
fillList();
|
||||
hideWindow();
|
||||
return;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = '/';
|
||||
} else if (e === 404) {
|
||||
fillList();
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
document.getElementById('edit-form').setAttribute('action', 'javascript:editReminder();');
|
||||
document.getElementById('normal-edit-button').addEventListener('click', e => toggleEditNormal());
|
||||
document.getElementById('repeat-edit-button').addEventListener('click', e => toggleEditRepeated());
|
||||
document.getElementById('close-edit').addEventListener('click', e => hideWindow());
|
||||
document.getElementById('delete-reminder').addEventListener('click', e => deleteReminder());
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
function fillTable(result) {
|
||||
const table = document.getElementById('reminder-list');
|
||||
table.querySelectorAll('button:not(#add-entry)').forEach(e => e.remove());
|
||||
table.querySelectorAll('button.entry:not(#add-entry)').forEach(e => e.remove());
|
||||
|
||||
result.forEach(reminder => {
|
||||
const entry = document.createElement('button');
|
||||
entry.classList.add('entry');
|
||||
entry.dataset.id = reminder.id;
|
||||
entry.addEventListener('click', e => showEdit(reminder.id));
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.innerText = reminder.title;
|
||||
@@ -14,29 +15,26 @@ function fillTable(result) {
|
||||
const time = document.createElement('p');
|
||||
var d = new Date(reminder.time * 1000);
|
||||
var formatted_date = d.toLocaleString('en-CA').slice(0,10) + ' ' + d.toTimeString().slice(0,5);
|
||||
if (reminder.repeat_interval !== null) {
|
||||
if (reminder.repeat_interval === 1) {
|
||||
var quantity = reminder.repeat_quantity.endsWith('s') ? reminder.repeat_quantity.slice(0, -1) : reminder.repeat_quantity;
|
||||
var interval_text = ` (each ${quantity})`;
|
||||
} else {
|
||||
var quantity = reminder.repeat_quantity.endsWith('s') ? reminder.repeat_quantity : reminder.repeat_quantity + 's';
|
||||
var interval_text = ` (every ${reminder.repeat_interval} ${quantity})`;
|
||||
};
|
||||
formatted_date += interval_text;
|
||||
};
|
||||
time.innerText = formatted_date;
|
||||
entry.appendChild(time);
|
||||
|
||||
const options = document.createElement('div');
|
||||
options.classList.add('entry-overlay');
|
||||
entry.appendChild(options);
|
||||
|
||||
const edit_entry = document.createElement('button');
|
||||
edit_entry.addEventListener('click', e => showEdit(reminder.id));
|
||||
edit_entry.innerHTML = edit_icon;
|
||||
edit_entry.title = 'Edit reminder';
|
||||
edit_entry.setAttribute('aria-label', 'Edit reminder');
|
||||
options.appendChild(edit_entry);
|
||||
|
||||
const delete_entry = document.createElement('button');
|
||||
delete_entry.addEventListener('click', e => deleteReminder(reminder.id));
|
||||
delete_entry.innerHTML = delete_icon;
|
||||
delete_entry.title = 'Delete reminder';
|
||||
delete_entry.setAttribute('aria-label', 'Delete reminder');
|
||||
options.appendChild(delete_entry);
|
||||
|
||||
table.appendChild(entry);
|
||||
|
||||
if (title.clientHeight < title.scrollHeight) {
|
||||
entry.classList.add('expand');
|
||||
};
|
||||
});
|
||||
table.querySelectorAll('button.entry:not(#add-entry)').forEach(reminder => reminder.classList.add('fit'));
|
||||
};
|
||||
|
||||
function fillList() {
|
||||
@@ -87,30 +85,6 @@ function clearSearch() {
|
||||
fillList();
|
||||
}
|
||||
|
||||
function deleteReminder(id) {
|
||||
const entry = document.querySelector(`button.entry[data-id="${id}"]`);
|
||||
entry.remove();
|
||||
|
||||
fetch(`/api/reminders/${id}?api_key=${api_key}`, {
|
||||
'method': 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = '/';
|
||||
} else if (e === 404) {
|
||||
fillList();
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
fillList();
|
||||
@@ -119,3 +93,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,54 +75,106 @@
|
||||
</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>
|
||||
<select id="notification-service-input" required></select>
|
||||
</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">
|
||||
<g>
|
||||
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
Normal
|
||||
</button>
|
||||
<button type="button" id="repeat-button" data-selected="false">
|
||||
<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">
|
||||
<g>
|
||||
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
Repeated
|
||||
</button>
|
||||
</div>
|
||||
<div class="repeat-bar hidden">
|
||||
<p>Repeat every </p>
|
||||
<input type="number" id="repeat-interval" placeholder="interval" min="1" step="1" oninput="validity.valid || (value='');">
|
||||
<select id="repeat-quantity">
|
||||
<option value="minutes">Minute(s)</option>
|
||||
<option value="hours">Hour(s)</option>
|
||||
<option value="day" selected>Day(s)</option>
|
||||
<option value="week">Week(s)</option>
|
||||
<option value="month">Month(s)</option>
|
||||
<option value="year">Year(s)</option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea id="text-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
|
||||
<div class="options">
|
||||
<button type="button" id="close-add">Cancel</button>
|
||||
@@ -139,9 +192,40 @@
|
||||
<input type="datetime-local" id="time-edit-input" required>
|
||||
<select id="notification-service-edit-input" required></select>
|
||||
</div>
|
||||
<div class="sub-inputs">
|
||||
<button type="button" id="normal-edit-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 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
Normal
|
||||
</button>
|
||||
<button type="button" id="repeat-edit-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 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
Repeated
|
||||
</button>
|
||||
</div>
|
||||
<div class="repeat-edit-bar">
|
||||
<p>Repeat every </p>
|
||||
<input type="number" id="repeat-edit-interval" placeholder="interval" min="1" step="1" oninput="validity.valid || (value='');">
|
||||
<select id="repeat-edit-quantity">
|
||||
<option value="minutes">Minute(s)</option>
|
||||
<option value="hours">Hour(s)</option>
|
||||
<option value="day" selected>Day(s)</option>
|
||||
<option value="week">Week(s)</option>
|
||||
<option value="month">Month(s)</option>
|
||||
<option value="year">Year(s)</option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea id="text-edit-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
|
||||
<div class="options">
|
||||
<button type="button" id="close-edit">Cancel</button>
|
||||
<button type="button" id="delete-reminder">Delete</button>
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -160,40 +244,42 @@
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="title-column">Title</th>
|
||||
<th class="url-column">Apprise URL</th>
|
||||
<th title="Actions" aria-label="Actions" class="action-column">
|
||||
<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="M11.24,24a2.262,2.262,0,0,1-.948-.212,2.18,2.18,0,0,1-1.2-2.622L10.653,16H6.975A3,3,0,0,1,4.1,12.131l3.024-10A2.983,2.983,0,0,1,10,0h3.693a2.6,2.6,0,0,1,2.433,3.511L14.443,8H17a3,3,0,0,1,2.483,4.684l-6.4,10.3A2.2,2.2,0,0,1,11.24,24ZM10,2a1,1,0,0,0-.958.71l-3.024,10A1,1,0,0,0,6.975,14H12a1,1,0,0,1,.957,1.29L11.01,21.732a.183.183,0,0,0,.121.241A.188.188,0,0,0,11.4,21.9l6.4-10.3a1,1,0,0,0,.078-1.063A.979.979,0,0,0,17,10H13a1,1,0,0,1-.937-1.351l2.19-5.84A.6.6,0,0,0,13.693,2Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="services-list">
|
||||
<tr id="add-row" class="hidden">
|
||||
<td class="title-column">
|
||||
<input type="text">
|
||||
</td>
|
||||
<td class="url-column">
|
||||
<input type="text">
|
||||
</td>
|
||||
<td class="action-column">
|
||||
<button data-type="save" title="Add" aria-label="Add">
|
||||
<div class="overflow-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="title-column">Title</th>
|
||||
<th class="url-column">Apprise URL</th>
|
||||
<th title="Actions" aria-label="Actions" class="action-column">
|
||||
<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>
|
||||
<path d="M11.24,24a2.262,2.262,0,0,1-.948-.212,2.18,2.18,0,0,1-1.2-2.622L10.653,16H6.975A3,3,0,0,1,4.1,12.131l3.024-10A2.983,2.983,0,0,1,10,0h3.693a2.6,2.6,0,0,1,2.433,3.511L14.443,8H17a3,3,0,0,1,2.483,4.684l-6.4,10.3A2.2,2.2,0,0,1,11.24,24ZM10,2a1,1,0,0,0-.958.71l-3.024,10A1,1,0,0,0,6.975,14H12a1,1,0,0,1,.957,1.29L11.01,21.732a.183.183,0,0,0,.121.241A.188.188,0,0,0,11.4,21.9l6.4-10.3a1,1,0,0,0,.078-1.063A.979.979,0,0,0,17,10H13a1,1,0,0,1-.937-1.351l2.19-5.84A.6.6,0,0,0,13.693,2Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="services-list">
|
||||
<tr id="add-row" class="hidden">
|
||||
<td class="title-column">
|
||||
<input type="text">
|
||||
</td>
|
||||
<td class="url-column">
|
||||
<input type="text">
|
||||
</td>
|
||||
<td class="action-column">
|
||||
<button data-type="save" title="Add" aria-label="Add">
|
||||
<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>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settings">
|
||||
@@ -208,6 +294,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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
apprise>=0.9.9
|
||||
python-dateutil>=2.8.2
|
||||
Flask>=2.1.2
|
||||
waitress>=2.1.2
|
||||
20
tests/Noted_test.py
Normal file
20
tests/Noted_test.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import unittest
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from frontend.api import api
|
||||
from frontend.ui import ui
|
||||
from Noted import _create_app
|
||||
|
||||
class Test_Noted(unittest.TestCase):
|
||||
def test_create_app(self):
|
||||
result = _create_app()
|
||||
self.assertIsInstance(result, Flask)
|
||||
|
||||
self.assertEqual(result.blueprints.get('ui'), ui)
|
||||
self.assertEqual(result.blueprints.get('api'), api)
|
||||
|
||||
handlers = result.error_handler_spec[None].keys()
|
||||
required_handlers = 404, 400, 405, 500
|
||||
for handler in required_handlers:
|
||||
self.assertIn(handler, handlers)
|
||||
62
tests/api_test.py
Normal file
62
tests/api_test.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import unittest
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from backend.custom_exceptions import *
|
||||
from frontend.api import api, auth, error_handler, extract_key, return_api
|
||||
|
||||
class Test_API(unittest.TestCase):
|
||||
def test_blueprint(self):
|
||||
self.assertIsInstance(api, Blueprint)
|
||||
|
||||
def test_return_api(self):
|
||||
for case in ({'result': {}, 'error': 'Error', 'code': 201},
|
||||
{'result': ''}):
|
||||
result = return_api(**case)
|
||||
self.assertEqual(result[0]['result'], case['result'])
|
||||
if case.get('error'):
|
||||
self.assertEqual(result[0]['error'], case['error'])
|
||||
else:
|
||||
self.assertIsNone(result[0]['error'])
|
||||
if case.get('code'):
|
||||
self.assertEqual(result[1], case['code'])
|
||||
else:
|
||||
self.assertEqual(result[1], 200)
|
||||
|
||||
def test_auth(self):
|
||||
method = lambda x: x
|
||||
result = auth(method)
|
||||
self.assertEqual(result.__name__, method.__name__)
|
||||
|
||||
def _raise_exception(self, e, *args):
|
||||
raise e(*args)
|
||||
|
||||
def test_error_handler(self):
|
||||
result = error_handler(self._raise_exception)
|
||||
self.assertEqual(result.__name__, self._raise_exception.__name__)
|
||||
self.assertEqual(result(UsernameTaken), return_api(**UsernameTaken.api_response))
|
||||
self.assertEqual(result(UsernameInvalid), return_api(**UsernameInvalid.api_response))
|
||||
self.assertEqual(result(UserNotFound), return_api(**UserNotFound.api_response))
|
||||
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(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):
|
||||
result(TypeError)
|
||||
with self.assertRaises(KeyError):
|
||||
result(KeyError)
|
||||
|
||||
def test_extract_key(self):
|
||||
with self.assertRaises(KeyNotFound):
|
||||
extract_key({'test': 'value'}, 'no_key')
|
||||
self.assertIsNone(extract_key({'test': 'value'}, 'no_key', check_existence=False))
|
||||
with self.assertRaises(InvalidKeyValue):
|
||||
extract_key({'time': ''}, 'time')
|
||||
self.assertIsInstance(extract_key({'time': '1'}, 'time'), int)
|
||||
|
||||
|
||||
|
||||
18
tests/custom_exceptions_test.py
Normal file
18
tests/custom_exceptions_test.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import unittest
|
||||
from inspect import getmembers, getmro, isclass
|
||||
from sys import modules
|
||||
from typing import List
|
||||
|
||||
import backend.custom_exceptions
|
||||
|
||||
class Test_Custom_Exceptions(unittest.TestCase):
|
||||
def test_type(self):
|
||||
defined_exceptions: List[Exception] = map(lambda c: c[1], getmembers(modules['backend.custom_exceptions'], isclass))
|
||||
for defined_exception in defined_exceptions:
|
||||
self.assertEqual(getmro(defined_exception)[1], Exception)
|
||||
result = defined_exception().api_response
|
||||
self.assertIsInstance(result, dict)
|
||||
result['error']
|
||||
result['result']
|
||||
result['code']
|
||||
self.assertIsInstance(result['code'], int)
|
||||
12
tests/db_test.py
Normal file
12
tests/db_test.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import unittest
|
||||
|
||||
from backend.db import DBConnection
|
||||
from Noted import DB_FILENAME
|
||||
|
||||
class Test_DB(unittest.TestCase):
|
||||
def test_foreign_key(self):
|
||||
DBConnection.file = DB_FILENAME
|
||||
instance = DBConnection(timeout=20.0)
|
||||
self.assertEqual(instance.cursor().execute("PRAGMA foreign_keys;").fetchone()[0], 1)
|
||||
|
||||
|
||||
29
tests/reminders_test.py
Normal file
29
tests/reminders_test.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import unittest
|
||||
from threading import Thread
|
||||
|
||||
from backend.reminders import filter_function, ReminderHandler
|
||||
|
||||
class Test_Reminder_Handler(unittest.TestCase):
|
||||
def test_starting_stopping(self):
|
||||
context = 'test'
|
||||
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',
|
||||
'text': 'TEXT',
|
||||
'notification_service_title': 'NOTIFICATION_SERVICE_TITLE'
|
||||
}
|
||||
for test_case in ('', 'title', 'service', 'ex'):
|
||||
self.assertTrue(filter_function(test_case, p))
|
||||
for test_case in (' ', 'Hello'):
|
||||
self.assertFalse(filter_function(test_case, p))
|
||||
|
||||
10
tests/security_test.py
Normal file
10
tests/security_test.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import unittest
|
||||
|
||||
from backend.security import generate_salt_hash, get_hash
|
||||
|
||||
class Test_Security(unittest.TestCase):
|
||||
def test_hash(self):
|
||||
for test_case in ('test', ''):
|
||||
result = generate_salt_hash(test_case)
|
||||
self.assertEqual(result[1], get_hash(result[0], test_case))
|
||||
|
||||
20
tests/ui_test.py
Normal file
20
tests/ui_test.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import unittest
|
||||
|
||||
from flask import Blueprint, Flask
|
||||
|
||||
from frontend.ui import methods, ui
|
||||
|
||||
class Test_UI(unittest.TestCase):
|
||||
def test_methods(self):
|
||||
self.assertEqual(len(methods), 1)
|
||||
self.assertEqual(methods[0], 'GET')
|
||||
|
||||
def test_blueprint(self):
|
||||
self.assertIsInstance(ui, Blueprint)
|
||||
|
||||
def test_route_methods(self):
|
||||
temp_app = Flask(__name__)
|
||||
temp_app.register_blueprint(ui)
|
||||
for rule in temp_app.url_map.iter_rules():
|
||||
self.assertEqual(len(rule.methods), 3)
|
||||
self.assertIn(methods[0], rule.methods)
|
||||
14
tests/users_test.py
Normal file
14
tests/users_test.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import unittest
|
||||
|
||||
from backend.custom_exceptions import UsernameInvalid
|
||||
from backend.users import ONEPASS_INVALID_USERNAMES, _check_username
|
||||
|
||||
class Test_Users(unittest.TestCase):
|
||||
def test_username_check(self):
|
||||
for test_case in ('', 'test'):
|
||||
_check_username(test_case)
|
||||
|
||||
for test_case in (' ', ' ', '0', 'api', *ONEPASS_INVALID_USERNAMES):
|
||||
with self.assertRaises(UsernameInvalid):
|
||||
_check_username(test_case)
|
||||
|
||||
Reference in New Issue
Block a user