1
0
Fork 0
forked from s3lph/matemat

first, horribly broken, undocumented implementation of the matemat webapp using jinja2 templates

This commit is contained in:
s3lph 2018-07-07 15:11:27 +02:00
parent a89f2cd15d
commit f699058cf0
37 changed files with 1217 additions and 119 deletions

View file

@ -5,6 +5,11 @@ 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
ADD . /home/matemat
RUN chown matemat:matemat -R /home/matemat
RUN pip3 install -r /home/matemat/requirements.txt
WORKDIR /home/matemat
USER matemat
CMD python3 -m matemat
EXPOSE 8080/tcp

View file

@ -13,4 +13,4 @@ if __name__ == '__main__':
port = int(sys.argv[1])
# Start the web server
MatematWebserver(port=port).start()
MatematWebserver(port=port, webroot='./static').start()

View file

@ -73,14 +73,28 @@ class MatematDatabase(object):
users: List[User] = []
with self.db.transaction(exclusive=False) as c:
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
'''):
# Decompose each row and put the values into a User object
user_id, username, email, is_admin, is_member = row
users.append(User(user_id, username, email, is_admin, is_member))
user_id, username, email, is_admin, is_member, balance = row
users.append(User(user_id, username, balance, email, is_admin, is_member))
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,
username: str,
password: str,
@ -119,7 +133,7 @@ class MatematDatabase(object):
# Fetch the new user's rowid.
c.execute('SELECT last_insert_rowid()')
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:
"""
@ -136,21 +150,21 @@ class MatematDatabase(object):
raise ValueError('Exactly one of password and touchkey must be provided')
with self.db.transaction(exclusive=False) as c:
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
WHERE username = ?
''', [username])
row = c.fetchone()
if row is None:
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:
raise AuthenticationError('Password mismatch')
elif touchkey is not None and tkhash is not None and crypt.crypt(touchkey, tkhash) != tkhash:
raise AuthenticationError('Touchkey mismatch')
elif touchkey is not None and tkhash is None:
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:
"""
@ -221,14 +235,18 @@ class MatematDatabase(object):
with self.db.transaction() as c:
c.execute('''
UPDATE users SET
username = :username,
email = :email,
balance = :balance,
is_admin = :is_admin,
is_member = :is_member,
lastchange = STRFTIME('%s', 'now')
WHERE user_id = :user_id
''', {
'user_id': user.id,
'username': user.name,
'email': user.email,
'balance': user.balance,
'is_admin': user.is_admin,
'is_member': user.is_member
})
@ -261,13 +279,30 @@ class MatematDatabase(object):
products: List[Product] = []
with self.db.transaction(exclusive=False) as c:
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
'''):
product_id, name, price_member, price_external = row
products.append(Product(product_id, name, price_member, price_external))
product_id, name, price_member, price_external, stock = row
products.append(Product(product_id, name, price_member, price_external, stock))
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:
"""
Creates a new product.
@ -292,7 +327,7 @@ class MatematDatabase(object):
})
c.execute('SELECT last_insert_rowid()')
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:
with self.db.transaction() as c:
@ -301,13 +336,15 @@ class MatematDatabase(object):
SET
name = :name,
price_member = :price_member,
price_non_member = :price_non_member
price_non_member = :price_non_member,
stock = :stock
WHERE product_id = :product_id
''', {
'product_id': product.id,
'name': product.name,
'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]
if affected != 1:

View file

@ -47,7 +47,7 @@ class DatabaseWrapper(object):
SCHEMA = '''
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
username TEXT UNIQUE NOT NULL,
email TEXT DEFAULT NULL,
password TEXT NOT NULL,
touchkey TEXT DEFAULT NULL,

View file

@ -8,11 +8,13 @@ class Product(object):
product_id: int,
name: str,
price_member: int,
price_non_member: int) -> None:
price_non_member: int,
stock: int) -> None:
self._product_id: int = product_id
self._name: str = name
self._price_member: int = price_member
self._price_non_member: int = price_non_member
self._stock: int = stock
def __eq__(self, other: Any) -> bool:
if other is None or not isinstance(other, Product):
@ -49,3 +51,11 @@ class Product(object):
@price_non_member.setter
def price_non_member(self, price: int) -> None:
self._price_non_member = price
@property
def stock(self) -> int:
return self._stock
@stock.setter
def stock(self, stock: int) -> None:
self._stock = stock

View file

@ -7,6 +7,7 @@ class User(object):
def __init__(self,
user_id: int,
username: str,
balance: int,
email: Optional[str] = None,
admin: bool = False,
member: bool = True) -> None:
@ -15,6 +16,7 @@ class User(object):
self._email: Optional[str] = email
self._admin: bool = admin
self._member: bool = member
self._balance: int = balance
def __eq__(self, other: Any) -> bool:
if other is None or not isinstance(other, User):
@ -33,6 +35,10 @@ class User(object):
def name(self) -> str:
return self._username
@name.setter
def name(self, value):
self._username = value
@property
def email(self) -> Optional[str]:
return self._email
@ -56,3 +62,11 @@ class User(object):
@is_member.setter
def is_member(self, member: bool) -> None:
self._member = member
@property
def balance(self) -> int:
return self._balance
@balance.setter
def balance(self, balance: int) -> None:
self._balance = balance

View file

@ -278,7 +278,9 @@ class HttpHandler(BaseHTTPRequestHandler):
except ValueError:
self.send_response(400, 'Bad Request')
self.end_headers()
except BaseException:
except BaseException as e:
print(e)
traceback.print_tb(e.__traceback__)
# Generic error handling
self.send_response(500, 'Internal Server Error')
self.end_headers()

View file

