Merge branch 'jinja2-template' into 'webserver-impl'

Jinja2 Template support, more explicit pagelet return API

See merge request s3lph/matemat!9
This commit is contained in:
s3lph 2018-07-10 19:53:23 +00:00
commit 652520dc85
16 changed files with 275 additions and 147 deletions

View file

@ -19,6 +19,7 @@ This project intends to provide a well-tested and maintainable alternative to
- Python 3 (>=3.6) - Python 3 (>=3.6)
- Python dependencies: - Python dependencies:
- apsw - apsw
- jinja2
## Usage ## Usage

2
doc

@ -1 +1 @@
Subproject commit 51e940460ddbaebb7f2ffc48d00d9ef19cf8d33f Subproject commit 9634785e5621b324f72598b3cee83bbc45c25d7b

View file

@ -7,4 +7,5 @@ server will attempt to serve the request with a static resource in a previously
""" """
from .requestargs import RequestArgument, RequestArguments from .requestargs import RequestArgument, RequestArguments
from .responses import PageletResponse, RedirectResponse, TemplateResponse
from .httpd import MatematWebserver, HttpHandler, pagelet from .httpd import MatematWebserver, HttpHandler, pagelet

View file

@ -12,9 +12,11 @@ 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, PageletResponse, RedirectResponse, TemplateResponse
from matemat.webserver.util import parse_args from matemat.webserver.util import parse_args
# #
@ -35,7 +37,7 @@ _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 PageletResponse, # A generic response
]]] = dict() ]]] = dict()
# Inactivity timeout for client sessions # Inactivity timeout for client sessions
@ -64,7 +66,8 @@ def pagelet(path: str):
headers: The dictionary of HTTP response headers. Add headers you wish to send with the response. headers: The dictionary of HTTP response headers. Add headers you wish to send with the response.
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 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 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 +80,7 @@ def pagelet(path: str):
Dict[str, str]], Dict[str, str]],
Union[ Union[
bytes, str, bytes, str,
Tuple[int, str] PageletResponse
]]): ]]):
# 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 +100,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 +129,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 +150,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 +215,11 @@ 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
PageletResponse], # Encapsulated or unresolved response body
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.
@ -215,29 +231,34 @@ class HttpHandler(BaseHTTPRequestHandler):
""" """
# The HTTP Response Status Code, defaults to 200 OK # The HTTP Response Status Code, defaults to 200 OK
hsc: int = 200 hsc: int = 200
# The HTTP Response body, defaults to None # The HTTP Response body, defaults to empty
data: Union[bytes, str] = None data: bytes = bytes()
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 response is a bytes object, it is used without further modification
head, tail = pagelet_res if isinstance(pagelet_res, bytes):
if head == 301:
# Set the HTTP Response Status Code, and the redirect header
hsc = 301
headers['Location'] = 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
data = pagelet_res 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: else:
# Return value is not a response body or a redirect
raise TypeError(f'Return value of pagelet not understood: {pagelet_res}') 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 the resulting status code and body
return hsc, data return hsc, data
@ -271,7 +292,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

@ -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.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.primitives import User
from matemat.db import MatematDatabase from matemat.db import MatematDatabase
@ -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, PageletResponse]:
if 'user' in session_vars: if 'user' in session_vars:
return 301, '/' return RedirectResponse('/')
if method == 'GET': if method == 'GET':
data = ''' return TemplateResponse('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 RedirectResponse('/login')
session_vars['user'] = user session_vars['user'] = user
return 301, '/' return RedirectResponse('/')
raise HttpException(405) raise HttpException(405)

View file

@ -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') @pagelet('/logout')
@ -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, PageletResponse]:
if 'user' in session_vars: if 'user' in session_vars:
del session_vars['user'] del session_vars['user']
return 301, '/' return RedirectResponse('/')

View file

@ -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.primitives import User
from matemat.db import MatematDatabase from matemat.db import MatematDatabase
@ -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, PageletResponse]:
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 TemplateResponse('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 TemplateResponse('main.html', list=users)
ulist = ulist + '<li/> <a href=/login>Password login</a>'
data = data.format(user='', list=ulist)
return data

View file

@ -1,8 +1,10 @@
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, PageletResponse, RedirectResponse, TemplateResponse
from matemat.primitives import User from matemat.primitives import User
from matemat.db import MatematDatabase from matemat.db import MatematDatabase
@ -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, PageletResponse]:
if 'user' in session_vars: if 'user' in session_vars:
return 301, '/' return RedirectResponse('/')
if method == 'GET': if method == 'GET':
data = ''' return TemplateResponse('touchkey.html', username=str(args.username) if 'username' in args else None)
<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 RedirectResponse(f'/touchkey?username={quoted}')
session_vars['user'] = user session_vars['user'] = user
return 301, '/' return RedirectResponse('/')
raise HttpException(405) raise HttpException(405)

