diff --git a/matemat/webserver/httpd.py b/matemat/webserver/httpd.py index 1f99aa1..c82ba10 100644 --- a/matemat/webserver/httpd.py +++ b/matemat/webserver/httpd.py @@ -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 diff --git a/matemat/webserver/pagelets/login.py b/matemat/webserver/pagelets/login.py index 150b174..d22fcd7 100644 --- a/matemat/webserver/pagelets/login.py +++ b/matemat/webserver/pagelets/login.py @@ -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 = ''' - - - - Matemat - - - -

Matemat

- {msg} -
- Username:
- Password:
- -
- - - ''' - 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) diff --git a/matemat/webserver/pagelets/logout.py b/matemat/webserver/pagelets/logout.py index 05bcfe2..766558a 100644 --- a/matemat/webserver/pagelets/logout.py +++ b/matemat/webserver/pagelets/logout.py @@ -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, '/' diff --git a/matemat/webserver/pagelets/main.py b/matemat/webserver/pagelets/main.py index db4c49a..24e6f0b 100644 --- a/matemat/webserver/pagelets/main.py +++ b/matemat/webserver/pagelets/main.py @@ -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 = ''' - - - - Matemat - - - -

Matemat

- {user} - - - - ''' + -> 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'
  • {p.name} ' + - 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'{user.name} (Logout)' - data = data.format(user=uname, list=plist) + return 'main.html', {'user': user, 'list': products} else: users = db.list_users() - ulist = '\n'.join([f'
  • {u.name}' for u in users]) - ulist = ulist + '
  • Password login' - data = data.format(user='', list=ulist) - return data + return 'main.html', {'list': users} diff --git a/matemat/webserver/pagelets/touchkey.py b/matemat/webserver/pagelets/touchkey.py index 280610e..27c943e 100644 --- a/matemat/webserver/pagelets/touchkey.py +++ b/matemat/webserver/pagelets/touchkey.py @@ -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 = ''' - - - - Matemat - - - -

    Matemat

    -
    -
    - Touchkey:
    - -
    - - - ''' - 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) diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..bdb7168 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,20 @@ + + + + Matemat + + + +

    Matemat

    +
    + Username:
    + Password:
    + +
    + + diff --git a/templates/main.html b/templates/main.html new file mode 100644 index 0000000..d9a6d45 --- /dev/null +++ b/templates/main.html @@ -0,0 +1,33 @@ + + + + Matemat + + + +

    Matemat

    + {{ user|default("") }} +
      + {% if user is defined %} + {% for l in list %} +
    • {{ l.name }} + {% if user.is_member %} + {{ l.price_member }} + {% else %} + {{ l.price_non_member }} + {% endif %} + {% endfor %} + {% else %} + {% for l in list %} +
    • {{ l.name }} + {% endfor %} +
    • Password login + {% endif %} +
    + + diff --git a/templates/touchkey.html b/templates/touchkey.html new file mode 100644 index 0000000..ab12308 --- /dev/null +++ b/templates/touchkey.html @@ -0,0 +1,20 @@ + + + + Matemat + + + +

    Matemat

    +
    +
    + Touchkey:
    + +
    + +