feat(FastAPI): added /requests (get) route

Introduces `aurweb.defaults` and `aurweb.filters`.

`aurweb.filters` is a location developers can put their additional
Jinja2 filters and/or functions. We should slowly move all of our
filters over here, where it makes sense.

`aurweb.defaults` is a new module which hosts some default constants
and utility functions, starting with offsets (O) and per page values
(PP).

As far as the new GET /requests is concerned, we match up here to
PHP's implementation, with some minor improvements:

Improvements:

* PP on this page is now configurable: 50 (default), 100, or 250.
    * Example: `https://localhost:8444/requests?PP=250`

Modifications:

* The pagination is a bit different, but serves the exact same purpose.
* "Last" no longer goes to an empty page.
    * Closes: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/14

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-09-10 13:28:11 -07:00
parent c164abe256
commit 99482f9962
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
11 changed files with 341 additions and 17 deletions

View file

@ -31,8 +31,23 @@ class StubQuery:
class AnonymousUser:
""" A stubbed User class used when an unauthenticated User
makes a request against FastAPI. """
# Stub attributes used to mimic a real user.
ID = 0
class AccountType:
""" A stubbed AccountType static class. In here, we use an ID
and AccountType which do not exist in our constant records.
All records primary keys (AccountType.ID) should be non-zero,
so using a zero here means that we'll never match against a
real AccountType. """
ID = 0
AccountType = "Anonymous"
# AccountTypeID == AccountType.ID; assign a stubbed column.
AccountTypeID = AccountType.ID
LangPreference = aurweb.config.get("options", "default_lang")
Timezone = aurweb.config.get("options", "default_timezone")

18
aurweb/defaults.py Normal file
View file

@ -0,0 +1,18 @@
""" Constant default values centralized in one place. """
# Default [O]ffset
O = 0
# Default [P]er [P]age
PP = 50
# A whitelist of valid PP values
PP_WHITELIST = {50, 100, 250}
def fallback_pp(per_page: int) -> int:
""" If `per_page` is a valid value in PP_WHITELIST, return it.
Otherwise, return defaults.PP. """
if per_page not in PP_WHITELIST:
return PP
return per_page

View file

@ -4,8 +4,8 @@ import paginate
from jinja2 import pass_context
from aurweb import util
from aurweb.templates import register_filter
from aurweb import config, util
from aurweb.templates import register_filter, register_function
@register_filter("pager_nav")
@ -48,3 +48,13 @@ def pager_nav(context: Dict[str, Any],
symbol_previous=" Previous",
symbol_next="Next ",
symbol_last="Last »")
@register_function("config_getint")
def config_getint(section: str, key: str) -> int:
return config.getint(section, key)
@register_function("round")
def do_round(f: float) -> int:
return round(f)

View file

@ -2,17 +2,18 @@ from datetime import datetime
from http import HTTPStatus
from typing import Any, Dict
from fastapi import APIRouter, Form, HTTPException, Request, Response
from fastapi import APIRouter, Form, HTTPException, Query, Request, Response
from fastapi.responses import JSONResponse, RedirectResponse
from sqlalchemy import and_
from sqlalchemy import and_, case
import aurweb.filters
import aurweb.models.package_comment
import aurweb.models.package_keyword
import aurweb.packages.util
from aurweb import db, l10n
from aurweb.auth import auth_required
from aurweb import db, defaults, l10n
from aurweb.auth import account_type_required, auth_required
from aurweb.models.account_type import DEVELOPER, TRUSTED_USER, TRUSTED_USER_AND_DEV
from aurweb.models.license import License
from aurweb.models.package import Package
from aurweb.models.package_base import PackageBase
@ -22,10 +23,11 @@ from aurweb.models.package_dependency import PackageDependency
from aurweb.models.package_license import PackageLicense
from aurweb.models.package_notification import PackageNotification
from aurweb.models.package_relation import PackageRelation
from aurweb.models.package_request import PackageRequest
from aurweb.models.package_request import PENDING_ID, PackageRequest
from aurweb.models.package_source import PackageSource
from aurweb.models.package_vote import PackageVote
from aurweb.models.relation_type import CONFLICTS_ID
from aurweb.models.request_type import RequestType
from aurweb.models.user import User
from aurweb.packages.search import PackageSearch
from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted
@ -535,3 +537,31 @@ async def package_base_comaintainers_post(
return RedirectResponse(f"/pkgbase/{pkgbase.Name}",
status_code=int(HTTPStatus.SEE_OTHER))
@router.get("/requests")
@account_type_required({TRUSTED_USER, DEVELOPER, TRUSTED_USER_AND_DEV})
@auth_required(True, redirect="/")
async def requests(request: Request,
O: int = Query(default=defaults.O),
PP: int = Query(default=defaults.PP)):
context = make_context(request, "Requests")
context["q"] = dict(request.query_params)
context["O"] = O
context["PP"] = PP
# A PackageRequest query, with left inner joined User and RequestType.
query = db.query(PackageRequest).join(
User, PackageRequest.UsersID == User.ID
).join(RequestType)
context["total"] = query.count()
context["results"] = query.order_by(
# Order primarily by the Status column being PENDING_ID,
# and secondarily by RequestTS; both in descending order.
case([(PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(),
PackageRequest.RequestTS.desc()
).limit(PP).offset(O).all()
return render_template(request, "requests.html", context)

View file

@ -71,6 +71,20 @@ def register_filter(name: str) -> Callable:
return decorator
def register_function(name: str) -> Callable:
""" A decorator that can be used to register a function.
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
if name in _env.globals:
raise KeyError(f"Jinja already has a function named '{name}'")
_env.globals[name] = wrapper
return wrapper
return decorator
def make_context(request: Request, title: str, next: str = None):
""" Create a context for a jinja2 TemplateResponse. """
@ -83,6 +97,7 @@ def make_context(request: Request, title: str, next: str = None):
"timezones": time.SUPPORTED_TIMEZONES,
"title": title,
"now": datetime.now(tz=zoneinfo.ZoneInfo(timezone)),
"utcnow": int(datetime.utcnow().timestamp()),
"config": aurweb.config,
"next": next if next else request.url.path
}