Merge pull request #45 from Casvt/Development

V1.2.1
This commit is contained in:
Casvt
2023-03-12 23:18:27 +01:00
committed by GitHub
17 changed files with 116 additions and 91 deletions

23
MIND.py
View File

@@ -1,13 +1,15 @@
#!/usr/bin/env python3
#-*- coding: utf-8 -*-
import logging
from os import makedirs, urandom
from os.path import abspath, dirname, join, isfile
from os.path import abspath, dirname, isfile, join
from shutil import move
from sys import version_info
from flask import Flask, render_template, request
from flask import Flask
from waitress.server import create_server
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from backend.db import DBConnection, close_db, setup_db
from frontend.api import api, reminder_handler
@@ -15,6 +17,7 @@ from frontend.ui import ui
HOST = '0.0.0.0'
PORT = '8080'
URL_PREFIX = '' # Must either be empty or start with '/' e.g. '/mind'
THREADS = 10
DB_FILENAME = 'db', 'MIND.db'
@@ -39,15 +42,10 @@ def _create_app() -> Flask:
app.config['SECRET_KEY'] = urandom(32)
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True
app.config['JSON_SORT_KEYS'] = False
app.config['APPLICATION_ROOT'] = URL_PREFIX
app.wsgi_app = DispatcherMiddleware(Flask(__name__), {URL_PREFIX: app.wsgi_app})
# Add error handlers
@app.errorhandler(404)
def not_found(e):
if request.path.startswith('/api'):
return {'error': 'Not Found', 'result': {}}, 404
else:
return render_template('page_not_found.html')
@app.errorhandler(400)
def bad_request(e):
return {'error': 'Bad request', 'result': {}}, 400
@@ -76,8 +74,13 @@ def MIND() -> None:
# Check python version
if (version_info.major < 3) or (version_info.major == 3 and version_info.minor < 7):
print('Error: the minimum python version required is python3.7 (currently ' + version_info.major + '.' + version_info.minor + '.' + version_info.micro + ')')
exit(1)
# Register web server
# We need to get the value to ui.py but MIND.py imports from ui.py so we get an import loop.
# To go around this, we abuse the fact that the logging module is a singleton.
# We add an attribute to the logging module and in ui.py get the value this way.
logging.URL_PREFIX = URL_PREFIX
app = _create_app()
with app.app_context():
if isfile(_folder_path('db', 'Noted.db')):
@@ -93,7 +96,7 @@ def MIND() -> None:
# Create waitress server and run
server = create_server(app, host=HOST, port=PORT, threads=THREADS)
print(f'MIND running on http://{HOST}:{PORT}/')
print(f'MIND running on http://{HOST}:{PORT}{URL_PREFIX}')
server.run()
print(f'\nShutting down MIND...')

View File

