first, horribly broken, undocumented implementation of the matemat webapp using jinja2 templates
This commit is contained in:
parent
a89f2cd15d
commit
f699058cf0
37 changed files with 1217 additions and 119 deletions
|
@ -5,6 +5,11 @@ RUN useradd -d /home/matemat -m matemat
|
||||||
RUN apt-get update -qy
|
RUN apt-get update -qy
|
||||||
RUN apt-get install -y --no-install-recommends python3-dev python3-pip python3-coverage python3-setuptools build-essential
|
RUN apt-get install -y --no-install-recommends python3-dev python3-pip python3-coverage python3-setuptools build-essential
|
||||||
RUN pip3 install wheel pycodestyle mypy
|
RUN pip3 install wheel pycodestyle mypy
|
||||||
|
ADD . /home/matemat
|
||||||
|
RUN chown matemat:matemat -R /home/matemat
|
||||||
|
RUN pip3 install -r /home/matemat/requirements.txt
|
||||||
|
|
||||||
WORKDIR /home/matemat
|
WORKDIR /home/matemat
|
||||||
USER matemat
|
USER matemat
|
||||||
|
CMD python3 -m matemat
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
|
|
@ -13,4 +13,4 @@ if __name__ == '__main__':
|
||||||
port = int(sys.argv[1])
|
port = int(sys.argv[1])
|
||||||
|
|
||||||
# Start the web server
|
# Start the web server
|
||||||
MatematWebserver(port=port).start()
|
MatematWebserver(port=port, webroot='./static').start()
|
||||||
|
|
|
@ -73,14 +73,28 @@ class MatematDatabase(object):
|
||||||
users: List[User] = []
|
users: List[User] = []
|
||||||
with self.db.transaction(exclusive=False) as c:
|
with self.db.transaction(exclusive=False) as c:
|
||||||
for row in c.execute('''
|
for row in c.execute('''
|
||||||
SELECT user_id, username, email, is_admin, is_member
|
SELECT user_id, username, email, is_admin, is_member, balance
|
||||||
FROM users
|
FROM users
|
||||||
'''):
|
'''):
|
||||||
# Decompose each row and put the values into a User object
|
# Decompose each row and put the values into a User object
|
||||||
user_id, username, email, is_admin, is_member = row
|
user_id, username, email, is_admin, is_member, balance = row
|
||||||
users.append(User(user_id, username, email, is_admin, is_member))
|
users.append(User(user_id, username, balance, email, is_admin, is_member))
|
||||||
return users
|
return users
|
||||||
|
|
||||||
|
def get_user(self, uid: int) -> User:
|
||||||
|
"""
|
||||||
|
Return a user identified by its user ID
|
||||||
|
:param uid: The user's ID
|
||||||
|
"""
|
||||||
|
with self.db.transaction(exclusive=False) as c:
|
||||||
|
c.execute('SELECT user_id, username, email, is_admin, is_member, balance FROM users WHERE user_id = ?',
|
||||||
|
[uid])
|
||||||
|
row = c.fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise ValueError(f'No user with user ID {uid} exists.')
|
||||||
|
user_id, username, email, is_admin, is_member, balance = row
|
||||||
|
return User(user_id, username, balance, email, is_admin, is_member)
|
||||||
|
|
||||||
def create_user(self,
|
def create_user(self,
|
||||||
username: str,
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
|
@ -119,7 +133,7 @@ class MatematDatabase(object):
|
||||||
# Fetch the new user's rowid.
|
# Fetch the new user's rowid.
|
||||||
c.execute('SELECT last_insert_rowid()')
|
c.execute('SELECT last_insert_rowid()')
|
||||||
user_id = int(c.fetchone()[0])
|
user_id = int(c.fetchone()[0])
|
||||||
return User(user_id, username, email, admin, member)
|
return User(user_id, username, 0, email, admin, member)
|
||||||
|
|
||||||
def login(self, username: str, password: Optional[str] = None, touchkey: Optional[str] = None) -> User:
|
def login(self, username: str, password: Optional[str] = None, touchkey: Optional[str] = None) -> User:
|
||||||
"""
|
"""
|
||||||
|
@ -136,21 +150,21 @@ class MatematDatabase(object):
|
||||||
raise ValueError('Exactly one of password and touchkey must be provided')
|
raise ValueError('Exactly one of password and touchkey must be provided')
|
||||||
with self.db.transaction(exclusive=False) as c:
|
with self.db.transaction(exclusive=False) as c:
|
||||||
c.execute('''
|
c.execute('''
|
||||||
SELECT user_id, username, email, password, touchkey, is_admin, is_member
|
SELECT user_id, username, email, password, touchkey, is_admin, is_member, balance
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = ?
|
WHERE username = ?
|
||||||
''', [username])
|
''', [username])
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
if row is None:
|
if row is None:
|
||||||
raise AuthenticationError('User does not exist')
|
raise AuthenticationError('User does not exist')
|
||||||
user_id, username, email, pwhash, tkhash, admin, member = row
|
user_id, username, email, pwhash, tkhash, admin, member, balance = row
|
||||||
if password is not None and crypt.crypt(password, pwhash) != pwhash:
|
if password is not None and crypt.crypt(password, pwhash) != pwhash:
|
||||||
raise AuthenticationError('Password mismatch')
|
raise AuthenticationError('Password mismatch')
|
||||||
elif touchkey is not None and tkhash is not None and crypt.crypt(touchkey, tkhash) != tkhash:
|
elif touchkey is not None and tkhash is not None and crypt.crypt(touchkey, tkhash) != tkhash:
|
||||||
raise AuthenticationError('Touchkey mismatch')
|
raise AuthenticationError('Touchkey mismatch')
|
||||||
elif touchkey is not None and tkhash is None:
|
elif touchkey is not None and tkhash is None:
|
||||||
raise AuthenticationError('Touchkey not set')
|
raise AuthenticationError('Touchkey not set')
|
||||||
return User(user_id, username, email, admin, member)
|
return User(user_id, username, balance, email, admin, member)
|
||||||
|
|
||||||
def change_password(self, user: User, oldpass: str, newpass: str, verify_password: bool = True) -> None:
|
def change_password(self, user: User, oldpass: str, newpass: str, verify_password: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -221,14 +235,18 @@ class MatematDatabase(object):
|
||||||
with self.db.transaction() as c:
|
with self.db.transaction() as c:
|
||||||
c.execute('''
|
c.execute('''
|
||||||
UPDATE users SET
|
UPDATE users SET
|
||||||
|
username = :username,
|
||||||
email = :email,
|
email = :email,
|
||||||
|
balance = :balance,
|
||||||
is_admin = :is_admin,
|
is_admin = :is_admin,
|
||||||
is_member = :is_member,
|
is_member = :is_member,
|
||||||
lastchange = STRFTIME('%s', 'now')
|
lastchange = STRFTIME('%s', 'now')
|
||||||
WHERE user_id = :user_id
|
WHERE user_id = :user_id
|
||||||
''', {
|
''', {
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
|
'username': user.name,
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
|
'balance': user.balance,
|
||||||
'is_admin': user.is_admin,
|
'is_admin': user.is_admin,
|
||||||
'is_member': user.is_member
|
'is_member': user.is_member
|
||||||
})
|
})
|
||||||
|
@ -261,13 +279,30 @@ class MatematDatabase(object):
|
||||||
products: List[Product] = []
|
products: List[Product] = []
|
||||||
with self.db.transaction(exclusive=False) as c:
|
with self.db.transaction(exclusive=False) as c:
|
||||||
for row in c.execute('''
|
for row in c.execute('''
|
||||||
SELECT product_id, name, price_member, price_non_member
|
SELECT product_id, name, price_member, price_non_member, stock
|
||||||
FROM products
|
FROM products
|
||||||
'''):
|
'''):
|
||||||
product_id, name, price_member, price_external = row
|
product_id, name, price_member, price_external, stock = row
|
||||||
products.append(Product(product_id, name, price_member, price_external))
|
products.append(Product(product_id, name, price_member, price_external, stock))
|
||||||
return products
|
return products
|
||||||
|
|
||||||
|
def get_product(self, pid: int) -> Product:
|
||||||
|
"""
|
||||||
|
Return a product identified by its product ID
|
||||||
|
:param pid: The products's ID
|
||||||
|
"""
|
||||||
|
with self.db.transaction(exclusive=False) as c:
|
||||||
|
c.execute('''
|
||||||
|
SELECT product_id, name, price_member, price_non_member, stock
|
||||||
|
FROM products
|
||||||
|
WHERE product_id = ?''',
|
||||||
|
[pid])
|
||||||
|
row = c.fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise ValueError(f'No product with product ID {pid} exists.')
|
||||||
|
product_id, name, price_member, price_non_member, stock = row
|
||||||
|
return Product(product_id, name, price_member, price_non_member, stock)
|
||||||
|
|
||||||
def create_product(self, name: str, price_member: int, price_non_member: int) -> Product:
|
def create_product(self, name: str, price_member: int, price_non_member: int) -> Product:
|
||||||
"""
|
"""
|
||||||
Creates a new product.
|
Creates a new product.
|
||||||
|
@ -292,7 +327,7 @@ class MatematDatabase(object):
|
||||||
})
|
})
|
||||||
c.execute('SELECT last_insert_rowid()')
|
c.execute('SELECT last_insert_rowid()')
|
||||||
product_id = int(c.fetchone()[0])
|
product_id = int(c.fetchone()[0])
|
||||||
return Product(product_id, name, price_member, price_non_member)
|
return Product(product_id, name, price_member, price_non_member, 0)
|
||||||
|
|
||||||
def change_product(self, product: Product) -> None:
|
def change_product(self, product: Product) -> None:
|
||||||
with self.db.transaction() as c:
|
with self.db.transaction() as c:
|
||||||
|
@ -301,13 +336,15 @@ class MatematDatabase(object):
|
||||||
SET
|
SET
|
||||||
name = :name,
|
name = :name,
|
||||||
price_member = :price_member,
|
price_member = :price_member,
|
||||||
price_non_member = :price_non_member
|
price_non_member = :price_non_member,
|
||||||
|
stock = :stock
|
||||||
WHERE product_id = :product_id
|
WHERE product_id = :product_id
|
||||||
''', {
|
''', {
|
||||||
'product_id': product.id,
|
'product_id': product.id,
|
||||||
'name': product.name,
|
'name': product.name,
|
||||||
'price_member': product.price_member,
|
'price_member': product.price_member,
|
||||||
'price_non_member': product.price_non_member
|
'price_non_member': product.price_non_member,
|
||||||
|
'stock': product.stock
|
||||||
})
|
})
|
||||||
affected = c.execute('SELECT changes()').fetchone()[0]
|
affected = c.execute('SELECT changes()').fetchone()[0]
|
||||||
if affected != 1:
|
if affected != 1:
|
||||||
|
|
|
@ -47,7 +47,7 @@ class DatabaseWrapper(object):
|
||||||
SCHEMA = '''
|
SCHEMA = '''
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
user_id INTEGER PRIMARY KEY,
|
user_id INTEGER PRIMARY KEY,
|
||||||
username TEXT NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
email TEXT DEFAULT NULL,
|
email TEXT DEFAULT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
touchkey TEXT DEFAULT NULL,
|
touchkey TEXT DEFAULT NULL,
|
||||||
|
|
|
@ -8,11 +8,13 @@ class Product(object):
|
||||||
product_id: int,
|
product_id: int,
|
||||||
name: str,
|
name: str,
|
||||||
price_member: int,
|
price_member: int,
|
||||||
price_non_member: int) -> None:
|
price_non_member: int,
|
||||||
|
stock: int) -> None:
|
||||||
self._product_id: int = product_id
|
self._product_id: int = product_id
|
||||||
self._name: str = name
|
self._name: str = name
|
||||||
self._price_member: int = price_member
|
self._price_member: int = price_member
|
||||||
self._price_non_member: int = price_non_member
|
self._price_non_member: int = price_non_member
|
||||||
|
self._stock: int = stock
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
if other is None or not isinstance(other, Product):
|
if other is None or not isinstance(other, Product):
|
||||||
|
@ -49,3 +51,11 @@ class Product(object):
|
||||||
@price_non_member.setter
|
@price_non_member.setter
|
||||||
def price_non_member(self, price: int) -> None:
|
def price_non_member(self, price: int) -> None:
|
||||||
self._price_non_member = price
|
self._price_non_member = price
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stock(self) -> int:
|
||||||
|
return self._stock
|
||||||
|
|
||||||
|
@stock.setter
|
||||||
|
def stock(self, stock: int) -> None:
|
||||||
|
self._stock = stock
|
||||||
|
|
|
@ -7,6 +7,7 @@ class User(object):
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
username: str,
|
username: str,
|
||||||
|
balance: int,
|
||||||
email: Optional[str] = None,
|
email: Optional[str] = None,
|
||||||
admin: bool = False,
|
admin: bool = False,
|
||||||
member: bool = True) -> None:
|
member: bool = True) -> None:
|
||||||
|
@ -15,6 +16,7 @@ class User(object):
|
||||||
self._email: Optional[str] = email
|
self._email: Optional[str] = email
|
||||||
self._admin: bool = admin
|
self._admin: bool = admin
|
||||||
self._member: bool = member
|
self._member: bool = member
|
||||||
|
self._balance: int = balance
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
if other is None or not isinstance(other, User):
|
if other is None or not isinstance(other, User):
|
||||||
|
@ -33,6 +35,10 @@ class User(object):
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self._username
|
return self._username
|
||||||
|
|
||||||
|
@name.setter
|
||||||
|
def name(self, value):
|
||||||
|
self._username = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email(self) -> Optional[str]:
|
def email(self) -> Optional[str]:
|
||||||
return self._email
|
return self._email
|
||||||
|
@ -56,3 +62,11 @@ class User(object):
|
||||||
@is_member.setter
|
@is_member.setter
|
||||||
def is_member(self, member: bool) -> None:
|
def is_member(self, member: bool) -> None:
|
||||||
self._member = member
|
self._member = member
|
||||||
|
|
||||||
|
@property
|
||||||
|
def balance(self) -> int:
|
||||||
|
return self._balance
|
||||||
|
|
||||||
|
@balance.setter
|
||||||
|
def balance(self, balance: int) -> None:
|
||||||
|
self._balance = balance
|
||||||
|
|
|
@ -278,7 +278,9 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.send_response(400, 'Bad Request')
|
self.send_response(400, 'Bad Request')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
except BaseException:
|
except BaseException as e:
|
||||||
|
print(e)
|
||||||
|
traceback.print_tb(e.__traceback__)
|
||||||
# Generic error handling
|
# Generic error handling
|
||||||
self.send_response(500, 'Internal Server Error')
|
self.send_response(500, 'Internal Server Error')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
|
@ -8,3 +8,8 @@ from .main import main_page
|
||||||
from .login import login_page
|
from .login import login_page
|
||||||
from .logout import logout
|
from .logout import logout
|
||||||
from .touchkey import touchkey_page
|
from .touchkey import touchkey_page
|
||||||
|
from .buy import buy
|
||||||
|
from .deposit import deposit
|
||||||
|
from .admin import admin
|
||||||
|
from .moduser import moduser
|
||||||
|
from .modproduct import modproduct
|
||||||
|
|
119
matemat/webserver/pagelets/admin.py
Normal file
119
matemat/webserver/pagelets/admin.py
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from matemat.webserver import pagelet, RequestArguments
|
||||||
|
from matemat.db import MatematDatabase
|
||||||
|
from matemat.primitives import User
|
||||||
|
from matemat.exceptions import DatabaseConsistencyError
|
||||||
|
|
||||||
|
|
||||||
|
@pagelet('/admin')
|
||||||
|
def admin(method: str,
|
||||||
|
path: str,
|
||||||
|
args: RequestArguments,
|
||||||
|
session_vars: Dict[str, Any],
|
||||||
|
headers: Dict[str, str]) \
|
||||||
|
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||||
|
env = Environment(loader=FileSystemLoader('templates'))
|
||||||
|
if 'authentication_level' not in session_vars or 'authenticated_user' not in session_vars:
|
||||||
|
headers['Location'] = '/login'
|
||||||
|
return 301, None
|
||||||
|
authlevel: int = session_vars['authentication_level']
|
||||||
|
uid: int = session_vars['authenticated_user']
|
||||||
|
if authlevel < 2:
|
||||||
|
return 403, None
|
||||||
|
|
||||||
|
with MatematDatabase('test.db') as db:
|
||||||
|
user = db.get_user(uid)
|
||||||
|
if method == 'POST' and 'change' in args:
|
||||||
|
handle_change(args, user, db)
|
||||||
|
elif method == 'POST' and 'adminchange' in args and user.is_admin:
|
||||||
|
handle_admin_change(args, db)
|
||||||
|
|
||||||
|
users = db.list_users()
|
||||||
|
products = db.list_products()
|
||||||
|
template = env.get_template('admin.html')
|
||||||
|
return 200, template.render(user=user, authlevel=authlevel, users=users, products=products)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_change(args: RequestArguments, user: User, db: MatematDatabase) -> None:
|
||||||
|
try:
|
||||||
|
change = str(args.change)
|
||||||
|
|
||||||
|
if change == 'account':
|
||||||
|
username = str(args.username)
|
||||||
|
email = str(args.email)
|
||||||
|
if len(email) == 0:
|
||||||
|
email = None
|
||||||
|
oldname = user.name
|
||||||
|
oldmail = user.email
|
||||||
|
try:
|
||||||
|
user.name = username
|
||||||
|
user.email = email
|
||||||
|
db.change_user(user)
|
||||||
|
except DatabaseConsistencyError:
|
||||||
|
user.name = oldname
|
||||||
|
user.email = oldmail
|
||||||
|
|
||||||
|
elif change == 'password':
|
||||||
|
oldpass = str(args.oldpass)
|
||||||
|
newpass = str(args.newpass)
|
||||||
|
newpass2 = str(args.newpass2)
|
||||||
|
if newpass != newpass2:
|
||||||
|
raise ValueError('New passwords don\'t match')
|
||||||
|
db.change_password(user, oldpass, newpass)
|
||||||
|
|
||||||
|
elif change == 'touchkey':
|
||||||
|
oldpass = str(args.oldpass)
|
||||||
|
touchkey = str(args.touchkey)
|
||||||
|
if len(touchkey) == 0:
|
||||||
|
touchkey = None
|
||||||
|
db.change_touchkey(user, oldpass, touchkey)
|
||||||
|
|
||||||
|
elif change == 'avatar':
|
||||||
|
avatar = bytes(args.avatar)
|
||||||
|
os.makedirs('./static/img/thumbnails/users/', exist_ok=True)
|
||||||
|
with open(f'./static/img/thumbnails/users/{user.id}.png', 'wb') as f:
|
||||||
|
f.write(avatar)
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise ValueError('an argument not a string')
|
||||||
|
|
||||||
|
|
||||||
|
def handle_admin_change(args: RequestArguments, db: MatematDatabase):
|
||||||
|
try:
|
||||||
|
change = str(args.adminchange)
|
||||||
|
|
||||||
|
if change == 'newuser':
|
||||||
|
username = str(args.username)
|
||||||
|
email = str(args.email)
|
||||||
|
if len(email) == 0:
|
||||||
|
email = None
|
||||||
|
password = str(args.password)
|
||||||
|
is_member = 'ismember' in args
|
||||||
|
is_admin = 'isadmin' in args
|
||||||
|
db.create_user(username, password, email, member=is_member, admin=is_admin)
|
||||||
|
|
||||||
|
elif change == 'newproduct':
|
||||||
|
name = str(args.name)
|
||||||
|
price_member = int(str(args.pricemember))
|
||||||
|
price_non_member = int(str(args.pricenonmember))
|
||||||
|
newproduct = db.create_product(name, price_member, price_non_member)
|
||||||
|
if 'image' in args:
|
||||||
|
image = bytes(args.image)
|
||||||
|
os.makedirs('./static/img/thumbnails/products/', exist_ok=True)
|
||||||
|
with open(f'./static/img/thumbnails/products/{newproduct.id}.png', 'wb') as f:
|
||||||
|
f.write(image)
|
||||||
|
|
||||||
|
elif change == 'restock':
|
||||||
|
productid = int(str(args.productid))
|
||||||
|
amount = int(str(args.amount))
|
||||||
|
product = db.get_product(productid)
|
||||||
|
db.restock(product, amount)
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise ValueError('an argument not a string')
|
30
matemat/webserver/pagelets/buy.py
Normal file
30
matemat/webserver/pagelets/buy.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from typing import Any, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from matemat.webserver import pagelet, RequestArguments
|
||||||
|
from matemat.primitives import User
|
||||||
|
from matemat.db import MatematDatabase
|
||||||
|
|
||||||
|
|
||||||
|
@pagelet('/buy')
|
||||||
|
def buy(method: str,
|
||||||
|
path: str,
|
||||||
|
args: RequestArguments,
|
||||||
|
session_vars: Dict[str, Any],
|
||||||
|
headers: Dict[str, str]) \
|
||||||
|
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||||
|
if 'authenticated_user' not in session_vars:
|
||||||
|
headers['Location'] = '/'
|
||||||
|
return 301, None
|
||||||
|
with MatematDatabase('test.db') as db:
|
||||||
|
uid: int = session_vars['authenticated_user']
|
||||||
|
user = db.get_user(uid)
|
||||||
|
if 'n' in args:
|
||||||
|
n = int(str(args.n))
|
||||||
|
else:
|
||||||
|
n = 1
|
||||||
|
if 'pid' in args:
|
||||||
|
pid = int(str(args.pid))
|
||||||
|
product = db.get_product(pid)
|
||||||
|
db.increment_consumption(user, product, n)
|
||||||
|
headers['Location'] = '/'
|
||||||
|
return 301, None
|
25
matemat/webserver/pagelets/deposit.py
Normal file
25
matemat/webserver/pagelets/deposit.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from typing import Any, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from matemat.webserver import pagelet, RequestArguments
|
||||||
|
from matemat.primitives import User
|
||||||
|
from matemat.db import MatematDatabase
|
||||||
|
|
||||||
|
|
||||||
|
@pagelet('/deposit')
|
||||||
|
def deposit(method: str,
|
||||||
|
path: str,
|
||||||
|
args: RequestArguments,
|
||||||
|
session_vars: Dict[str, Any],
|
||||||
|
headers: Dict[str, str]) \
|
||||||
|
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||||
|
if 'authenticated_user' not in session_vars:
|
||||||
|
headers['Location'] = '/'
|
||||||
|
return 301, None
|
||||||
|
with MatematDatabase('test.db') as db:
|
||||||
|
uid: int = session_vars['authenticated_user']
|
||||||
|
user = db.get_user(uid)
|
||||||
|
if 'n' in args:
|
||||||
|
n = int(str(args.n))
|
||||||
|
db.deposit(user, n)
|
||||||
|
headers['Location'] = '/'
|
||||||
|
return 301, None
|
|
@ -1,6 +1,8 @@
|
||||||
|
|
||||||
from typing import Any, Dict, Optional, Tuple, Union
|
from typing import Any, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from matemat.exceptions import AuthenticationError
|
from matemat.exceptions import AuthenticationError
|
||||||
from matemat.webserver import pagelet, RequestArguments
|
from matemat.webserver import pagelet, RequestArguments
|
||||||
from matemat.primitives import User
|
from matemat.primitives import User
|
||||||
|
@ -14,42 +16,22 @@ def login_page(method: str,
|
||||||
session_vars: Dict[str, Any],
|
session_vars: Dict[str, Any],
|
||||||
headers: Dict[str, str])\
|
headers: Dict[str, str])\
|
||||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||||
if 'user' in session_vars:
|
if 'authenticated_user' in session_vars:
|
||||||
headers['Location'] = '/'
|
headers['Location'] = '/'
|
||||||
return 301, None
|
return 301, bytes()
|
||||||
|
env = Environment(loader=FileSystemLoader('templates'))
|
||||||
if method == 'GET':
|
if method == 'GET':
|
||||||
data = '''
|
template = env.get_template('login.html')
|
||||||
<DOCTYPE html>
|
return 200, template.render()
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Matemat</title>
|
|
||||||
<style>
|
|
||||||
body {{
|
|
||||||
color: #f0f0f0;
|
|
||||||
background: #000000;
|
|
||||||
}};
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Matemat</h1>
|
|
||||||
{msg}
|
|
||||||
<form action="/login" method="post">
|
|
||||||
Username: <input type="text" name="username"/><br/>
|
|
||||||
Password: <input type="password" name="password" /><br/>
|
|
||||||
<input type="submit" value="Login"/>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
'''
|
|
||||||
return 200, data.format(msg=str(args.msg) if 'msg' in args else '')
|
|
||||||
elif method == 'POST':
|
elif method == 'POST':
|
||||||
with MatematDatabase('test.db') as db:
|
with MatematDatabase('test.db') as db:
|
||||||
try:
|
try:
|
||||||
user: User = db.login(str(args.username), str(args.password))
|
user: User = db.login(str(args.username), str(args.password))
|
||||||
except AuthenticationError:
|
except AuthenticationError:
|
||||||
headers['Location'] = '/login?msg=Username%20or%20password%20wrong.%20Please%20try%20again.'
|
headers['Location'] = '/login'
|
||||||
return 301, bytes()
|
return 301, bytes()
|
||||||
session_vars['user'] = user
|
session_vars['authenticated_user'] = user.id
|
||||||
|
session_vars['authentication_level'] = 2
|
||||||
headers['Location'] = '/'
|
headers['Location'] = '/'
|
||||||
return 301, bytes()
|
return 301, None
|
||||||
return 405, None
|
return 405, None
|
||||||
|
|
|
@ -11,7 +11,8 @@ def logout(method: str,
|
||||||
session_vars: Dict[str, Any],
|
session_vars: Dict[str, Any],
|
||||||
headers: Dict[str, str])\
|
headers: Dict[str, str])\
|
||||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||||
if 'user' in session_vars:
|
if 'authenticated_user' in session_vars:
|
||||||
del session_vars['user']
|
del session_vars['authenticated_user']
|
||||||
|
session_vars['authentication_level'] = 0
|
||||||
headers['Location'] = '/'
|
headers['Location'] = '/'
|
||||||
return 301, None
|
return 301, None
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
|
||||||
from typing import Any, Dict, Optional, Tuple, Union
|
from typing import Any, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from matemat.webserver import pagelet, RequestArguments
|
from matemat.webserver import pagelet, RequestArguments
|
||||||
from matemat.primitives import User
|
|
||||||
from matemat.db import MatematDatabase
|
from matemat.db import MatematDatabase
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,45 +14,16 @@ def main_page(method: str,
|
||||||
session_vars: Dict[str, Any],
|
session_vars: Dict[str, Any],
|
||||||
headers: Dict[str, str])\
|
headers: Dict[str, str])\
|
||||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||||
data = '''
|
env = Environment(loader=FileSystemLoader('templates'))
|
||||||
<DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Matemat</title>
|
|
||||||
<style>
|
|
||||||
body {{
|
|
||||||
color: #f0f0f0;
|
|
||||||
background: #000000;
|
|
||||||
}};
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Matemat</h1>
|
|
||||||
{user}
|
|
||||||
<ul>
|
|
||||||
{list}
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
with MatematDatabase('test.db') as db:
|
with MatematDatabase('test.db') as db:
|
||||||
if 'user' in session_vars:
|
if 'authenticated_user' in session_vars:
|
||||||
user: User = session_vars['user']
|
uid: int = session_vars['authenticated_user']
|
||||||
|
authlevel: int = session_vars['authentication_level']
|
||||||
|
user = db.get_user(uid)
|
||||||
products = db.list_products()
|
products = db.list_products()
|
||||||
plist = '\n'.join([f'<li/> <b>{p.name}</b> ' +
|
template = env.get_template('productlist.html')
|
||||||
f'{p.price_member//100 if user.is_member else p.price_non_member//100}' +
|
return 200, template.render(user=user, products=products, authlevel=authlevel)
|
||||||
f'.{p.price_member%100 if user.is_member else p.price_non_member%100}'
|
|
||||||
for p in products])
|
|
||||||
uname = f'<b>{user.name}</b> <a href="/logout">(Logout)</a>'
|
|
||||||
data = data.format(user=uname, list=plist)
|
|
||||||
else:
|
else:
|
||||||
users = db.list_users()
|
users = db.list_users()
|
||||||
ulist = '\n'.join([f'<li/> <b><a href=/touchkey?username={u.name}>{u.name}</a></b>' for u in users])
|
template = env.get_template('userlist.html')
|
||||||
ulist = ulist + '<li/> <a href=/login>Password login</a>'
|
return 200, template.render(users=users)
|
||||||
data = data.format(user='', list=ulist)
|
|
||||||
return 200, data
|
|
||||||
except BaseException as e:
|
|
||||||
import traceback
|
|
||||||
traceback.print_tb(e.__traceback__)
|
|
||||||
return 500, None
|
|
||||||
|
|
84
matemat/webserver/pagelets/modproduct.py
Normal file
84
matemat/webserver/pagelets/modproduct.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
from typing import Any, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from matemat.webserver import pagelet, RequestArguments
|
||||||
|
from matemat.db import MatematDatabase
|
||||||
|
from matemat.primitives import Product
|
||||||
|
from matemat.exceptions import DatabaseConsistencyError
|
||||||
|
|
||||||
|
|
||||||
|
@pagelet('/modproduct')
|
||||||
|
def modproduct(method: str,
|
||||||
|
path: str,
|
||||||
|
args: RequestArguments,
|
||||||
|
session_vars: Dict[str, Any],
|
||||||
|
headers: Dict[str, str]) \
|
||||||
|
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||||
|
env = Environment(loader=FileSystemLoader('templates'))
|
||||||
|
if 'authentication_level' not in session_vars or 'authenticated_user' not in session_vars:
|
||||||
|
headers['Location'] = '/login'
|
||||||
|
return 301, None
|
||||||
|
authlevel: int = session_vars['authentication_level']
|
||||||
|
auth_uid: int = session_vars['authenticated_user']
|
||||||
|
if authlevel < 2:
|
||||||
|
return 403, None
|
||||||
|
|
||||||
|
with MatematDatabase('test.db') as db:
|
||||||
|
authuser = db.get_user(auth_uid)
|
||||||
|
if not authuser.is_admin:
|
||||||
|
return 403, None
|
||||||
|
if 'productid' not in args:
|
||||||
|
return 400, None
|
||||||
|
|
||||||
|
modproduct_id = int(str(args.productid))
|
||||||
|
product = db.get_product(modproduct_id)
|
||||||
|
|
||||||
|
if 'change' in args:
|
||||||
|
handle_change(args, product, db)
|
||||||
|
if str(args.change) == 'del':
|
||||||
|
headers['Location'] = '/admin'
|
||||||
|
return 301, None
|
||||||
|
|
||||||
|
template = env.get_template('modproduct.html')
|
||||||
|
return 200, template.render(product=product, authlevel=authlevel)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_change(args: RequestArguments, product: Product, db: MatematDatabase) -> None:
|
||||||
|
change = str(args.change)
|
||||||
|
|
||||||
|
if change == 'del':
|
||||||
|
db.delete_product(product)
|
||||||
|
try:
|
||||||
|
os.remove(f'./static/img/thumbnails/products/{product.id}.png')
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif change == 'update':
|
||||||
|
name = str(args.name)
|
||||||
|
price_member = int(str(args.pricemember))
|
||||||
|
price_non_member = int(str(args.pricenonmember))
|
||||||
|
stock = int(str(args.stock))
|
||||||
|
oldname = product.name
|
||||||
|
oldprice_member = product.price_member
|
||||||
|
oldprice_non_member = product.price_non_member
|
||||||
|
oldstock = product.stock
|
||||||
|
product.name = name
|
||||||
|
product.price_member = price_member
|
||||||
|
product.price_non_member = price_non_member
|
||||||
|
product.stock = stock
|
||||||
|
try:
|
||||||
|
db.change_product(product)
|
||||||
|
except DatabaseConsistencyError:
|
||||||
|
product.name = oldname
|
||||||
|
product.email = oldprice_member
|
||||||
|
product.is_member = oldprice_non_member
|
||||||
|
product.stock = oldstock
|
||||||
|
if 'image' in args:
|
||||||
|
image = bytes(args.image)
|
||||||
|
if len(image) > 0:
|
||||||
|
os.makedirs('./static/img/thumbnails/products/', exist_ok=True)
|
||||||
|
with open(f'./static/img/thumbnails/products/{product.id}.png', 'wb') as f:
|
||||||
|
f.write(image)
|
92
matemat/webserver/pagelets/moduser.py
Normal file
92
matemat/webserver/pagelets/moduser.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
from typing import Any, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from matemat.webserver import pagelet, RequestArguments
|
||||||
|
from matemat.db import MatematDatabase
|
||||||
|
from matemat.primitives import User
|
||||||
|
from matemat.exceptions import DatabaseConsistencyError
|
||||||
|
|
||||||
|
|
||||||
|
@pagelet('/moduser')
|
||||||
|
def moduser(method: str,
|
||||||
|
path: str,
|
||||||
|
args: RequestArguments,
|
||||||
|
session_vars: Dict[str, Any],
|
||||||
|
headers: Dict[str, str]) \
|
||||||
|
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||||
|
env = Environment(loader=FileSystemLoader('templates'))
|
||||||
|
if 'authentication_level' not in session_vars or 'authenticated_user' not in session_vars:
|
||||||
|
headers['Location'] = '/login'
|
||||||
|
return 301, None
|
||||||
|
authlevel: int = session_vars['authentication_level']
|
||||||
|
auth_uid: int = session_vars['authenticated_user']
|
||||||
|
if authlevel < 2:
|
||||||
|
return 403, None
|
||||||
|
|
||||||
|
with MatematDatabase('test.db') as db:
|
||||||
|
authuser = db.get_user(auth_uid)
|
||||||
|
if not authuser.is_admin:
|
||||||
|
return 403, None
|
||||||
|
if 'userid' not in args:
|
||||||
|
return 400, None
|
||||||
|
|
||||||
|
moduser_id = int(str(args.userid))
|
||||||
|
user = db.get_user(moduser_id)
|
||||||
|
|
||||||
|
if 'change' in args:
|
||||||
|
handle_change(args, user, db)
|
||||||
|
if str(args.change) == 'del':
|
||||||
|
headers['Location'] = '/admin'
|
||||||
|
return 301, None
|
||||||
|
|
||||||
|
template = env.get_template('moduser.html')
|
||||||
|
return 200, template.render(user=user, authlevel=authlevel)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_change(args: RequestArguments, user: User, db: MatematDatabase) -> None:
|
||||||
|
change = str(args.change)
|
||||||
|
|
||||||
|
if change == 'del':
|
||||||
|
db.delete_user(user)
|
||||||
|
try:
|
||||||
|
os.remove(f'./static/img/thumbnails/users/{user.id}.png')
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif change == 'update':
|
||||||
|
username = str(args.username)
|
||||||
|
email = str(args.email)
|
||||||
|
password = str(args.password)
|
||||||
|
balance = int(str(args.balance))
|
||||||
|
if len(email) == 0:
|
||||||
|
email = None
|
||||||
|
oldname = user.name
|
||||||
|
oldmail = user.email
|
||||||
|
oldmember = user.is_member
|
||||||
|
oldadmin = user.is_admin
|
||||||
|
oldbalance = user.balance
|
||||||
|
user.name = username
|
||||||
|
user.email = email
|
||||||
|
user.is_member = 'ismember' in args
|
||||||
|
user.is_admin = 'isadmin' in args
|
||||||
|
user.balance = balance
|
||||||
|
try:
|
||||||
|
db.change_user(user)
|
||||||
|
except DatabaseConsistencyError:
|
||||||
|
user.name = oldname
|
||||||
|
user.email = oldmail
|
||||||
|
user.is_member = oldmember
|
||||||
|
user.is_admin = oldadmin
|
||||||
|
user.balance = oldbalance
|
||||||
|
if len(password) > 0:
|
||||||
|
db.change_password(user, '', password, verify_password=False)
|
||||||
|
if 'avatar' in args:
|
||||||
|
avatar = bytes(args.avatar)
|
||||||
|
if len(avatar) > 0:
|
||||||
|
os.makedirs('./static/img/thumbnails/products/', exist_ok=True)
|
||||||
|
with open(f'./static/img/thumbnails/products/{user.id}.png', 'wb') as f:
|
||||||
|
f.write(avatar)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
|
||||||
from typing import Any, Dict, Optional, Tuple, Union
|
from typing import Any, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from matemat.exceptions import AuthenticationError
|
from matemat.exceptions import AuthenticationError
|
||||||
from matemat.webserver import pagelet, RequestArguments
|
from matemat.webserver import pagelet, RequestArguments
|
||||||
from matemat.primitives import User
|
from matemat.primitives import User
|
||||||
|
@ -14,41 +16,22 @@ def touchkey_page(method: str,
|
||||||
session_vars: Dict[str, Any],
|
session_vars: Dict[str, Any],
|
||||||
headers: Dict[str, str])\
|
headers: Dict[str, str])\
|
||||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||||
if 'user' in session_vars:
|
if 'authenticated_user' in session_vars:
|
||||||
headers['Location'] = '/'
|
headers['Location'] = '/'
|
||||||
return 301, bytes()
|
return 301, bytes()
|
||||||
|
env = Environment(loader=FileSystemLoader('templates'))
|
||||||
if method == 'GET':
|
if method == 'GET':
|
||||||
data = '''
|
template = env.get_template('touchkey.html')
|
||||||
<DOCTYPE html>
|
return 200, template.render(username=str(args.username), uid=int(str(args.uid)))
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Matemat</title>
|
|
||||||
<style>
|
|
||||||
body {{
|
|
||||||
color: #f0f0f0;
|
|
||||||
background: #000000;
|
|
||||||
}};
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Matemat</h1>
|
|
||||||
<form action="/touchkey" method="post">
|
|
||||||
<input type="hidden" name="username" value="{username}"/><br/>
|
|
||||||
Touchkey: <input type="password" name="touchkey" /><br/>
|
|
||||||
<input type="submit" value="Login"/>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
'''
|
|
||||||
return 200, data.format(username=str(args.username) if 'username' in args else '')
|
|
||||||
elif method == 'POST':
|
elif method == 'POST':
|
||||||
with MatematDatabase('test.db') as db:
|
with MatematDatabase('test.db') as db:
|
||||||
try:
|
try:
|
||||||
user: User = db.login(str(args.username), touchkey=str(args.touchkey))
|
user: User = db.login(str(args.username), touchkey=str(args.touchkey))
|
||||||
except AuthenticationError:
|
except AuthenticationError:
|
||||||
headers['Location'] = f'/touchkey?username={args["username"]}&msg=Please%20try%20again.'
|
headers['Location'] = f'/touchkey?uid={str(args.uid)}&username={str(args.username)}&fail=1'
|
||||||
return 301, bytes()
|
return 301, bytes()
|
||||||
session_vars['user'] = user
|
session_vars['authenticated_user'] = user.id
|
||||||
|
session_vars['authentication_level'] = 1
|
||||||
headers['Location'] = '/'
|
headers['Location'] = '/'
|
||||||
return 301, None
|
return 301, None
|
||||||
return 405, None
|
return 405, None
|
||||||
|
|
|
@ -46,8 +46,11 @@ def _parse_multipart(body: bytes, boundary: str) -> List[RequestArgument]:
|
||||||
# Add header to hdr dict
|
# Add header to hdr dict
|
||||||
hk, hv = head.decode('utf-8').split(':')
|
hk, hv = head.decode('utf-8').split(':')
|
||||||
hdr[hk.strip()] = hv.strip()
|
hdr[hk.strip()] = hv.strip()
|
||||||
# At least Content-Type and Content-Disposition must be present
|
# No content type set - set broadest possible type
|
||||||
if 'Content-Type' not in hdr or 'Content-Disposition' not in hdr:
|
if 'Content-Type' not in hdr:
|
||||||
|
hdr['Content-Type'] = 'application/octet-stream'
|
||||||
|
# At least Content-Disposition must be present
|
||||||
|
if 'Content-Disposition' not in hdr:
|
||||||
raise ValueError('Missing Content-Type or Content-Disposition header')
|
raise ValueError('Missing Content-Type or Content-Disposition header')
|
||||||
# Extract Content-Disposition header value and its arguments
|
# Extract Content-Disposition header value and its arguments
|
||||||
cd, *cdargs = hdr['Content-Disposition'].split(';')
|
cd, *cdargs = hdr['Content-Disposition'].split(';')
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
apsw
|
apsw
|
||||||
|
jinja2
|
||||||
|
|
BIN
static/img/thumbnails/products/1.png
Normal file
BIN
static/img/thumbnails/products/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 118 KiB |
BIN
static/img/thumbnails/products/2.png
Normal file
BIN
static/img/thumbnails/products/2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 477 KiB |
BIN
static/img/thumbnails/users/1.png
Normal file
BIN
static/img/thumbnails/users/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
static/img/thumbnails/users/2.png
Normal file
BIN
static/img/thumbnails/users/2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
127
static/js/touchkey.js
Normal file
127
static/js/touchkey.js
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
|
||||||
|
HTMLCollection.prototype.forEach = Array.prototype.forEach;
|
||||||
|
HTMLCollection.prototype.slice = Array.prototype.slice;
|
||||||
|
|
||||||
|
initTouchkey = (keepPattern, svgid, formid, formfieldid) => {
|
||||||
|
|
||||||
|
let svg = document.getElementById(svgid);
|
||||||
|
let form;
|
||||||
|
if (formid !== null) {
|
||||||
|
form = document.getElementById(formid);
|
||||||
|
}
|
||||||
|
let formfield = document.getElementById(formfieldid);
|
||||||
|
|
||||||
|
let currentStroke = null;
|
||||||
|
let strokeId = 0;
|
||||||
|
let doneMap = {};
|
||||||
|
let enteredKey = '';
|
||||||
|
|
||||||
|
let drawLine = (fromX, fromY, toX, toY) => {
|
||||||
|
let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||||
|
let id = 'l-' + (strokeId++);
|
||||||
|
let idAttr = document.createAttribute('id');
|
||||||
|
let classAttr = document.createAttribute('class');
|
||||||
|
let x1attr = document.createAttribute('x1');
|
||||||
|
let y1attr = document.createAttribute('y1');
|
||||||
|
let x2attr = document.createAttribute('x2');
|
||||||
|
let y2attr = document.createAttribute('y2');
|
||||||
|
let styleAttr = document.createAttribute('style');
|
||||||
|
idAttr.value = id;
|
||||||
|
classAttr.value = 'l';
|
||||||
|
x1attr.value = fromX;
|
||||||
|
y1attr.value = fromY;
|
||||||
|
x2attr.value = toX;
|
||||||
|
y2attr.value = toY;
|
||||||
|
styleAttr.value = 'stroke: grey; stroke-width: 5%; stroke-linecap: round';
|
||||||
|
line.setAttributeNode(idAttr);
|
||||||
|
line.setAttributeNode(classAttr);
|
||||||
|
line.setAttributeNode(x1attr);
|
||||||
|
line.setAttributeNode(y1attr);
|
||||||
|
line.setAttributeNode(x2attr);
|
||||||
|
line.setAttributeNode(y2attr);
|
||||||
|
line.setAttributeNode(styleAttr);
|
||||||
|
svg.appendChild(line);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
let endPath = () => {
|
||||||
|
svg.removeChild(svg.getElementById(currentStroke));
|
||||||
|
currentStroke = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let clearTouchkey = () => {
|
||||||
|
doneMap = {};
|
||||||
|
enteredKey = '';
|
||||||
|
svg.getElementsByClassName('l').slice().reverse().forEach((line) => {
|
||||||
|
svg.removeChild(line);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
svg.onmousedown = (ev) => {
|
||||||
|
clearTouchkey();
|
||||||
|
let svgrect = svg.getBoundingClientRect();
|
||||||
|
let minId = '';
|
||||||
|
let minDist = Infinity;
|
||||||
|
let minx = 0;
|
||||||
|
let miny = 0;
|
||||||
|
doneMap = {};
|
||||||
|
document.getElementsByClassName('c').forEach((circle) => {
|
||||||
|
let x = parseFloat(circle.getAttribute('cx')) / 100.0 * svgrect.width;
|
||||||
|
let y = parseFloat(circle.getAttribute('cy')) / 100.0 * svgrect.height;
|
||||||
|
let dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
minId = circle.id;
|
||||||
|
minx = x;
|
||||||
|
miny = y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||||
|
doneMap[minId] = 1;
|
||||||
|
enteredKey += minId;
|
||||||
|
};
|
||||||
|
|
||||||
|
svg.onmouseup = (ev) => {
|
||||||
|
endPath();
|
||||||
|
formfield.value = enteredKey;
|
||||||
|
if (keepPattern !== true) {
|
||||||
|
clearTouchkey();
|
||||||
|
}
|
||||||
|
if (formid !== null) {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
svg.onmousemove = (ev) => {
|
||||||
|
if (currentStroke != null) {
|
||||||
|
let svgrect = svg.getBoundingClientRect();
|
||||||
|
let minId = '';
|
||||||
|
let minDist = Infinity;
|
||||||
|
let minx = 0;
|
||||||
|
let miny = 0;
|
||||||
|
document.getElementsByClassName('c').forEach((circle) => {
|
||||||
|
let x = parseFloat(circle.getAttribute('cx')) / 100.0 * svgrect.width;
|
||||||
|
let y = parseFloat(circle.getAttribute('cy')) / 100.0 * svgrect.height;
|
||||||
|
let dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
minId = circle.id;
|
||||||
|
minx = x;
|
||||||
|
miny = y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (minDist < 2000 && !(minId in doneMap)) {
|
||||||
|
let line = svg.getElementById(currentStroke);
|
||||||
|
line.setAttribute('x2', minx);
|
||||||
|
line.setAttribute('y2', miny);
|
||||||
|
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||||
|
doneMap[minId] = 1;
|
||||||
|
enteredKey += minId;
|
||||||
|
}
|
||||||
|
let line = svg.getElementById(currentStroke);
|
||||||
|
line.setAttribute('x2', ev.offsetX);
|
||||||
|
line.setAttribute('y2', ev.offsetY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
22
templates/admin.html
Normal file
22
templates/admin.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
{% if user.is_admin %}
|
||||||
|
<h1>Administration</h1>
|
||||||
|
{% else %}
|
||||||
|
<h1>Settings</h1>
|
||||||
|
{% endif %}
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
{% include "admin_all.html" %}
|
||||||
|
|
||||||
|
{% if user.is_admin %}
|
||||||
|
{% include "admin_restricted.html" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
{% endblock %}
|
71
templates/admin_all.html
Normal file
71
templates/admin_all.html
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<section id="admin-myaccount">
|
||||||
|
<h2>My Account</h2>
|
||||||
|
|
||||||
|
<form id="admin-myaccount-form" method="post" action="/admin?change=account">
|
||||||
|
<label for="admin-myaccount-username">Username: </label>
|
||||||
|
<input id="admin-myaccount-username" type="text" name="username" value="{{ user.name }}" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-myaccount-email">E-Mail: </label>
|
||||||
|
<input id="admin-myaccount-email" type="text" name="email" value="{% if user.email is not none %}{{ user.email }}{% endif %}" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-myaccount-ismember">Member: </label>
|
||||||
|
<input id="admin-myaccount-ismember" type="checkbox" disabled="disabled" {% if user.is_member %} checked="checked" {% endif %}/><br/>
|
||||||
|
|
||||||
|
<label for="admin-myaccount-isadmin">Admin: </label>
|
||||||
|
<input id="admin-myaccount-isadmin" type="checkbox" disabled="disabled" {% if user.is_admin %} checked="checked" {% endif %}/><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Save changes" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="admin-avatar">
|
||||||
|
<h2>Avatar</h2>
|
||||||
|
|
||||||
|
<form id="admin-avatar-form" method="post" action="/admin?change=avatar" enctype="multipart/form-data">
|
||||||
|
<img src="/img/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-avatar-avatar">Upload new file: </label>
|
||||||
|
<input id="admin-avatar-avatar" type="file" name="avatar" accept="image/png" /><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Save changes" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="admin-password">
|
||||||
|
<h2>Password</h2>
|
||||||
|
|
||||||
|
<form id="admin-password-form" method="post" action="/admin?change=password">
|
||||||
|
<label for="admin-password-oldpass">Current password: </label>
|
||||||
|
<input id="admin-password-oldpass" type="password" name="oldpass" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-password-newpass">New password: </label>
|
||||||
|
<input id="admin-password-newpass" type="password" name="newpass" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-password-newpass2">Repeat password: </label>
|
||||||
|
<input id="admin-password-newpass2" type="password" name="newpass2" /><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Save changes" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="admin-touchkey">
|
||||||
|
<h2>Touchkey</h2>
|
||||||
|
|
||||||
|
<form id="admin-touchkey-form" method="post" action="/admin?change=touchkey">
|
||||||
|
<label for="admin-touchkey-oldpass">Current password: </label>
|
||||||
|
<input id="admin-touchkey-oldpass" type="password" name="oldpass" /><br/>
|
||||||
|
|
||||||
|
Draw a new touchkey (leave empty to disable):
|
||||||
|
<br/>
|
||||||
|
{% include "touchkey.svg" %}
|
||||||
|
<br/>
|
||||||
|
<input id="admin-touchkey-touchkey" type="hidden" name="touchkey" value="" />
|
||||||
|
|
||||||
|
<input type="submit" value="Save changes" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="/js/touchkey.js" ></script>
|
||||||
|
<script>
|
||||||
|
initTouchkey(true, 'touchkey-svg', null, 'admin-touchkey-touchkey');
|
||||||
|
</script>
|
||||||
|
</section>
|
90
templates/admin_restricted.html
Normal file
90
templates/admin_restricted.html
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<section id="admin-restricted-newuser">
|
||||||
|
<h2>Create New User</h2>
|
||||||
|
|
||||||
|
<form id="admin-newuser-form" method="post" action="/admin?adminchange=newuser">
|
||||||
|
<label for="admin-newuser-username">Username: </label>
|
||||||
|
<input id="admin-newuser-username" type="text" name="username" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-newuser-email">E-Mail (optional): </label>
|
||||||
|
<input id="admin-newuser-email" type="text" name="email" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-newuser-password">Password: </label>
|
||||||
|
<input id="admin-newuser-password" type="password" name="password" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-newuser-ismember">Member: </label>
|
||||||
|
<input id="admin-newuser-ismember" type="checkbox" name="ismember" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-newuser-isadmin">Admin: </label>
|
||||||
|
<input id="admin-newuser-isadmin" type="checkbox" name="isadmin" /><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Create User" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="admin-restricted-moduser">
|
||||||
|
<h2>Modify User</h2>
|
||||||
|
|
||||||
|
<form id="admin-moduser-form" method="get" action="/moduser">
|
||||||
|
<label for="admin-moduser-userid">Username: </label>
|
||||||
|
<select id="admin-moduser-userid" name="userid">
|
||||||
|
{% for user in users %}
|
||||||
|
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Go" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="admin-restricted-newproduct">
|
||||||
|
<h2>Create New Product</h2>
|
||||||
|
|
||||||
|
<form id="admin-newproduct-form" method="post" action="/admin?adminchange=newproduct" enctype="multipart/form-data">
|
||||||
|
<label for="admin-newproduct-name">Name: </label>
|
||||||
|
<input id="admin-newproduct-name" type="text" name="name" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-newproduct-price-member">Member price: </label>
|
||||||
|
<input id="admin-newproduct-price-member" type="number" min="0" name="pricemember" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-newproduct-price-non-member">Non-member price: </label>
|
||||||
|
<input id="admin-newproduct-price-non-member" type="number" min="0" name="pricenonmember" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-newproduct-image">Image: </label>
|
||||||
|
<input id="admin-newproduct-image" type="file" accept="image/png" /><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Create Product" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="admin-restricted-restock">
|
||||||
|
<h2>Restock Product</h2>
|
||||||
|
|
||||||
|
<form id="admin-restock-form" method="post" action="/admin?adminchange=restock">
|
||||||
|
<label for="admin-restock-productid">Product: </label>
|
||||||
|
<select id="admin-restock-productid" name="productid">
|
||||||
|
{% for product in products %}
|
||||||
|
<option value="{{ product.id }}">{{ product.name }} ({{ product.stock }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select><br/>
|
||||||
|
|
||||||
|
<label for="admin-restock-amount">Amount: </label>
|
||||||
|
<input id="admin-restock-amount" type="number" min="0" name="amount" /><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Restock" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="admin-restricted-modproduct">
|
||||||
|
<h2>Modify Product</h2>
|
||||||
|
|
||||||
|
<form id="admin-modproduct-form" method="get" action="/modproduct">
|
||||||
|
<label for="admin-modproduct-productid">Product: </label>
|
||||||
|
<select id="admin-modproduct-productid" name="productid">
|
||||||
|
{% for product in products %}
|
||||||
|
<option value="{{ product.id }}">{{ product.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Go">
|
||||||
|
</form>
|
||||||
|
</section>
|
36
templates/base.html
Normal file
36
templates/base.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% block head %}
|
||||||
|
<title>Matemat</title>
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
{% block header %}
|
||||||
|
|
||||||
|
<a href="/">Home</a>
|
||||||
|
{% if authlevel|default(0) > 1 %}
|
||||||
|
{% if user.is_admin %}
|
||||||
|
<a href="/admin">Administration</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/admin">Settings</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block main %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
{% block footer %}
|
||||||
|
<ul>
|
||||||
|
<li> Matemat
|
||||||
|
<li> © 2018 s3lph
|
||||||
|
<li> MIT License
|
||||||
|
<li> <a href="https://gitlab.com/s3lph/matemat">Source & Documentation</a>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
23
templates/login.html
Normal file
23
templates/login.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Welcome</h1>
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<form method="post" action="/login" id="loginform">
|
||||||
|
<label for="login-username">Username: </label>
|
||||||
|
<input id="login-username" type="text" name="username" /><br/>
|
||||||
|
|
||||||
|
<label for="login-password">Password: </label>
|
||||||
|
<input id="login-password" type="password" name="password" /><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Login">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
{% endblock %}
|
45
templates/modproduct.html
Normal file
45
templates/modproduct.html
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Administration</h1>
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<section id="modproduct">
|
||||||
|
<h2>Modify {{ product.name }}</h2>
|
||||||
|
|
||||||
|
<form id="modproduct-form" method="post" action="/modproduct?change=update" enctype="multipart/form-data">
|
||||||
|
<label for="modproduct-name">Name: </label>
|
||||||
|
<input id="modproduct-name" type="text" name="name" value="{{ product.name }}" /><br/>
|
||||||
|
|
||||||
|
<label for="modproduct-price-member">Member price: </label>
|
||||||
|
<input id="modproduct-price-member" type="text" name="pricemember" value="{{ product.price_member }}" /><br/>
|
||||||
|
|
||||||
|
<label for="modproduct-price-non-member">Non-member price: </label>
|
||||||
|
<input id="modproduct-price-non-member" type="text" name="pricenonmember" value="{{ product.price_non_member }}" /><br/>
|
||||||
|
|
||||||
|
<label for="modproduct-balance">Stock: </label>
|
||||||
|
<input id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/>
|
||||||
|
|
||||||
|
<label for="modproduct-image">
|
||||||
|
<img height="150" src="/img/thumbnails/products/{{ product.id }}.png" alt="Image of {{ product.name }}" />
|
||||||
|
</label><br/>
|
||||||
|
<input id="modproduct-image" type="file" name="image" accept="image/png" /><br/>
|
||||||
|
|
||||||
|
<input id="modproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Save changes">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form id="modproduct-delproduct-form" method="post" action="/modproduct?change=del">
|
||||||
|
<input id="modproduct-delproduct-userid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
|
||||||
|
<input type="submit" value="Delete product" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
{% endblock %}
|
51
templates/moduser.html
Normal file
51
templates/moduser.html
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Administration</h1>
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<section id="moduser-account">
|
||||||
|
<h2>Modify {{ user.name }}</h2>
|
||||||
|
|
||||||
|
<form id="moduser-account-form" method="post" action="/moduser?change=update" enctype="multipart/form-data">
|
||||||
|
<label for="moduser-account-username">Username: </label>
|
||||||
|
<input id="moduser-account-username" type="text" name="username" value="{{ user.name }}" /><br/>
|
||||||
|
|
||||||
|
<label for="moduser-account-email">E-Mail: </label>
|
||||||
|
<input id="moduser-account-email" type="text" name="email" value="{% if user.email is not none %}{{ user.email }}{% endif %}" /><br/>
|
||||||
|
|
||||||
|
<label for="moduser-account-password">Password: </label>
|
||||||
|
<input id="moduser-account-password" type="password" name="password" /><br/>
|
||||||
|
|
||||||
|
<label for="moduser-account-ismember">Member: </label>
|
||||||
|
<input id="moduser-account-ismember" name="ismember" type="checkbox" {% if user.is_member %} checked="checked" {% endif %}/><br/>
|
||||||
|
|
||||||
|
<label for="moduser-account-isadmin">Admin: </label>
|
||||||
|
<input id="moduser-account-isadmin" name="isadmin" type="checkbox" {% if user.is_admin %} checked="checked" {% endif %}/><br/>
|
||||||
|
|
||||||
|
<label for="moduser-account-balance">Balance: </label>
|
||||||
|
<input id="moduser-account-balance" name="balance" type="text" value="{{ user.balance }}" /><br/>
|
||||||
|
|
||||||
|
<label for="moduser-account-avatar">
|
||||||
|
<img height="150" src="/img/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" />
|
||||||
|
</label><br/>
|
||||||
|
<input id="moduser-account-avatar" type="file" name="avatar" accept="image/png" /><br/>
|
||||||
|
|
||||||
|
<input id="moduser-account-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Save changes">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form id="moduser-deluser-form" method="post" action="/moduser?change=del">
|
||||||
|
<input id="moduser-deluser-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/>
|
||||||
|
<input type="submit" value="Delete user" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
{% endblock %}
|
36
templates/productlist.html
Normal file
36
templates/productlist.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Welcome, {{ user.name }}</h1>
|
||||||
|
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
Your balance: {{ user.balance }}
|
||||||
|
|
||||||
|
<a href="/deposit?n=100">Deposit CHF 1</a>
|
||||||
|
<a href="/deposit?n=1000">Deposit CHF 10</a>
|
||||||
|
|
||||||
|
{% for product in products %}
|
||||||
|
<div class="thumblist-item">
|
||||||
|
<a href="/buy?pid={{ product.id }}">
|
||||||
|
<span class="thumblist-title">{{ product.name }}</span>
|
||||||
|
<span class="thumblist-detail">Price:
|
||||||
|
{% if user.is_member %}
|
||||||
|
{{ product.price_member }}
|
||||||
|
{% else %}
|
||||||
|
{{ product.price_non_member }}
|
||||||
|
{% endif %}
|
||||||
|
; Stock: {{ product.stock }}</span><br/>
|
||||||
|
<img height="150" src="/img/thumbnails/products/{{ product.id }}.png" alt="Picture of {{ product.name }}" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<a href="/logout">Logout</a>
|
||||||
|
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
{% endblock %}
|
35
templates/touchkey.html
Normal file
35
templates/touchkey.html
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
width: 600px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Welcome, {{ username }}</h1>
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "touchkey.svg" %}
|
||||||
|
|
||||||
|
<form method="post" action="/touchkey" id="loginform">
|
||||||
|
<input type="hidden" name="uid" value="{{ uid }}" />
|
||||||
|
<input type="hidden" name="username" value="{{ username }}" />
|
||||||
|
<input type="hidden" name="touchkey" value="" id="loginform-touchkey-value" />
|
||||||
|
</form>
|
||||||
|
<a href="/">Cancel</a>
|
||||||
|
|
||||||
|
<script src="/js/touchkey.js"></script>
|
||||||
|
<script>
|
||||||
|
initTouchkey(false, 'touchkey-svg', 'loginform', 'loginform-touchkey-value');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
{% endblock %}
|
21
templates/touchkey.svg
Normal file
21
templates/touchkey.svg
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<svg id="touchkey-svg" width="400" height="400">
|
||||||
|
<circle class="c" id="0" cx="12.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="1" cx="37.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="2" cx="62.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="3" cx="87.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
|
||||||
|
<circle class="c" id="4" cx="12.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="5" cx="37.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="6" cx="62.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="7" cx="87.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
|
||||||
|
<circle class="c" id="8" cx="12.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="9" cx="37.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="a" cx="62.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="b" cx="87.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
|
||||||
|
<circle class="c" id="c" cx="12.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="d" cx="37.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="e" cx="62.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
<circle class="c" id="f" cx="87.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
19
templates/userlist.html
Normal file
19
templates/userlist.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Welcome</h1>
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% for user in users %}
|
||||||
|
<div class="thumblist-item">
|
||||||
|
<a href="/touchkey?uid={{ user.id }}&username={{ user.name }}">
|
||||||
|
<span class="thumblist-title">{{ user.name }}</span><br/>
|
||||||
|
<img height="150" src="/img/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<a href="/login">Password-based login</a>
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
10
testing/Dockerfile
Normal file
10
testing/Dockerfile
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
FROM debian:buster
|
||||||
|
|
||||||
|
RUN useradd -d /home/matemat -m matemat
|
||||||
|
RUN apt-get update -qy
|
||||||
|
RUN apt-get install -y --no-install-recommends python3-dev python3-pip python3-coverage python3-setuptools build-essential
|
||||||
|
RUN pip3 install wheel pycodestyle mypy
|
||||||
|
|
||||||
|
WORKDIR /home/matemat
|
||||||
|
USER matemat
|
147
touchkey.html
Normal file
147
touchkey.html
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
width: 600px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Welcome, {{username}}</h1>
|
||||||
|
|
||||||
|
<svg id="svg" width="400" height="400">
|
||||||
|
<circle class="c" id="0" cx="12.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="1" cx="37.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="2" cx="62.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="3" cx="87.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
|
||||||
|
<circle class="c" id="4" cx="12.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="5" cx="37.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="6" cx="62.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="7" cx="87.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
|
||||||
|
<circle class="c" id="8" cx="12.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="9" cx="37.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="a" cx="62.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="b" cx="87.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
|
||||||
|
<circle class="c" id="c" cx="12.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="d" cx="37.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="e" cx="62.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
<circle class="c" id="f" cx="87.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
HTMLCollection.prototype.forEach = Array.prototype.forEach;
|
||||||
|
HTMLCollection.prototype.slice = Array.prototype.slice;
|
||||||
|
|
||||||
|
mouseDown = false;
|
||||||
|
currentStroke = null;
|
||||||
|
strokeId = 0;
|
||||||
|
doneMap = {};
|
||||||
|
enteredKey = '';
|
||||||
|
|
||||||
|
svg = document.getElementById('svg');
|
||||||
|
|
||||||
|
drawLine = (fromX, fromY, toX, toY) => {
|
||||||
|
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||||
|
var id = 'l-' + (strokeId++);
|
||||||
|
var idAttr = document.createAttribute('id');
|
||||||
|
var classAttr = document.createAttribute('class');
|
||||||
|
var x1attr = document.createAttribute('x1');
|
||||||
|
var y1attr = document.createAttribute('y1');
|
||||||
|
var x2attr = document.createAttribute('x2');
|
||||||
|
var y2attr = document.createAttribute('y2');
|
||||||
|
var styleAttr = document.createAttribute('style');
|
||||||
|
idAttr.value = id;
|
||||||
|
classAttr.value = 'l';
|
||||||
|
x1attr.value = fromX;
|
||||||
|
y1attr.value = fromY;
|
||||||
|
x2attr.value = toX;
|
||||||
|
y2attr.value = toY;
|
||||||
|
styleAttr.value = 'stroke: grey; stroke-width: 5%; stroke-linecap: round';
|
||||||
|
line.setAttributeNode(idAttr);
|
||||||
|
line.setAttributeNode(classAttr);
|
||||||
|
line.setAttributeNode(x1attr);
|
||||||
|
line.setAttributeNode(y1attr);
|
||||||
|
line.setAttributeNode(x2attr);
|
||||||
|
line.setAttributeNode(y2attr);
|
||||||
|
line.setAttributeNode(styleAttr);
|
||||||
|
svg.appendChild(line);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
svg.onmousedown = (ev) => {
|
||||||
|
var svgrect = svg.getBoundingClientRect();
|
||||||
|
var minId = '';
|
||||||
|
var minDist = Infinity;
|
||||||
|
var minx = 0;
|
||||||
|
var miny = 0;
|
||||||
|
doneMap = {};
|
||||||
|
document.getElementsByClassName('c').forEach((circle) => {
|
||||||
|
var x = parseFloat(circle.getAttribute('cx'))/100.0 * svgrect.width;
|
||||||
|
var y = parseFloat(circle.getAttribute('cy'))/100.0 * svgrect.height;
|
||||||
|
var dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
minId = circle.id;
|
||||||
|
minx = x;
|
||||||
|
miny = y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var minNode = svg.getElementById(minId);
|
||||||
|
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||||
|
doneMap[minId] = 1;
|
||||||
|
enteredKey += minId;
|
||||||
|
};
|
||||||
|
|
||||||
|
svg.onmouseup = (ev) => {
|
||||||
|
currentStroke = null;
|
||||||
|
doneMap = {};
|
||||||
|
console.log(enteredKey);
|
||||||
|
enteredKey = '';
|
||||||
|
svg.getElementsByClassName('l').slice().reverse().forEach((line) => {
|
||||||
|
svg.removeChild(line);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
svg.onmousemove = (ev) => {
|
||||||
|
if (currentStroke != null) {
|
||||||
|
var svgrect = svg.getBoundingClientRect();
|
||||||
|
var minId = '';
|
||||||
|
var minDist = Infinity;
|
||||||
|
var minx = 0;
|
||||||
|
var miny = 0;
|
||||||
|
document.getElementsByClassName('c').forEach((circle) => {
|
||||||
|
var x = parseFloat(circle.getAttribute('cx'))/100.0 * svgrect.width;
|
||||||
|
var y = parseFloat(circle.getAttribute('cy'))/100.0 * svgrect.height;
|
||||||
|
var dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
minId = circle.id;
|
||||||
|
minx = x;
|
||||||
|
miny = y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (minDist < 2000 && !(minId in doneMap)) {
|
||||||
|
var line = svg.getElementById(currentStroke);
|
||||||
|
line.setAttribute('x2', minx);
|
||||||
|
line.setAttribute('y2', miny);
|
||||||
|
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||||
|
doneMap[minId] = 1;
|
||||||
|
enteredKey += minId;
|
||||||
|
}
|
||||||
|
var line = svg.getElementById(currentStroke);
|
||||||
|
line.setAttribute('x2', ev.offsetX);
|
||||||
|
line.setAttribute('y2', ev.offsetY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue