port over base HTML layout from PHP to FastAPI+Jinja2

+ Mounted static files (at web/html) to /static.
+ Added AURWEB_VERSION to aurweb.config (this is used around HTML
  to refer back to aurweb's release on git.archlinux.org), so we
  need it easily accessible in the Python codebase.
+ Implemented basic Jinja2 partials to put together whole aurweb
  pages. This may be missing some things currently and is a WIP
  until this set is ready to be merged.
+ Added config [options] aurwebdir = YOUR_AUR_ROOT; this configuration
  option should specify the root directory of the aurweb project.
  It is used by various parts of the FastAPI codebase to target
  project directories.

Added routes via aurweb.routers.html:
    * POST /language: Set your session language.
    * GET /favicon.ico: Redirect to /static/images/favicon.ico.
        * Some browsers always look for $ROOT/favicon.ico to get an icon
          for the page being loaded, regardless of a specified "shortcut
          icon" given in a <link> directive.
    * GET /: Home page; WIP.

* Updated aurweb.routers.html.language passes query parameters to
  its next redirection.

When calling aurweb.templates.render_template, the context passed should
be formed via the aurweb.templates.make_context. See
aurweb.routers.html.index for an example of this.

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-03-29 15:20:04 -07:00
parent 1ff822bb14
commit 2df90ce280
18 changed files with 339 additions and 2 deletions

View file

@ -2,13 +2,26 @@ import http
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
import aurweb.config
from aurweb.routers import sso
from aurweb.routers import html, sso
routes = set()
# Setup the FastAPI app.
app = FastAPI()
app.mount("/static/css",
StaticFiles(directory="web/html/css"),
name="static_css")
app.mount("/static/js",
StaticFiles(directory="web/html/js"),
name="static_js")
app.mount("/static/images",
StaticFiles(directory="web/html/images"),
name="static_images")
session_secret = aurweb.config.get("fastapi", "session_secret")
if not session_secret:
@ -17,6 +30,14 @@ if not session_secret:
app.add_middleware(SessionMiddleware, secret_key=session_secret)
app.include_router(sso.router)
app.include_router(html.router)
# NOTE: Always keep this dictionary updated with all routes
# that the application contains. We use this to check for
# parameter value verification.
routes = {route.path for route in app.routes}
routes.update({route.path for route in sso.router.routes})
routes.update({route.path for route in html.router.routes})
@app.exception_handler(HTTPException)

View file

@ -1,6 +1,11 @@
import configparser
import os
# Publicly visible version of aurweb. This is used to display
# aurweb versioning in the footer and must be maintained.
# Todo: Make this dynamic/automated.
AURWEB_VERSION = "v5.0.0"
_parser = None

50
aurweb/routers/html.py Normal file
View file

@ -0,0 +1,50 @@
""" AURWeb's primary routing module. Define all routes via @app.app.{get,post}
decorators in some way; more complex routes should be defined in their
own modules and imported here. """
from http import HTTPStatus
from urllib.parse import unquote
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from aurweb.templates import make_context, render_template
router = APIRouter()
@router.get("/favicon.ico")
async def favicon(request: Request):
""" Some browsers attempt to find a website's favicon via root uri at
/favicon.ico, so provide a redirection here to our static icon. """
return RedirectResponse("/static/images/favicon.ico")
@router.post("/language", response_class=RedirectResponse)
async def language(request: Request,
set_lang: str = Form(...),
next: str = Form(...),
q: str = Form(default=None)):
""" A POST route used to set a session's language.
Return a 303 See Other redirect to {next}?next={next}. If we are
setting the language on any page, we want to preserve query
parameters across the redirect.
"""
from aurweb.asgi import routes
if unquote(next) not in routes:
return HTMLResponse(
b"Invalid 'next' parameter.",
status_code=400)
query_string = "?" + q if q else str()
response = RedirectResponse(url=f"{next}{query_string}",
status_code=int(HTTPStatus.SEE_OTHER))
response.set_cookie("AURLANG", set_lang)
return response
@router.get("/", response_class=HTMLResponse)
async def index(request: Request):
""" Homepage route. """
context = make_context(request, "Home")
return render_template("index.html", context)

57
aurweb/templates.py Normal file
View file

@ -0,0 +1,57 @@
import copy
import os
from datetime import datetime
from http import HTTPStatus
import jinja2
from fastapi import Request
from fastapi.responses import HTMLResponse
import aurweb.config
from aurweb import l10n
# Prepare jinja2 objects.
loader = jinja2.FileSystemLoader(os.path.join(
aurweb.config.get("options", "aurwebdir"), "templates"))
env = jinja2.Environment(loader=loader, autoescape=True,
extensions=["jinja2.ext.i18n"])
# Add tr translation filter.
env.filters["tr"] = l10n.tr
def make_context(request: Request, title: str, next: str = None):
""" Create a context for a jinja2 TemplateResponse. """
return {
"request": request,
"language": l10n.get_request_language(request),
"languages": l10n.SUPPORTED_LANGUAGES,
"title": title,
# The 'now' context variable will not show proper datetimes
# until we've implemented timezone support here.
"now": datetime.now(),
"config": aurweb.config,
"next": next if next else request.url.path
}
def render_template(path: str, context: dict, status_code=int(HTTPStatus.OK)):
""" Render a Jinja2 multi-lingual template with some context. """
# Create a deep copy of our jinja2 environment. The environment in
# total by itself is 48 bytes large (according to sys.getsizeof).
# This is done so we can install gettext translations on the template
# environment being rendered without installing them into a global
# which is reused in this function.
templates = copy.copy(env)
translator = l10n.get_raw_translator_for_request(context.get("request"))
templates.install_gettext_translations(translator)
template = templates.get_template(path)
rendered = template.render(context)
return HTMLResponse(rendered, status_code=status_code)