A first, semi-sane integration of Jinja2 templates

This commit is contained in:
s3lph 2018-07-09 01:09:53 +02:00
parent 4d2d2d30c1
commit e3c65776b5
8 changed files with 127 additions and 98 deletions

View file

@ -12,6 +12,8 @@ from http.cookies import SimpleCookie
from uuid import uuid4 from uuid import uuid4
from datetime import datetime, timedelta from datetime import datetime, timedelta
import jinja2
from matemat import __version__ as matemat_version from matemat import __version__ as matemat_version
from matemat.exceptions import HttpException from matemat.exceptions import HttpException
from matemat.webserver import RequestArguments from matemat.webserver import RequestArguments
@ -35,7 +37,8 @@ _PAGELET_PATHS: Dict[str, Callable[[str, # HTTP method (GET, POST, ...)
Dict[str, str]], # Response headers Dict[str, str]], # Response headers
Union[ # Return type: either a response body, or a redirect Union[ # Return type: either a response body, or a redirect
bytes, str, # Response body: will assign HTTP/1.0 200 OK 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() ]]] = dict()
# Inactivity timeout for client sessions # Inactivity timeout for client sessions
@ -65,6 +68,8 @@ def pagelet(path: str):
returns: One of the following: returns: One of the following:
- A HTTP Response body as str or bytes - 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 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 raises: HttpException: If a non-200 HTTP status code should be returned
:param path: The path to register the function for. :param path: The path to register the function for.
@ -77,7 +82,8 @@ def pagelet(path: str):
Dict[str, str]], Dict[str, str]],
Union[ Union[
bytes, str, bytes, str,
Tuple[int, str] Tuple[int, str],
Tuple[str, Dict[str, Any]]
]]): ]]):
# Add the function to the dict of pagelets # Add the function to the dict of pagelets
_PAGELET_PATHS[path] = fun _PAGELET_PATHS[path] = fun
@ -97,13 +103,18 @@ class MatematHTTPServer(HTTPServer):
def __init__(self, def __init__(self,
server_address: Any, server_address: Any,
handler: Type[BaseHTTPRequestHandler], handler: Type[BaseHTTPRequestHandler],
webroot: str, staticroot: str,
templateroot: str,
bind_and_activate: bool = True) -> None: bind_and_activate: bool = True) -> None:
super().__init__(server_address, handler, bind_and_activate) super().__init__(server_address, handler, bind_and_activate)
# Resolve webroot directory # Resolve webroot directory
self.webroot = os.path.abspath(webroot) self.webroot = os.path.abspath(staticroot)
# Set up session vars dict # Set up session vars dict
self.session_vars: Dict[str, Tuple[datetime, Dict[str, Any]]] = 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): class MatematWebserver(object):
@ -121,13 +132,18 @@ class MatematWebserver(object):
server.start() 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. Instantiate a MatematWebserver.
:param listen: The IPv4 or IPv6 address to listen on :param listen: The IPv4 or IPv6 address to listen on.
:param port: The TCP port to listen on :param port: The TCP port to listen on.
:param webroot: Path to the webroot directory :param staticroot: Path to the static webroot directory.
:param templateroot: Path to the Jinja2 templates root directory.
""" """
if len(listen) == 0: if len(listen) == 0:
# Empty string should be interpreted as all addresses # Empty string should be interpreted as all addresses
@ -137,7 +153,7 @@ class MatematWebserver(object):
# Rewrite IPv4 address to IPv6-mapped form # Rewrite IPv4 address to IPv6-mapped form
listen = f'::ffff:{listen}' listen = f'::ffff:{listen}'
# Create the http server # Create the http server
self._httpd = MatematHTTPServer((listen, port), HttpHandler, webroot) self._httpd = MatematHTTPServer((listen, port), HttpHandler, staticroot, templateroot)
def start(self) -> None: def start(self) -> None:
""" """
@ -202,8 +218,12 @@ class HttpHandler(BaseHTTPRequestHandler):
if session_id in self.server.session_vars: if session_id in self.server.session_vars:
del self.server.session_vars[session_id] del self.server.session_vars[session_id]
@staticmethod def _parse_pagelet_result(self,
def _parse_pagelet_result(pagelet_res: Union[bytes, str, Tuple[int, str]], headers: Dict[str, str]) \ 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]: -> Tuple[int, bytes]:
""" """
Process the return value of a pagelet function call. Process the return value of a pagelet function call.
@ -218,12 +238,19 @@ class HttpHandler(BaseHTTPRequestHandler):
# The HTTP Response body, defaults to None # The HTTP Response body, defaults to None
data: Union[bytes, str] = None data: Union[bytes, str] = None
if isinstance(pagelet_res, tuple): 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 head, tail = pagelet_res
if head == 301: if head == 301 and isinstance(tail, str):
# Set the HTTP Response Status Code, and the redirect header # Set the HTTP Response Status Code, and the redirect header
hsc = 301 hsc = 301
headers['Location'] = tail 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: else:
raise TypeError(f'Return value of pagelet not understood: {pagelet_res}') raise TypeError(f'Return value of pagelet not understood: {pagelet_res}')
elif isinstance(pagelet_res, str) or isinstance(pagelet_res, bytes): elif isinstance(pagelet_res, str) or isinstance(pagelet_res, bytes):
@ -271,7 +298,7 @@ class HttpHandler(BaseHTTPRequestHandler):
# Call the pagelet function # Call the pagelet function
pagelet_res = _PAGELET_PATHS[path](method, path, args, self.session_vars, headers) 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 # 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 # Send the HTTP status code
self.send_response(hsc) self.send_response(hsc)
# Format the session cookie timeout string and send the session cookie header # Format the session cookie timeout string and send the session cookie header