@ -8,3 +8,8 @@ from .main import main_page
from .login import login_page
from .logout import logout
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

View 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')

View 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

View 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

View file

@ -1,6 +1,8 @@
from typing import Any, Dict, Optional, Tuple, Union
from jinja2 import Environment, FileSystemLoader
from matemat.exceptions import AuthenticationError
from matemat.webserver import pagelet, RequestArguments
from matemat.primitives import User
@ -14,42 +16,22 @@ def login_page(method: str,
session_vars: Dict[str, Any],
headers: Dict[str, str])\
-> Tuple[int, Optional[Union[str, bytes]]]:
if 'user' in session_vars:
if 'authenticated_user' in session_vars:
headers['Location'] = '/'
return 301, None
return 301, bytes()
env = Environment(loader=FileSystemLoader('templates'))
if method == 'GET':
data = '''
<DOCTYPE html>
<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 '')
template = env.get_template('login.html')
return 200, template.render()
elif method == 'POST':
with MatematDatabase('test.db') as db:
try:
user: User = db.login(str(args.username), str(args.password))
except AuthenticationError:
headers['Location'] = '/login?msg=Username%20or%20password%20wrong.%20Please%20try%20again.'
headers['Location'] = '/login'
return 301, bytes()
session_vars['user'] = user
session_vars['authenticated_user'] = user.id
session_vars['authentication_level'] = 2
headers['Location'] = '/'
return 301, bytes()
return 301, None
return 405, None

View file

@ -11,7 +11,8 @@ def logout(method: str,
session_vars: Dict[str, Any],
headers: Dict[str, str])\
-> Tuple[int, Optional[Union[str, bytes]]]:
if 'user' in session_vars:
del session_vars['user']
if 'authenticated_user' in session_vars:
del session_vars['authenticated_user']
session_vars['authentication_level'] = 0
headers['Location'] = '/'
return 301, None

View file

@ -1,8 +1,9 @@
from typing import Any, Dict, Optional, Tuple, Union
from jinja2 import Environment, FileSystemLoader
from matemat.webserver import pagelet, RequestArguments
from matemat.primitives import User
from matemat.db import MatematDatabase
@ -13,45 +14,16 @@ def main_page(method: str,
session_vars: Dict[str, Any],
headers: Dict[str, str])\
-> Tuple[int, Optional[Union[str, bytes]]]:
data = '''
<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:
if 'user' in session_vars:
user: User = session_vars['user']
products = db.list_products()
plist = '\n'.join([f'<li/> <b>{p.name}</b> ' +
f'{p.price_member//100 if user.is_member else p.price_non_member//100}' +
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:
users = db.list_users()
ulist = '\n'.join([f'<li/> <b><a href=/touchkey?username={u.name}>{u.name}</a></b>' for u in users])
ulist = ulist + '<li/> <a href=/login>Password login</a>'
data = data.format(user='', list=ulist)
return 200, data
except BaseException as e:
import traceback
traceback.print_tb(e.__traceback__)
return 500, None
env = Environment(loader=FileSystemLoader('templates'))
with MatematDatabase('test.db') as db:
if 'authenticated_user' in session_vars:
uid: int = session_vars['authenticated_user']
authlevel: int = session_vars['authentication_level']
user = db.get_user(uid)
products = db.list_products()
template = env.get_template('productlist.html')
return 200, template.render(user=user, products=products, authlevel=authlevel)
else:
users = db.list_users()
template = env.get_template('userlist.html')
return 200, template.render(users=users)

View 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)

View 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)

View file

@ -1,6 +1,8 @@
from typing import Any, Dict, Optional, Tuple, Union
from jinja2 import Environment, FileSystemLoader
from matemat.exceptions import AuthenticationError
from matemat.webserver import pagelet, RequestArguments
from matemat.primitives import User
@ -14,41 +16,22 @@ def touchkey_page(method: str,
session_vars: Dict[str, Any],
headers: Dict[str, str])\
-> Tuple[int, Optional[Union[str, bytes]]]:
if 'user' in session_vars:
if 'authenticated_user' in session_vars:
headers['Location'] = '/'
return 301, bytes()
env = Environment(loader=FileSystemLoader('templates'))
if method == 'GET':
data = '''
<DOCTYPE html>
<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 '')
template = env.get_template('touchkey.html')
return 200, template.render(username=str(args.username), uid=int(str(args.uid)))
elif method == 'POST':
with MatematDatabase('test.db') as db:
try:
user: User = db.login(str(args.username), touchkey=str(args.touchkey))
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()
session_vars['user'] = user
session_vars['authenticated_user'] = user.id
session_vars['authentication_level'] = 1
headers['Location'] = '/'
return 301, None
return 405, None

View file

@ -46,8 +46,11 @@ def _parse_multipart(body: bytes, boundary: str) -> List[RequestArgument]:
# Add header to hdr dict
hk, hv = head.decode('utf-8').split(':')
hdr[hk.strip()] = hv.strip()
# At least Content-Type and Content-Disposition must be present
if 'Content-Type' not in hdr or 'Content-Disposition' not in hdr:
# No content type set - set broadest possible type
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')
# Extract Content-Disposition header value and its arguments
cd, *cdargs = hdr['Content-Disposition'].split(';')

View file

@ -1 +1,2 @@
apsw
jinja2

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

127
static/js/touchkey.js Normal file
View 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
View 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
View 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>

View 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
View 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> &copy; 2018 s3lph
<li> MIT License
<li> <a href="https://gitlab.com/s3lph/matemat">Source &amp; Documentation</a>
</ul>
{% endblock %}
</footer>
</body>
</html>

23
templates/login.html Normal file
View 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
View 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
View 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 %}

View 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
View 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
View 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
View 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
View 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
View 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>