@@ -8,7 +8,7 @@ from typing import Union
from flask import g
__DATABASE_VERSION__ = 3
__DATABASE_VERSION__ = 4
class Singleton(type):
_instances = {}
@@ -90,6 +90,15 @@ def migrate_db(current_db_version: int) -> None:
ADD color VARCHAR(7);
""")
current_db_version = 3
if current_db_version == 3:
# V3 -> V4
cursor.executescript("""
UPDATE reminders
SET repeat_quantity = repeat_quantity || 's'
WHERE repeat_quantity NOT LIKE '%s';
""")
current_db_version = 4
return

View File

@@ -4,7 +4,6 @@ from datetime import datetime
from sqlite3 import IntegrityError
from threading import Thread
from time import sleep
from time import time as epoch_time
from typing import List, Literal
from apprise import Apprise
@@ -24,7 +23,7 @@ filter_function = lambda query, p: (
def _find_next_time(
original_time: int,
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"],
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"],
repeat_interval: int
) -> int:
td = relativedelta(**{repeat_quantity: repeat_interval})
@@ -176,7 +175,7 @@ class Reminder:
time: int = None,
notification_service: int = None,
text: str = None,
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None,
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"] = None,
repeat_interval: int = None,
color: str = None
) -> dict:
@@ -187,7 +186,7 @@ class Reminder:
time (int): The new UTC epoch timestamp the the reminder should be send. Defaults to None.
notification_service (int): The new id of the notification service to use to send the reminder. Defaults to None.
text (str, optional): The new body of the reminder. Defaults to None.
repeat_quantity (Literal["year", "month", "week", "day", "hours", "minutes"], optional): The new quantity of the repeat specified for the reminder. Defaults to None.
repeat_quantity (Literal["years", "months", "weeks", "days", "hours", "minutes"], optional): The new quantity of the repeat specified for the reminder. Defaults to None.
repeat_interval (int, optional): The new amount of repeat_quantity, like "5" (hours). Defaults to None.
color (str, optional): The new hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.
@@ -358,7 +357,7 @@ class Reminders:
time: int,
notification_service: int,
text: str = '',
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None,
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"] = None,
repeat_interval: int = None,
color: str = None
) -> Reminder:
@@ -369,7 +368,7 @@ class Reminders:
time (int): The UTC epoch timestamp the the reminder should be send.
notification_service (int): The id of the notification service to use to send the reminder.
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_quantity (Literal["years", "months", "weeks", "days", "hours", "minutes"], optional): The quantity of the repeat specified for the reminder. Defaults to None.
repeat_interval (int, optional): The amount of repeat_quantity, like "5" (hours). Defaults to None.
color (str, optional): The hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.

View File

@@ -99,7 +99,7 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
raise InvalidKeyValue(key, value)
elif key == 'repeat_quantity':
if not value in ("year", "month", "week", "day", "hours", "minutes"):
if not value in ("years", "months", "weeks", "days", "hours", "minutes"):
raise InvalidKeyValue(key, value)
elif key in ('username', 'password', 'new_password', 'title', 'url',
@@ -120,6 +120,10 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
return value
@api.errorhandler(404)
def not_found(e):
return return_api({}, 'Not Found', 404)
#===================
# Authentication endpoints
#===================
@@ -410,7 +414,7 @@ def api_reminders_list():
time (required): the UTC epoch timestamp that the reminder should be sent at
notification_service (required): the id of the notification service to use to send the notification
text: the body of the reminder
repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The quantity of the repeat_interval
repeat_quantity ('years', 'months', 'weeks', 'days', 'hours', 'minutes'): The quantity of the repeat_interval
repeat_interval: The number of the interval
color: The hex code of the color of the reminder, which is shown in the web-ui
Returns:
@@ -523,7 +527,7 @@ def api_get_reminder(r_id: int):
time: The new UTC epoch timestamp the the reminder should be send.
notification_service: The new id of the notification service to use to send the reminder.
text: The new body of the reminder.
repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The new quantity of the repeat_interval.
repeat_quantity ('years', 'months', 'weeks', 'days', 'hours', 'minutes'): The new quantity of the repeat_interval.
repeat_interval: The new number of the interval.
color: The new hex code of the color of the reminder, which is shown in the web-ui.
Returns:

View File

@@ -35,7 +35,7 @@ function addReminder() {
data['repeat_interval'] = type_buttons['repeat-interval'].value
};
fetch(`/api/reminders?api_key=${api_key}`, {
fetch(`${url_prefix}/api/reminders?api_key=${api_key}`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -52,7 +52,7 @@ function addReminder() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else if (e === 400) {
inputs.time.classList.add('error-input');
inputs.time.title = 'Time is in the past';
@@ -144,7 +144,7 @@ function testReminder() {
'notification_service': inputs.notification_service.value,
'text': inputs.text.value
};
fetch(`/api/reminders/test?api_key=${api_key}`, {
fetch(`${url_prefix}/api/reminders/test?api_key=${api_key}`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -158,7 +158,7 @@ function testReminder() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
};
});
};

View File

@@ -34,7 +34,7 @@ function editReminder() {
data['repeat_interval'] = edit_type_buttons['repeat-edit-interval'].value;
};
fetch(`/api/reminders/${id}?api_key=${api_key}`, {
fetch(`${url_prefix}/api/reminders/${id}?api_key=${api_key}`, {
'method': 'PUT',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -51,7 +51,7 @@ function editReminder() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};
@@ -60,7 +60,7 @@ function editReminder() {
function showEdit(id) {
document.getElementById('edit-form').dataset.id = id;
fetch(`/api/reminders/${id}?api_key=${api_key}`)
fetch(`${url_prefix}/api/reminders/${id}?api_key=${api_key}`)
.then(response => {
// catch errors
if (!response.ok) {
@@ -99,7 +99,7 @@ function showEdit(id) {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else if (e === 404) {
fillList();
} else {
@@ -127,7 +127,7 @@ function toggleEditRepeated() {
function deleteReminder() {
const id = document.getElementById('edit-form').dataset.id;
fetch(`/api/reminders/${id}?api_key=${api_key}`, {
fetch(`${url_prefix}/api/reminders/${id}?api_key=${api_key}`, {
'method': 'DELETE'
})
.then(response => {
@@ -142,7 +142,7 @@ function deleteReminder() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else if (e === 404) {
fillList();
} else {

View File

@@ -1,10 +1,10 @@
function logout() {
fetch(`/api/auth/logout?api_key=${api_key}`, {
fetch(`${url_prefix}/api/auth/logout?api_key=${api_key}`, {
'method': 'POST'
})
.then(response => {
sessionStorage.removeItem('api_key');
window.location.href = '/';
window.location.href = url_prefix || '/';
});
};
@@ -49,9 +49,10 @@ function showTab(tab_id, button_id, load_function=null) {
// code run on load
const url_prefix = document.getElementById('url_prefix').dataset.value;
const api_key = sessionStorage.getItem('api_key');
if (api_key === null) {
window.location.href = '/';
window.location.href = url_prefix;
};
document.getElementById('toggle-nav').addEventListener('click', e => toggleNav());

View File

@@ -10,7 +10,7 @@ function login(data=null) {
'password': document.getElementById('password-input').value
};
};
fetch(`/api/auth/login`, {
fetch(`${url_prefix}/api/auth/login`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -25,7 +25,7 @@ function login(data=null) {
})
.then(json => {
sessionStorage.setItem('api_key', json.result.api_key);
window.location.href = '/reminders';
window.location.href = `${url_prefix}/reminders`;
})
.catch(e => {
if (e === 401) {
@@ -49,7 +49,7 @@ function create() {
'username': document.getElementById('new-username-input').value,
'password': document.getElementById('new-password-input').value
};
fetch(`/api/user/add`, {
fetch(`${url_prefix}/api/user/add`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -81,6 +81,8 @@ function toggleWindow() {
// code run on load
const url_prefix = document.getElementById('url_prefix').dataset.value;
document.getElementById('login-form').setAttribute('action', 'javascript:login();');
document.getElementById('create-form').setAttribute('action', 'javascript:create();');
document.querySelectorAll('.switch-button').forEach(e => e.addEventListener('click', e => toggleWindow()));

View File

@@ -1,5 +1,5 @@
function fillNotificationSelection() {
fetch(`/api/notificationservices?api_key=${api_key}`)
fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`)
.then(response => {
// catch errors
if (!response.ok) {
@@ -87,7 +87,7 @@ function fillNotificationSelection() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};
@@ -96,7 +96,7 @@ function fillNotificationSelection() {
function deleteService(id) {
const row = document.querySelector(`tr[data-id="${id}"]`);
fetch(`/api/notificationservices/${id}?api_key=${api_key}`, {
fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}`, {
'method': 'DELETE'
})
.then(response => response.json())
@@ -113,7 +113,7 @@ function deleteService(id) {
})
.catch(e => {
if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid') {
window.location.href = '/';
window.location.href = url_prefix;
} else if (e.error === 'NotificationServiceInUse') {
const delete_button = row.querySelector('button[title="Delete"]');
delete_button.classList.add('error-icon');
@@ -136,7 +136,7 @@ function saveService(id) {
'title': row.querySelector(`td.title-column > input`).value,
'url': row.querySelector(`td.url-column > input`).value
};
fetch(`/api/notificationservices/${id}?api_key=${api_key}`, {
fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}`, {
'method': 'PUT',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -151,7 +151,7 @@ function saveService(id) {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else if (e === 400) {
save_button.classList.add('error-icon');
save_button.title = 'Invalid Apprise URL';
@@ -176,7 +176,7 @@ function addService() {
'title': inputs_buttons.title.value,
'url': inputs_buttons.url.value
};
fetch(`/api/notificationservices?api_key=${api_key}`, {
fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -199,7 +199,7 @@ function addService() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else if (e === 400) {
inputs_buttons.save_button.classList.add('error-icon');
inputs_buttons.save_button.title = 'Invalid Apprise URL';

View File

@@ -42,7 +42,7 @@ function fillTable(result) {
};
function fillList() {
fetch(`/api/reminders?api_key=${api_key}`)
fetch(`${url_prefix}/api/reminders?api_key=${api_key}`)
.then(response => {
// catch errors
if (!response.ok) {
@@ -55,7 +55,7 @@ function fillList() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};
@@ -64,7 +64,7 @@ function fillList() {
function search() {
const query = document.getElementById('search-input').value;
fetch(`/api/reminders/search?api_key=${api_key}&query=${query}`)
fetch(`${url_prefix}/api/reminders/search?api_key=${api_key}&query=${query}`)
.then(response => {
// catch errors
if (!response.ok) {
@@ -77,7 +77,7 @@ function search() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};

View File

@@ -2,7 +2,7 @@ function changePassword() {
const data = {
'new_password': document.getElementById('password-input').value
};
fetch(`/api/user?api_key=${api_key}`, {
fetch(`${url_prefix}/api/user?api_key=${api_key}`, {
'method': 'PUT',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -16,7 +16,7 @@ function changePassword() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};
@@ -24,11 +24,11 @@ function changePassword() {
};
function deleteAccount() {
fetch(`/api/user?api_key=${api_key}`, {
fetch(`${url_prefix}/api/user?api_key=${api_key}`, {
'method': 'DELETE'
})
.then(response => {
window.location.href = '/';
window.location.href = url_prefix;
});
};

View File

@@ -18,7 +18,7 @@ function loadTemplates(force=true) {
return
};
fetch(`/api/templates?api_key=${api_key}`)
fetch(`${url_prefix}/api/templates?api_key=${api_key}`)
.then(response => {
// catch errors
if (!response.ok) {
@@ -57,7 +57,7 @@ function loadTemplates(force=true) {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};
@@ -74,7 +74,7 @@ function loadTemplate() {
toggleColor(inputs.color);
};
} else {
fetch(`/api/templates/${id}?api_key=${api_key}`)
fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`)
.then(response => {
// catch errors
if (!response.ok) {
@@ -99,7 +99,7 @@ function loadTemplate() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};
@@ -117,7 +117,7 @@ function addTemplate() {
if (!template_inputs.color.classList.contains('hidden')) {
data['color'] = template_inputs.color.querySelector('button[data-selected="true"]').dataset.color;
};
fetch(`/api/templates?api_key=${api_key}`, {
fetch(`${url_prefix}/api/templates?api_key=${api_key}`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -134,7 +134,7 @@ function addTemplate() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};
@@ -154,7 +154,7 @@ function closeAddTemplate() {
};
function showEditTemplate(id) {
fetch(`/api/templates/${id}?api_key=${api_key}`)
fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`)
.then(response => {
// catch errors
if (!response.ok) {
@@ -177,7 +177,7 @@ function showEditTemplate(id) {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};
@@ -195,7 +195,7 @@ function saveTemplate() {
if (!edit_template_inputs.color.classList.contains('hidden')) {
data['color'] = edit_template_inputs.color.querySelector('button[data-selected="true"]').dataset.color;
};
fetch(`/api/templates/${id}?api_key=${api_key}`, {
fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, {
'method': 'PUT',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -210,7 +210,7 @@ function saveTemplate() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};
@@ -219,7 +219,7 @@ function saveTemplate() {
function deleteTemplate() {
const id = document.getElementById('template-edit-form').dataset.id;
fetch(`/api/templates/${id}?api_key=${api_key}`, {
fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, {
'method': 'DELETE'
})
.then(response => {
@@ -233,7 +233,7 @@ function deleteTemplate() {
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
window.location.href = url_prefix;
} else {
console.log(e);
};

View File

@@ -4,10 +4,11 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta id="url_prefix" data-value="{{url_prefix}}">
<link rel="stylesheet" href="/static/css/general.css">
<link rel="stylesheet" href="/static/css/login.css">
<script src="/static/js/login.js" defer></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/general.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
<title>Login - MIND</title>
</head>

View File

@@ -17,7 +17,7 @@
<main>
<h2>MIND</h1>
<p>404 - Page not found :(</p>
<a href="/">Go to home page</a>
<a href="{{url_prefix}}">Go to home page</a>
</main>
</body>
</html>

View File

@@ -4,19 +4,20 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta id="url_prefix" data-value="{{url_prefix}}">
<link rel="stylesheet" href="/static/css/general.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">
<script src="/static/js/general.js" defer></script>
<script src="/static/js/reminders.js" defer></script>
<script src="/static/js/add.js" defer></script>
<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>
<link rel="stylesheet" href="{{ url_for('static', filename='css/general.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/reminders_templates.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/add_edit.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/notification.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<script src="{{ url_for('static', filename='js/general.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/reminders.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/add.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/edit.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/notification.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/settings.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/templates.js') }}" defer></script>
<title>Reminders - MIND</title>
</head>
@@ -173,10 +174,10 @@
<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>
<option value="days" selected>Day(s)</option>
<option value="weeks">Week(s)</option>
<option value="months">Month(s)</option>
<option value="years">Year(s)</option>
</select>
</div>
<textarea id="text-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
@@ -226,10 +227,10 @@
<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>
<option value="days" selected>Day(s)</option>
<option value="weeks">Week(s)</option>
<option value="months">Month(s)</option>
<option value="years">Year(s)</option>
</select>
</div>
<textarea id="text-edit-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>

View File

@@ -1,15 +1,20 @@
#-*- coding: utf-8 -*-
import logging
from flask import Blueprint, render_template
ui = Blueprint('ui', __name__)
methods = ['GET']
@ui.errorhandler(404)
def not_found(e):
return render_template('page_not_found.html', url_prefix=logging.URL_PREFIX)
@ui.route('/', methods=methods)
def ui_login():
return render_template('login.html')
return render_template('login.html', url_prefix=logging.URL_PREFIX)
@ui.route('/reminders', methods=methods)
def ui_reminders():
return render_template('reminders.html')
return render_template('reminders.html', url_prefix=logging.URL_PREFIX)

View File

@@ -15,6 +15,6 @@ class Test_MIND(unittest.TestCase):
self.assertEqual(result.blueprints.get('api'), api)
handlers = result.error_handler_spec[None].keys()
required_handlers = 404, 400, 405, 500
required_handlers = 400, 405, 500
for handler in required_handlers:
self.assertIn(handler, handlers)