A first, semi-sane integration of Jinja2 templates
This commit is contained in:
parent
4d2d2d30c1
commit
e3c65776b5
8 changed files with 127 additions and 98 deletions
|
@ -12,6 +12,8 @@ from http.cookies import SimpleCookie
|
|||
from uuid import uuid4
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import jinja2
|
||||
|
||||
from matemat import __version__ as matemat_version
|
||||
from matemat.exceptions import HttpException
|
||||
from matemat.webserver import RequestArguments
|
||||
|
@ -35,7 +37,8 @@ _PAGELET_PATHS: Dict[str, Callable[[str, # HTTP method (GET, POST, ...)
|
|||
Dict[str, str]], # Response headers
|
||||
Union[ # Return type: either a response body, or a redirect
|
||||
bytes, str, # Response body: will assign HTTP/1.0 200 OK
|
||||
Tuple[int, str] # Redirect: First element must be 301, second the redirect path
|
||||
Tuple[int, str], # Redirect: First element must be 301, second the redirect path
|
||||
Tuple[str, Dict[str, Any]] # Jinja template name and kwargs
|
||||
]]] = dict()
|
||||
|
||||
# Inactivity timeout for client sessions
|
||||
|
@ -65,6 +68,8 @@ def pagelet(path: str):
|
|||
returns: One of the following:
|
||||
- A HTTP Response body as str or bytes
|
||||
- A HTTP redirect: A tuple of 301 (an int) and the path to redirect to (a str)
|
||||
- A Jinja template call: A tuple of the template name (a string) and the template rendering
|
||||
arguments (a kwargs dict)
|
||||
raises: HttpException: If a non-200 HTTP status code should be returned
|
||||
|
||||
:param path: The path to register the function for.
|
||||
|
@ -77,7 +82,8 @@ def pagelet(path: str):
|
|||
Dict[str, str]],
|
||||
Union[
|
||||
bytes, str,
|
||||
Tuple[int, str]
|
||||
Tuple[int, str],
|
||||
Tuple[str, Dict[str, Any]]
|
||||
]]):
|
||||
# Add the function to the dict of pagelets
|
||||
_PAGELET_PATHS[path] = fun
|
||||
|
@ -97,13 +103,18 @@ class MatematHTTPServer(HTTPServer):
|
|||
def __init__(self,
|
||||
server_address: Any,
|
||||
handler: Type[BaseHTTPRequestHandler],
|
||||
webroot: str,
|
||||
staticroot: str,
|
||||
templateroot: str,
|
||||
bind_and_activate: bool = True) -> None:
|
||||
super().__init__(server_address, handler, bind_and_activate)
|
||||
# Resolve webroot directory
|
||||
self.webroot = os.path.abspath(webroot)
|
||||
self.webroot = os.path.abspath(staticroot)
|
||||
# Set up session vars dict
|
||||
self.session_vars: Dict[str, Tuple[datetime, Dict[str, Any]]] = dict()
|
||||
# Set up the Jinja2 environment
|
||||
self.jinja_env: jinja2.Environment = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(os.path.abspath(templateroot))
|
||||
)
|
||||
|
||||
|
||||
class MatematWebserver(object):
|
||||
|
@ -121,13 +132,18 @@ class MatematWebserver(object):
|
|||
server.start()
|
||||
"""
|
||||
|
||||
def __init__(self, listen: str = '::', port: int = 80, webroot: str = './webroot') -> None:
|
||||
def __init__(self,
|
||||
listen: str = '::',
|
||||
port: int = 80,
|
||||
staticroot: str = './static',
|
||||
templateroot: str = './templates') -> None:
|
||||
"""
|
||||
Instantiate a MatematWebserver.
|
||||
|
||||
:param listen: The IPv4 or IPv6 address to listen on
|
||||
:param port: The TCP port to listen on
|
||||
:param webroot: Path to the webroot directory
|
||||
:param listen: The IPv4 or IPv6 address to listen on.
|
||||
:param port: The TCP port to listen on.
|
||||
:param staticroot: Path to the static webroot directory.
|
||||
:param templateroot: Path to the Jinja2 templates root directory.
|
||||
"""
|
||||
if len(listen) == 0:
|
||||
# Empty string should be interpreted as all addresses
|
||||
|
@ -137,7 +153,7 @@ class MatematWebserver(object):
|
|||
# Rewrite IPv4 address to IPv6-mapped form
|
||||
listen = f'::ffff:{listen}'
|
||||
# Create the http server
|
||||
self._httpd = MatematHTTPServer((listen, port), HttpHandler, webroot)
|
||||
self._httpd = MatematHTTPServer((listen, port), HttpHandler, staticroot, templateroot)
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
|
@ -202,8 +218,12 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
if session_id in self.server.session_vars:
|
||||
del self.server.session_vars[session_id]
|
||||
|
||||
@staticmethod
|
||||
def _parse_pagelet_result(pagelet_res: Union[bytes, str, Tuple[int, str]], headers: Dict[str, str]) \
|
||||
def _parse_pagelet_result(self,
|
||||
pagelet_res: Union[bytes, # Response body as bytes
|
||||
str, # Response body as str
|
||||
Tuple[int, str], # Redirect
|
||||
Tuple[str, Dict[str, Any]]], # Jinja template name, kwargs dict
|
||||
headers: Dict[str, str]) \
|
||||
-> Tuple[int, bytes]:
|
||||
"""
|
||||
Process the return value of a pagelet function call.
|
||||
|
@ -218,12 +238,19 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
# The HTTP Response body, defaults to None
|
||||
data: Union[bytes, str] = None
|
||||
if isinstance(pagelet_res, tuple):
|
||||
# If the return type is a tuple, the first element must be 301 (the HTTP Redirect status code)
|
||||
# If the return type is a tuple, it has to be either a redirect, in which case the first element must be
|
||||
# int(301), or it is a template call, in which casse the first element must be the template name and the
|
||||
# second element must be the kwargs dict to the template's render function
|
||||
head, tail = pagelet_res
|
||||
if head == 301:
|
||||
if head == 301 and isinstance(tail, str):
|
||||
# Set the HTTP Response Status Code, and the redirect header
|
||||
hsc = 301
|
||||
headers['Location'] = tail
|
||||
elif isinstance(head, str) and isinstance(tail, dict):
|
||||
# Load the Jinja2 template and render it with the provided arguments
|
||||
template = self.server.jinja_env.get_template(head)
|
||||
tail['matemat_version'] = self.server_version
|
||||
data = template.render(**tail)
|
||||
else:
|
||||
raise TypeError(f'Return value of pagelet not understood: {pagelet_res}')
|
||||
elif isinstance(pagelet_res, str) or isinstance(pagelet_res, bytes):
|
||||
|
@ -271,7 +298,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
# Call the pagelet function
|
||||
pagelet_res = _PAGELET_PATHS[path](method, path, args, self.session_vars, headers)
|
||||
# Parse the pagelet's return value, vielding a HTTP status code and a response body
|
||||
hsc, data = HttpHandler._parse_pagelet_result(pagelet_res, headers)
|
||||
hsc, data = self._parse_pagelet_result(pagelet_res, headers)
|
||||
# Send the HTTP status code
|
||||
self.send_response(hsc)
|
||||
# Format the session cookie timeout string and send the session cookie header
|
||||
|
|
|
@ -13,40 +13,17 @@ def login_page(method: str,
|
|||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Union[bytes, str, Tuple[int, str]]:
|
||||
-> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]:
|
||||
if 'user' in session_vars:
|
||||
return 301, '/'
|
||||
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 data.format(msg=str(args.msg) if 'msg' in args else '')
|
||||
return 'login.html', {}
|
||||
elif method == 'POST':
|
||||
with MatematDatabase('test.db') as db:
|
||||
try:
|
||||
user: User = db.login(str(args.username), str(args.password))
|
||||
except AuthenticationError:
|
||||
return 301, '/login?msg=Username%20or%20password%20wrong.%20Please%20try%20again.'
|
||||
return 301, '/login'
|
||||
session_vars['user'] = user
|
||||
return 301, '/'
|
||||
raise HttpException(405)
|
||||
|
|
|
@ -10,7 +10,7 @@ def logout(method: str,
|
|||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Union[bytes, str, Tuple[int, str]]:
|
||||
-> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]:
|
||||
if 'user' in session_vars:
|
||||
del session_vars['user']
|
||||
return 301, '/'
|
||||
|
|
|
@ -12,41 +12,12 @@ def main_page(method: str,
|
|||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Union[bytes, str, Tuple[int, str]]:
|
||||
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>
|
||||
'''
|
||||
-> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]:
|
||||
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)
|
||||
return 'main.html', {'user': user, 'list': products}
|
||||
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 data
|
||||
return 'main.html', {'list': users}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
from typing import Any, Dict, Tuple, Union
|
||||
|
||||
import urllib.parse
|
||||
|
||||
from matemat.exceptions import AuthenticationError, HttpException
|
||||
from matemat.webserver import pagelet, RequestArguments
|
||||
from matemat.primitives import User
|
||||
|
@ -13,39 +15,18 @@ def touchkey_page(method: str,
|
|||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Union[bytes, str, Tuple[int, str]]:
|
||||
-> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]:
|
||||
if 'user' in session_vars:
|
||||
return 301, '/'
|
||||
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 data.format(username=str(args.username) if 'username' in args else '')
|
||||
return 'touchkey.html', {'username': str(args.username)} if 'username' in args else {}
|
||||
elif method == 'POST':
|
||||
with MatematDatabase('test.db') as db:
|
||||
try:
|
||||
user: User = db.login(str(args.username), touchkey=str(args.touchkey))
|
||||
except AuthenticationError:
|
||||
return 301, f'/touchkey?username={args["username"]}&msg=Please%20try%20again.'
|
||||
quoted = urllib.parse.quote_plus(bytes(args.username))
|
||||
return 301, f'/touchkey?username={quoted}'
|
||||
session_vars['user'] = user
|
||||
return 301, '/'
|
||||
raise HttpException(405)
|
||||
|
|
20
templates/login.html
Normal file
20
templates/login.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Matemat</title>
|
||||
<style>
|
||||
body {
|
||||
color: #f0f0f0;
|
||||
background: #000000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Matemat</h1>
|
||||
<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>
|
33
templates/main.html
Normal file
33
templates/main.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Matemat</title>
|
||||
<style>
|
||||
body {
|
||||
color: #f0f0f0;
|
||||
background: #000000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Matemat</h1>
|
||||
{{ user|default("") }}
|
||||
<ul>
|
||||
{% if user is defined %}
|
||||
{% for l in list %}
|
||||
<li/> <b>{{ l.name }}</b>
|
||||
{% if user.is_member %}
|
||||
{{ l.price_member }}
|
||||
{% else %}
|
||||
{{ l.price_non_member }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for l in list %}
|
||||
<li/> <b><a href="/touchkey?username={{ l.name }}">{{ l.name }}</a></b>
|
||||
{% endfor %}
|
||||
<li/> <a href="/login">Password login</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
20
templates/touchkey.html
Normal file
20
templates/touchkey.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<!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>
|
Loading…
Reference in a new issue