From 4d2d2d30c1a7943d25dcd204a8a38bca870126fe Mon Sep 17 00:00:00 2001 From: s3lph Date: Mon, 9 Jul 2018 00:11:40 +0200 Subject: [PATCH 1/5] Added Jinja2 dependency --- README.md | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index d1eec08..b27f921 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This project intends to provide a well-tested and maintainable alternative to - Python 3 (>=3.6) - Python dependencies: - apsw + - jinja2 ## Usage diff --git a/requirements.txt b/requirements.txt index b8ab4ea..7663204 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ apsw +jinja2 From e3c65776b55835b2831c5b2305825e3fc830b796 Mon Sep 17 00:00:00 2001 From: s3lph Date: Mon, 9 Jul 2018 01:09:53 +0200 Subject: [PATCH 2/5] A first, semi-sane integration of Jinja2 templates --- matemat/webserver/httpd.py | 55 +++++++++++++++++++------- matemat/webserver/pagelets/login.py | 29 ++------------ matemat/webserver/pagelets/logout.py | 2 +- matemat/webserver/pagelets/main.py | 35 ++-------------- matemat/webserver/pagelets/touchkey.py | 31 +++------------ templates/login.html | 20 ++++++++++ templates/main.html | 33 ++++++++++++++++ templates/touchkey.html | 20 ++++++++++ 8 files changed, 127 insertions(+), 98 deletions(-) create mode 100644 templates/login.html create mode 100644 templates/main.html create mode 100644 templates/touchkey.html 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} -
    - {list} -