View file

@ -0,0 +1,65 @@
from jinja2 import Environment, Template
from matemat import __version__
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, __version__=__version__).encode('utf-8')

View file

@ -9,6 +9,8 @@ from abc import ABC
from datetime import datetime from datetime import datetime
from http.server import HTTPServer from http.server import HTTPServer
import jinja2
from matemat.webserver import pagelet, RequestArguments from matemat.webserver import pagelet, RequestArguments
@ -107,6 +109,10 @@ class MockServer:
self.session_vars: Dict[str, Tuple[datetime, Dict[str, Any]]] = dict() self.session_vars: Dict[str, Tuple[datetime, Dict[str, Any]]] = dict()
# Webroot for statically served content # Webroot for statically served content
self.webroot: str = webroot 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): class MockSocket(bytes):

View file

@ -1,21 +1,31 @@
from typing import Any, Dict from typing import Any, Dict, Union
import os import os
import os.path import os.path
from matemat.exceptions import HttpException 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 from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet
@test_pagelet('/just/testing/serve_pagelet_ok') @test_pagelet('/just/testing/serve_pagelet_str')
def serve_test_pagelet_ok(method: str, def serve_test_pagelet_str(method: str,
path: str, path: 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, PageletResponse]:
headers['Content-Type'] = 'text/plain' 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') @test_pagelet('/just/testing/serve_pagelet_redirect')
@ -23,16 +33,27 @@ def serve_test_pagelet_redirect(method: str,
path: str, path: 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, PageletResponse]:
return 301, '/foo/bar' 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') @test_pagelet('/just/testing/serve_pagelet_fail')
def serve_test_pagelet_fail(method: str, def serve_test_pagelet_fail(method: str,
path: str, path: 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, PageletResponse]:
session_vars['test'] = 'hello, world!' session_vars['test'] = 'hello, world!'
headers['Content-Type'] = 'text/plain' headers['Content-Type'] = 'text/plain'
raise HttpException() raise HttpException()
@ -54,17 +75,29 @@ class TestServe(AbstractHttpdTest):
f.write('This should not be readable') f.write('This should not be readable')
os.chmod(forbidden, 0) 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 # 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) HttpHandler(self.client_sock, ('::1', 45678), self.server)
packet = self.client_sock.get_response() packet = self.client_sock.get_response()
# Make sure the correct pagelet was called # 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 # Make sure the expected content is served
self.assertEqual(200, packet.statuscode) 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): def test_serve_pagelet_fail(self):
# Call the test pagelet that produces a 500 Internal Server Error result # 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) HttpHandler(self.client_sock, ('::1', 45678), self.server)
packet = self.client_sock.get_response() 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 # Make sure the correct redirect is issued
self.assertEqual(301, packet.statuscode) self.assertEqual(301, packet.statuscode)
self.assertEqual('/foo/bar', packet.headers['Location']) self.assertEqual('/foo/bar', packet.headers['Location'])
# Make sure the response body is empty # Make sure the response body is empty
self.assertEqual(0, len(packet.body)) 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): def test_serve_static_ok(self):
# Request a static resource # Request a static resource
self.client_sock.set_request(b'GET /static_resource.txt HTTP/1.1\r\n\r\n') self.client_sock.set_request(b'GET /static_resource.txt HTTP/1.1\r\n\r\n')

View file

@ -1 +1,2 @@
apsw apsw
jinja2

17
templates/base.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>Matemat</title>
<style>
body {
color: #f0f0f0;
background: #000000;
}
</style>
</head>
<body>
<h1>Matemat {{__version__}}</h1>
{% block main %}
{% endblock %}
</body>
</html>

9
templates/login.html Normal file
View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block main %}
<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>
{% endblock%}

22
templates/main.html Normal file
View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block main %}
{{ 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>
{% endblock %}

9
templates/touchkey.html Normal file
View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block main %}
<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>
{% endblock %}