View file

@ -13,40 +13,17 @@ def login_page(method: str,
args: RequestArguments, args: RequestArguments,
session_vars: Dict[str, Any], session_vars: Dict[str, Any],
headers: Dict[str, str])\ 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: if 'user' in session_vars:
return 301, '/' return 301, '/'
if method == 'GET': if method == 'GET':
data = ''' return 'login.html', {}
<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 '')
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:
return 301, '/login?msg=Username%20or%20password%20wrong.%20Please%20try%20again.' return 301, '/login'
session_vars['user'] = user session_vars['user'] = user
return 301, '/' return 301, '/'
raise HttpException(405) raise HttpException(405)

View file

@ -10,7 +10,7 @@ def logout(method: str,
args: RequestArguments, args: RequestArguments,
session_vars: Dict[str, Any], session_vars: Dict[str, Any],
headers: Dict[str, str])\ 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: if 'user' in session_vars:
del session_vars['user'] del session_vars['user']
return 301, '/' return 301, '/'

View file

@ -12,41 +12,12 @@ def main_page(method: str,
args: RequestArguments, args: RequestArguments,
session_vars: Dict[str, Any], session_vars: Dict[str, Any],
headers: Dict[str, str])\ headers: Dict[str, str])\
-> Union[bytes, str, Tuple[int, str]]: -> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]:
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>
'''
with MatematDatabase('test.db') as db: with MatematDatabase('test.db') as db:
if 'user' in session_vars: if 'user' in session_vars:
user: User = session_vars['user'] user: User = session_vars['user']
products = db.list_products() products = db.list_products()
plist = '\n'.join([f'<li/> <b>{p.name}</b> ' + return 'main.html', {'user': user, 'list': products}
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: 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]) return 'main.html', {'list': users}
ulist = ulist + '<li/> <a href=/login>Password login</a>'
data = data.format(user='', list=ulist)
return data

View file

@ -1,6 +1,8 @@
from typing import Any, Dict, Tuple, Union from typing import Any, Dict, Tuple, Union
import urllib.parse
from matemat.exceptions import AuthenticationError, HttpException from matemat.exceptions import AuthenticationError, HttpException
from matemat.webserver import pagelet, RequestArguments from matemat.webserver import pagelet, RequestArguments
from matemat.primitives import User from matemat.primitives import User
@ -13,39 +15,18 @@ def touchkey_page(method: str,
args: RequestArguments, args: RequestArguments,
session_vars: Dict[str, Any], session_vars: Dict[str, Any],
headers: Dict[str, str])\ 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: if 'user' in session_vars:
return 301, '/' return 301, '/'
if method == 'GET': if method == 'GET':
data = ''' return 'touchkey.html', {'username': str(args.username)} if 'username' in args else {}
<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 '')
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:
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 session_vars['user'] = user
return 301, '/' return 301, '/'
raise HttpException(405) raise HttpException(405)

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