- - - ''' + -> 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:
    + +
    + + From 1b00c80133b2cf53835824390db6b2f462ca182d Mon Sep 17 00:00:00 2001 From: s3lph Date: Mon, 9 Jul 2018 20:50:02 +0200 Subject: [PATCH 3/5] Implemented a more explicit Pagelet return API using class instances to describe the action to take. --- matemat/webserver/__init__.py | 1 + matemat/webserver/httpd.py | 68 ++++++++-------- matemat/webserver/pagelets/login.py | 14 ++-- matemat/webserver/pagelets/logout.py | 8 +- matemat/webserver/pagelets/main.py | 10 +-- matemat/webserver/pagelets/touchkey.py | 12 +-- matemat/webserver/responses.py | 63 +++++++++++++++ matemat/webserver/test/abstract_httpd_test.py | 6 ++ matemat/webserver/test/test_serve.py | 79 +++++++++++++++---- 9 files changed, 186 insertions(+), 75 deletions(-) create mode 100644 matemat/webserver/responses.py diff --git a/matemat/webserver/__init__.py b/matemat/webserver/__init__.py index c52368e..6059687 100644 --- a/matemat/webserver/__init__.py +++ b/matemat/webserver/__init__.py @@ -7,4 +7,5 @@ server will attempt to serve the request with a static resource in a previously """ from .requestargs import RequestArgument, RequestArguments +from .responses import PageletResponse, RedirectResponse, TemplateResponse from .httpd import MatematWebserver, HttpHandler, pagelet diff --git a/matemat/webserver/httpd.py b/matemat/webserver/httpd.py index c82ba10..41866de 100644 --- a/matemat/webserver/httpd.py +++ b/matemat/webserver/httpd.py @@ -16,7 +16,7 @@ import jinja2 from matemat import __version__ as matemat_version from matemat.exceptions import HttpException -from matemat.webserver import RequestArguments +from matemat.webserver import RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.webserver.util import parse_args # @@ -37,8 +37,7 @@ _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[str, Dict[str, Any]] # Jinja template name and kwargs + PageletResponse, # A generic response ]]] = dict() # Inactivity timeout for client sessions @@ -67,9 +66,8 @@ def pagelet(path: str): headers: The dictionary of HTTP response headers. Add headers you wish to send with the response. 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) + - A PageletResponse class instance: An instance of (a subclass of) + matemat.webserver.PageletResponse, e.g. encapsulating a redirect or a Jinja2 template. raises: HttpException: If a non-200 HTTP status code should be returned :param path: The path to register the function for. @@ -82,8 +80,7 @@ def pagelet(path: str): Dict[str, str]], Union[ bytes, str, - Tuple[int, str], - Tuple[str, Dict[str, Any]] + PageletResponse ]]): # Add the function to the dict of pagelets _PAGELET_PATHS[path] = fun @@ -221,8 +218,7 @@ class HttpHandler(BaseHTTPRequestHandler): 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 + PageletResponse], # Encapsulated or unresolved response body headers: Dict[str, str]) \ -> Tuple[int, bytes]: """ @@ -235,36 +231,34 @@ class HttpHandler(BaseHTTPRequestHandler): """ # The HTTP Response Status Code, defaults to 200 OK hsc: int = 200 - # The HTTP Response body, defaults to None - data: Union[bytes, str] = None - if isinstance(pagelet_res, tuple): - # 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 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): - # Return value is a response body + # The HTTP Response body, defaults to empty + data: bytes = bytes() + + # If the response is a bytes object, it is used without further modification + if isinstance(pagelet_res, bytes): data = pagelet_res + + # If the response is a str object, it is encoded into a bytes object + elif isinstance(pagelet_res, str): + data = pagelet_res.encode('utf-8') + + # If the response is a PageletResponse object, the status code is extracted. Generation of the body depends + # on the subtype + elif isinstance(pagelet_res, PageletResponse): + hsc = pagelet_res.status + + # If the object is a RedirectResponse instance, no body is needed + if isinstance(pagelet_res, RedirectResponse): + headers['Location'] = pagelet_res.location + # If the object is a TemplateRespinse instance, pass the Jinja2 environment instance for rendering + elif isinstance(pagelet_res, TemplateResponse): + # noinspection PyProtectedMember + data = pagelet_res._render(self.server.jinja_env) + # else: Empty body + else: - # Return value is not a response body or a redirect raise TypeError(f'Return value of pagelet not understood: {pagelet_res}') - # The pagelet may return None as data as a shorthand for an empty response - if data is None: - data = bytes() - # If the pagelet returns a Python str, convert it to an UTF-8 encoded bytes object - if isinstance(data, str): - data = data.encode('utf-8') + # Return the resulting status code and body return hsc, data diff --git a/matemat/webserver/pagelets/login.py b/matemat/webserver/pagelets/login.py index d22fcd7..6ef9a9c 100644 --- a/matemat/webserver/pagelets/login.py +++ b/matemat/webserver/pagelets/login.py @@ -1,8 +1,8 @@ -from typing import Any, Dict, Tuple, Union +from typing import Any, Dict, Union from matemat.exceptions import AuthenticationError, HttpException -from matemat.webserver import pagelet, RequestArguments +from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.primitives import User from matemat.db import MatematDatabase @@ -13,17 +13,17 @@ def login_page(method: str, args: RequestArguments, session_vars: Dict[str, Any], headers: Dict[str, str])\ - -> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]: + -> Union[bytes, str, PageletResponse]: if 'user' in session_vars: - return 301, '/' + return RedirectResponse('/') if method == 'GET': - return 'login.html', {} + return TemplateResponse('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' + return RedirectResponse('/login') session_vars['user'] = user - return 301, '/' + return RedirectResponse('/') raise HttpException(405) diff --git a/matemat/webserver/pagelets/logout.py b/matemat/webserver/pagelets/logout.py index 766558a..cbd7cad 100644 --- a/matemat/webserver/pagelets/logout.py +++ b/matemat/webserver/pagelets/logout.py @@ -1,7 +1,7 @@ -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, Union -from matemat.webserver import pagelet, RequestArguments +from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse @pagelet('/logout') @@ -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], Tuple[str, Dict[str, Any]]]: + -> Union[bytes, str, PageletResponse]: if 'user' in session_vars: del session_vars['user'] - return 301, '/' + return RedirectResponse('/') diff --git a/matemat/webserver/pagelets/main.py b/matemat/webserver/pagelets/main.py index 24e6f0b..be0bd60 100644 --- a/matemat/webserver/pagelets/main.py +++ b/matemat/webserver/pagelets/main.py @@ -1,7 +1,7 @@ -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, Union -from matemat.webserver import pagelet, RequestArguments +from matemat.webserver import pagelet, RequestArguments, PageletResponse, TemplateResponse from matemat.primitives import User from matemat.db import MatematDatabase @@ -12,12 +12,12 @@ def main_page(method: str, args: RequestArguments, session_vars: Dict[str, Any], headers: Dict[str, str])\ - -> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]: + -> Union[bytes, str, PageletResponse]: with MatematDatabase('test.db') as db: if 'user' in session_vars: user: User = session_vars['user'] products = db.list_products() - return 'main.html', {'user': user, 'list': products} + return TemplateResponse('main.html', user=user, list=products) else: users = db.list_users() - return 'main.html', {'list': users} + return TemplateResponse('main.html', list=users) diff --git a/matemat/webserver/pagelets/touchkey.py b/matemat/webserver/pagelets/touchkey.py index 27c943e..2b81c89 100644 --- a/matemat/webserver/pagelets/touchkey.py +++ b/matemat/webserver/pagelets/touchkey.py @@ -4,7 +4,7 @@ from typing import Any, Dict, Tuple, Union import urllib.parse from matemat.exceptions import AuthenticationError, HttpException -from matemat.webserver import pagelet, RequestArguments +from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.primitives import User from matemat.db import MatematDatabase @@ -15,18 +15,18 @@ def touchkey_page(method: str, args: RequestArguments, session_vars: Dict[str, Any], headers: Dict[str, str])\ - -> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]: + -> Union[bytes, str, PageletResponse]: if 'user' in session_vars: - return 301, '/' + return RedirectResponse('/') if method == 'GET': - return 'touchkey.html', {'username': str(args.username)} if 'username' in args else {} + return TemplateResponse('touchkey.html', username=str(args.username) if 'username' in args else None) elif method == 'POST': with MatematDatabase('test.db') as db: try: user: User = db.login(str(args.username), touchkey=str(args.touchkey)) except AuthenticationError: quoted = urllib.parse.quote_plus(bytes(args.username)) - return 301, f'/touchkey?username={quoted}' + return RedirectResponse(f'/touchkey?username={quoted}') session_vars['user'] = user - return 301, '/' + return RedirectResponse('/') raise HttpException(405) diff --git a/matemat/webserver/responses.py b/matemat/webserver/responses.py new file mode 100644 index 0000000..f1e7f90 --- /dev/null +++ b/matemat/webserver/responses.py @@ -0,0 +1,63 @@ + +from jinja2 import Environment, Template + + +class PageletResponse: + """ + Base class for pagelet return values that require more action than simply sending plain data. + + An instance of this base class will result in an empty 200 OK response. + """ + + def __init__(self, status: int = 200): + """ + Create an empty response. + + :param status: The HTTP status code, defaults to 200 (OK). + """ + self.status: int = status + + +class RedirectResponse(PageletResponse): + """ + A pagelet response that causes the server to redirect to another location, using a 301 Permanently Moved (uncached) + response status, and a Location header. + """ + + def __init__(self, location: str): + """ + Create a redirection response with the given redirection location. + + :param location: The location to redirect to. + """ + super().__init__(status=301) + self.location: str = location + + +class TemplateResponse(PageletResponse): + """ + A pagelet response that causes the server to load a Jinja2 template and render it with the provided arguments, then + sending the result as response body, with a 200 OK response status. + """ + + def __init__(self, name: str, **kwargs): + """ + Create a template response with the given template name and arguments. + + :param name: Name of the template to load. + :param kwargs: Arguments for rendering the template, will be passed to jinja2.Template.render as is. + """ + super().__init__() + self.name: str = name + self.kwargs = kwargs + + def _render(self, jinja_env: Environment) -> bytes: + """ + Load and render the template using the Jinja2 environment managed by the web server instance. This method + should not be called by a pagelet. + + :param jinja_env: The Jinja2 environment. + :return: An UTF-8 encoded bytes object containing the template rendering result. + """ + template: Template = jinja_env.get_template(self.name) + return template.render(**self.kwargs).encode('utf-8') diff --git a/matemat/webserver/test/abstract_httpd_test.py b/matemat/webserver/test/abstract_httpd_test.py index ff4c0c6..b121dfd 100644 --- a/matemat/webserver/test/abstract_httpd_test.py +++ b/matemat/webserver/test/abstract_httpd_test.py @@ -9,6 +9,8 @@ from abc import ABC from datetime import datetime from http.server import HTTPServer +import jinja2 + from matemat.webserver import pagelet, RequestArguments @@ -107,6 +109,10 @@ class MockServer: self.session_vars: Dict[str, Tuple[datetime, Dict[str, Any]]] = dict() # Webroot for statically served content self.webroot: str = webroot + # Jinja environment with a single, static template + self.jinja_env = jinja2.Environment( + loader=jinja2.DictLoader({'test.txt': 'Hello, {{ what }}!'}) + ) class MockSocket(bytes): diff --git a/matemat/webserver/test/test_serve.py b/matemat/webserver/test/test_serve.py index 5bcdd4d..7b0b61d 100644 --- a/matemat/webserver/test/test_serve.py +++ b/matemat/webserver/test/test_serve.py @@ -1,21 +1,31 @@ -from typing import Any, Dict +from typing import Any, Dict, Union import os import os.path from matemat.exceptions import HttpException -from matemat.webserver import HttpHandler, RequestArguments +from matemat.webserver import HttpHandler, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet -@test_pagelet('/just/testing/serve_pagelet_ok') -def serve_test_pagelet_ok(method: str, - path: str, - args: RequestArguments, - session_vars: Dict[str, Any], - headers: Dict[str, str]): +@test_pagelet('/just/testing/serve_pagelet_str') +def serve_test_pagelet_str(method: str, + path: str, + args: RequestArguments, + session_vars: Dict[str, Any], + headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]: headers['Content-Type'] = 'text/plain' - return 'serve test pagelet ok' + return 'serve test pagelet str' + + +@test_pagelet('/just/testing/serve_pagelet_bytes') +def serve_test_pagelet_bytes(method: str, + path: str, + args: RequestArguments, + session_vars: Dict[str, Any], + headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]: + headers['Content-Type'] = 'application/octet-stream' + return b'serve\x80test\xffpagelet\xfebytes' @test_pagelet('/just/testing/serve_pagelet_redirect') @@ -23,16 +33,27 @@ def serve_test_pagelet_redirect(method: str, path: str, args: RequestArguments, session_vars: Dict[str, Any], - headers: Dict[str, str]): - return 301, '/foo/bar' + headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]: + return RedirectResponse('/foo/bar') +@test_pagelet('/just/testing/serve_pagelet_template') +def serve_test_pagelet_template(method: str, + path: str, + args: RequestArguments, + session_vars: Dict[str, Any], + headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]: + headers['Content-Type'] = 'text/plain' + return TemplateResponse('test.txt', what='World') + + +# noinspection PyTypeChecker @test_pagelet('/just/testing/serve_pagelet_fail') def serve_test_pagelet_fail(method: str, path: str, args: RequestArguments, session_vars: Dict[str, Any], - headers: Dict[str, str]): + headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]: session_vars['test'] = 'hello, world!' headers['Content-Type'] = 'text/plain' raise HttpException() @@ -54,17 +75,29 @@ class TestServe(AbstractHttpdTest): f.write('This should not be readable') os.chmod(forbidden, 0) - def test_serve_pagelet_ok(self): + def test_serve_pagelet_str(self): # Call the test pagelet that produces a 200 OK result - self.client_sock.set_request(b'GET /just/testing/serve_pagelet_ok HTTP/1.1\r\n\r\n') + self.client_sock.set_request(b'GET /just/testing/serve_pagelet_str HTTP/1.1\r\n\r\n') HttpHandler(self.client_sock, ('::1', 45678), self.server) packet = self.client_sock.get_response() # Make sure the correct pagelet was called - self.assertEqual('serve_test_pagelet_ok', packet.pagelet) + self.assertEqual('serve_test_pagelet_str', packet.pagelet) # Make sure the expected content is served self.assertEqual(200, packet.statuscode) - self.assertEqual(b'serve test pagelet ok', packet.body) + self.assertEqual(b'serve test pagelet str', packet.body) + + def test_serve_pagelet_bytes(self): + # Call the test pagelet that produces a 200 OK result + self.client_sock.set_request(b'GET /just/testing/serve_pagelet_bytes HTTP/1.1\r\n\r\n') + HttpHandler(self.client_sock, ('::1', 45678), self.server) + packet = self.client_sock.get_response() + + # Make sure the correct pagelet was called + self.assertEqual('serve_test_pagelet_bytes', packet.pagelet) + # Make sure the expected content is served + self.assertEqual(200, packet.statuscode) + self.assertEqual(b'serve\x80test\xffpagelet\xfebytes', packet.body) def test_serve_pagelet_fail(self): # Call the test pagelet that produces a 500 Internal Server Error result @@ -81,12 +114,26 @@ class TestServe(AbstractHttpdTest): HttpHandler(self.client_sock, ('::1', 45678), self.server) packet = self.client_sock.get_response() + # Make sure the correct pagelet was called + self.assertEqual('serve_test_pagelet_redirect', packet.pagelet) # Make sure the correct redirect is issued self.assertEqual(301, packet.statuscode) self.assertEqual('/foo/bar', packet.headers['Location']) # Make sure the response body is empty self.assertEqual(0, len(packet.body)) + def test_serve_pagelet_template(self): + # Call the test pagelet that redirects to another path + self.client_sock.set_request(b'GET /just/testing/serve_pagelet_template HTTP/1.1\r\n\r\n') + HttpHandler(self.client_sock, ('::1', 45678), self.server) + packet = self.client_sock.get_response() + + # Make sure the correct pagelet was called + self.assertEqual('serve_test_pagelet_template', packet.pagelet) + self.assertEqual(200, packet.statuscode) + # Make sure the response body was rendered correctly by the templating engine + self.assertEqual(b'Hello, World!', packet.body) + def test_serve_static_ok(self): # Request a static resource self.client_sock.set_request(b'GET /static_resource.txt HTTP/1.1\r\n\r\n') From 0481b5bf9853163c055db9893103e2c518968bf5 Mon Sep 17 00:00:00 2001 From: s3lph Date: Mon, 9 Jul 2018 22:46:35 +0200 Subject: [PATCH 4/5] Always add Matemat version number to Jinja template. --- matemat/webserver/responses.py | 4 +++- templates/base.html | 17 +++++++++++++++++ templates/login.html | 19 ++++--------------- templates/main.html | 19 ++++--------------- templates/touchkey.html | 19 ++++--------------- 5 files changed, 32 insertions(+), 46 deletions(-) create mode 100644 templates/base.html diff --git a/matemat/webserver/responses.py b/matemat/webserver/responses.py index f1e7f90..6f0308f 100644 --- a/matemat/webserver/responses.py +++ b/matemat/webserver/responses.py @@ -1,6 +1,8 @@ from jinja2 import Environment, Template +from matemat import __version__ + class PageletResponse: """ @@ -60,4 +62,4 @@ class TemplateResponse(PageletResponse): :return: An UTF-8 encoded bytes object containing the template rendering result. """ template: Template = jinja_env.get_template(self.name) - return template.render(**self.kwargs).encode('utf-8') + return template.render(**self.kwargs, __version__=__version__).encode('utf-8') diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..1ddd113 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,17 @@ + + + + Matemat + + + +

    Matemat {{__version__}}

    + {% block main %} + {% endblock %} + + diff --git a/templates/login.html b/templates/login.html index bdb7168..13c5dca 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,20 +1,9 @@ - - - - Matemat - - - -

    Matemat

    +{% extends "base.html" %} + +{% block main %}
    Username:
    Password:
    - - +{% endblock%} diff --git a/templates/main.html b/templates/main.html index d9a6d45..6aa5887 100644 --- a/templates/main.html +++ b/templates/main.html @@ -1,16 +1,6 @@ - - - - Matemat - - - -

    Matemat

    +{% extends "base.html" %} + +{% block main %} {{ user|default("") }}
      {% if user is defined %} @@ -29,5 +19,4 @@
    • Password login {% endif %}
    - - +{% endblock %} diff --git a/templates/touchkey.html b/templates/touchkey.html index ab12308..bb0a5b9 100644 --- a/templates/touchkey.html +++ b/templates/touchkey.html @@ -1,20 +1,9 @@ - - - - Matemat - - - -

    Matemat

    +{% extends "base.html" %} + +{% block main %}

    Touchkey:
    - - +{% endblock %} From 63a1ac291f6aaf2f1826cba21ceec69c627a798b Mon Sep 17 00:00:00 2001 From: s3lph Date: Mon, 9 Jul 2018 23:48:37 +0200 Subject: [PATCH 5/5] Updated pagelet documentation to adopt the PageletResponse return value API. --- doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc b/doc index 51e9404..9634785 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit 51e940460ddbaebb7f2ffc48d00d9ef19cf8d33f +Subproject commit 9634785e5621b324f72598b3cee83bbc45c25d7b