mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
implement /packages/{name} as its own route
A few things added with this commit: - aurweb.packages.util - A module providing package and pkgbase helpers. - aurweb.template.register_filter - A decorator that can be used to register a filter: @register_filter("some_filter") def f(): pass Additionally, template partials have been split off a bit differently. Changes: - /packages/{name} is defined in packages/show.html. - partials/packages/package_actions.html is now partials/packages/actions.html. - partials/packages/details.html has been added. - partials/packages/comments.html has been added. - partials/packages/comment.html has been added. - models.dependency_type additions: name and id constants. - models.relation_type additions: name and id constants. - models.official_provider additions: base official url constant. Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
parent
2d3d03e01e
commit
ae3d302c47
22 changed files with 1166 additions and 254 deletions
|
@ -1,7 +1,13 @@
|
|||
from sqlalchemy import Column, Integer
|
||||
|
||||
from aurweb import db
|
||||
from aurweb.models.declarative import Base
|
||||
|
||||
DEPENDS = "depends"
|
||||
MAKEDEPENDS = "makedepends"
|
||||
CHECKDEPENDS = "checkdepends"
|
||||
OPTDEPENDS = "optdepends"
|
||||
|
||||
|
||||
class DependencyType(Base):
|
||||
__tablename__ = "DependencyTypes"
|
||||
|
@ -12,3 +18,13 @@ class DependencyType(Base):
|
|||
|
||||
def __init__(self, Name: str = None):
|
||||
self.Name = Name
|
||||
|
||||
|
||||
DEPENDS_ID = db.query(DependencyType).filter(
|
||||
DependencyType.Name == DEPENDS).first().ID
|
||||
MAKEDEPENDS_ID = db.query(DependencyType).filter(
|
||||
DependencyType.Name == MAKEDEPENDS).first().ID
|
||||
CHECKDEPENDS_ID = db.query(DependencyType).filter(
|
||||
DependencyType.Name == CHECKDEPENDS).first().ID
|
||||
OPTDEPENDS_ID = db.query(DependencyType).filter(
|
||||
DependencyType.Name == OPTDEPENDS).first().ID
|
||||
|
|
|
@ -3,6 +3,8 @@ from sqlalchemy.exc import IntegrityError
|
|||
|
||||
from aurweb.models.declarative import Base
|
||||
|
||||
OFFICIAL_BASE = "https://aur.archlinux.org"
|
||||
|
||||
|
||||
class OfficialProvider(Base):
|
||||
__tablename__ = "OfficialProviders"
|
||||
|
|
|
@ -61,3 +61,12 @@ class PackageDependency(Base):
|
|||
self.DepDesc = DepDesc
|
||||
self.DepCondition = DepCondition
|
||||
self.DepArch = DepArch
|
||||
|
||||
def is_package(self) -> bool:
|
||||
from aurweb import db
|
||||
from aurweb.models.official_provider import OfficialProvider
|
||||
from aurweb.models.package import Package
|
||||
pkg = db.query(Package, Package.Name == self.DepName)
|
||||
official = db.query(OfficialProvider,
|
||||
OfficialProvider.Name == self.DepName)
|
||||
return pkg.count() > 0 or official.count() > 0
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
from sqlalchemy import Column, Integer
|
||||
|
||||
from aurweb import db
|
||||
from aurweb.models.declarative import Base
|
||||
|
||||
CONFLICTS = "conflicts"
|
||||
PROVIDES = "provides"
|
||||
REPLACES = "replaces"
|
||||
|
||||
|
||||
class RelationType(Base):
|
||||
__tablename__ = "RelationTypes"
|
||||
|
@ -12,3 +17,11 @@ class RelationType(Base):
|
|||
|
||||
def __init__(self, Name: str = None):
|
||||
self.Name = Name
|
||||
|
||||
|
||||
CONFLICTS_ID = db.query(RelationType).filter(
|
||||
RelationType.Name == CONFLICTS).first().ID
|
||||
PROVIDES_ID = db.query(RelationType).filter(
|
||||
RelationType.Name == PROVIDES).first().ID
|
||||
REPLACES_ID = db.query(RelationType).filter(
|
||||
RelationType.Name == REPLACES).first().ID
|
||||
|
|
0
aurweb/packages/__init__.py
Normal file
0
aurweb/packages/__init__.py
Normal file
117
aurweb/packages/util.py
Normal file
117
aurweb/packages/util.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import and_
|
||||
|
||||
from aurweb import db
|
||||
from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider
|
||||
from aurweb.models.package import Package
|
||||
from aurweb.models.package_base import PackageBase
|
||||
from aurweb.models.package_dependency import PackageDependency
|
||||
from aurweb.models.package_relation import PackageRelation
|
||||
from aurweb.models.relation_type import PROVIDES_ID, RelationType
|
||||
from aurweb.templates import register_filter
|
||||
|
||||
|
||||
def dep_depends_extra(dep: PackageDependency) -> str:
|
||||
""" A function used to produce extra text for dependency display. """
|
||||
return str()
|
||||
|
||||
|
||||
def dep_makedepends_extra(dep: PackageDependency) -> str:
|
||||
""" A function used to produce extra text for dependency display. """
|
||||
return "(make)"
|
||||
|
||||
|
||||
def dep_checkdepends_extra(dep: PackageDependency) -> str:
|
||||
""" A function used to produce extra text for dependency display. """
|
||||
return "(check)"
|
||||
|
||||
|
||||
def dep_optdepends_extra(dep: PackageDependency) -> str:
|
||||
""" A function used to produce extra text for dependency display. """
|
||||
return "(optional)"
|
||||
|
||||
|
||||
@register_filter("dep_extra")
|
||||
def dep_extra(dep: PackageDependency) -> str:
|
||||
""" Some dependency types have extra text added to their
|
||||
display. This function provides that output. However, it
|
||||
**assumes** that the dep passed is bound to a valid one
|
||||
of: depends, makedepends, checkdepends or optdepends. """
|
||||
f = globals().get(f"dep_{dep.DependencyType.Name}_extra")
|
||||
return f(dep)
|
||||
|
||||
|
||||
@register_filter("dep_extra_desc")
|
||||
def dep_extra_desc(dep: PackageDependency) -> str:
|
||||
extra = dep_extra(dep)
|
||||
return extra + f" – {dep.DepDesc}"
|
||||
|
||||
|
||||
@register_filter("pkgname_link")
|
||||
def pkgname_link(pkgname: str) -> str:
|
||||
base = "/".join([OFFICIAL_BASE, "packages"])
|
||||
pkg = db.query(Package).filter(Package.Name == pkgname)
|
||||
official = db.query(OfficialProvider).filter(
|
||||
OfficialProvider.Name == pkgname)
|
||||
if not pkg.count() or official.count():
|
||||
return f"{base}/?q={pkgname}"
|
||||
return f"/packages/{pkgname}"
|
||||
|
||||
|
||||
@register_filter("package_link")
|
||||
def package_link(package: Package) -> str:
|
||||
base = "/".join([OFFICIAL_BASE, "packages"])
|
||||
official = db.query(OfficialProvider).filter(
|
||||
OfficialProvider.Name == package.Name)
|
||||
if official.count():
|
||||
return f"{base}/?q={package.Name}"
|
||||
return f"/packages/{package.Name}"
|
||||
|
||||
|
||||
@register_filter("provides_list")
|
||||
def provides_list(package: Package, depname: str) -> list:
|
||||
providers = db.query(Package).join(
|
||||
PackageRelation).join(RelationType).filter(
|
||||
and_(
|
||||
PackageRelation.RelName == depname,
|
||||
RelationType.ID == PROVIDES_ID
|
||||
)
|
||||
)
|
||||
|
||||
string = str()
|
||||
has_providers = providers.count() > 0
|
||||
|
||||
if has_providers:
|
||||
string += "<em>("
|
||||
|
||||
string += ", ".join([
|
||||
f'<a href="{package_link(pkg)}">{pkg.Name}</a>'
|
||||
for pkg in providers
|
||||
])
|
||||
|
||||
if has_providers:
|
||||
string += ")</em>"
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def get_pkgbase(name: str) -> PackageBase:
|
||||
""" Get a PackageBase instance by its name or raise a 404 if
|
||||
it can't be foudn in the database.
|
||||
|
||||
:param name: PackageBase.Name
|
||||
:raises HTTPException: With status code 404 if PackageBase doesn't exist
|
||||
:return: PackageBase instance
|
||||
"""
|
||||
pkgbase = db.query(PackageBase).filter(PackageBase.Name == name).first()
|
||||
if not pkgbase:
|
||||
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
|
||||
|
||||
provider = db.query(OfficialProvider).filter(
|
||||
OfficialProvider.Name == name).first()
|
||||
if provider:
|
||||
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
|
||||
|
||||
return pkgbase
|
|
@ -1,39 +1,98 @@
|
|||
from http import HTTPStatus
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import and_
|
||||
|
||||
import aurweb.models.package
|
||||
import aurweb.models.package_comment
|
||||
import aurweb.models.package_keyword
|
||||
import aurweb.packages.util
|
||||
|
||||
from aurweb import db
|
||||
from aurweb.models.license import License
|
||||
from aurweb.models.package import Package
|
||||
from aurweb.models.package_base import PackageBase
|
||||
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_source import PackageSource
|
||||
from aurweb.models.package_vote import PackageVote
|
||||
from aurweb.models.relation_type import CONFLICTS_ID, RelationType
|
||||
from aurweb.packages.util import get_pkgbase
|
||||
from aurweb.templates import make_variable_context, render_template
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/packages/{package}")
|
||||
async def package_base(request: Request, package: str):
|
||||
package = db.query(PackageBase).filter(PackageBase.Name == package).first()
|
||||
if not package:
|
||||
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
|
||||
async def make_single_context(request: Request,
|
||||
pkgbase: PackageBase) -> Dict[str, Any]:
|
||||
""" Make a basic context for package or pkgbase.
|
||||
|
||||
context = await make_variable_context(request, package.Name)
|
||||
context["git_clone_uri_anon"] = aurweb.config.get("options", "git_clone_uri_anon")
|
||||
context["git_clone_uri_priv"] = aurweb.config.get("options", "git_clone_uri_priv")
|
||||
context["pkgbase"] = package
|
||||
context["packages"] = package.packages.all()
|
||||
context["packages_count"] = package.packages.count()
|
||||
context["keywords"] = package.keywords.all()
|
||||
context["comments"] = package.comments.all()
|
||||
context["is_maintainer"] = request.user.is_authenticated() \
|
||||
and request.user.Username == package.Maintainer.Username
|
||||
:param request: FastAPI request
|
||||
:param pkgbase: PackageBase instance
|
||||
:return: A pkgbase context without specific differences
|
||||
"""
|
||||
context = await make_variable_context(request, pkgbase.Name)
|
||||
context["git_clone_uri_anon"] = aurweb.config.get("options",
|
||||
"git_clone_uri_anon")
|
||||
context["git_clone_uri_priv"] = aurweb.config.get("options",
|
||||
"git_clone_uri_priv")
|
||||
context["pkgbase"] = pkgbase
|
||||
context["packages_count"] = pkgbase.packages.count()
|
||||
context["keywords"] = pkgbase.keywords
|
||||
context["comments"] = pkgbase.comments
|
||||
context["is_maintainer"] = (request.user.is_authenticated()
|
||||
and request.user == pkgbase.Maintainer)
|
||||
context["notified"] = db.query(
|
||||
PackageNotification).join(PackageBase).filter(
|
||||
and_(PackageBase.ID == pkgbase.ID,
|
||||
PackageNotification.UserID == request.user.ID)).count() > 0
|
||||
|
||||
return render_template(request, "pkgbase.html", context)
|
||||
context["out_of_date"] = bool(pkgbase.OutOfDateTS)
|
||||
|
||||
context["voted"] = pkgbase.package_votes.filter(
|
||||
PackageVote.UsersID == request.user.ID).count() > 0
|
||||
|
||||
context["notifications_enabled"] = db.query(
|
||||
PackageNotification).join(PackageBase).filter(
|
||||
PackageBase.ID == pkgbase.ID).count() > 0
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@router.get("/pkgbase/{package}")
|
||||
async def package_base_redirect(request: Request, package: str):
|
||||
return RedirectResponse(f"/packages/{package}")
|
||||
@router.get("/packages/{name}")
|
||||
async def package(request: Request, name: str) -> Response:
|
||||
# Get the PackageBase.
|
||||
pkgbase = get_pkgbase(name)
|
||||
|
||||
# Add our base information.
|
||||
context = await make_single_context(request, pkgbase)
|
||||
|
||||
# Package sources.
|
||||
sources = db.query(PackageSource).join(Package).filter(
|
||||
Package.PackageBaseID == pkgbase.ID)
|
||||
context["sources"] = sources
|
||||
|
||||
# Package dependencies.
|
||||
dependencies = db.query(PackageDependency).join(Package).filter(
|
||||
Package.PackageBaseID == pkgbase.ID)
|
||||
context["dependencies"] = dependencies
|
||||
|
||||
# Package requirements (other packages depend on this one).
|
||||
required_by = db.query(PackageDependency).join(Package).filter(
|
||||
PackageDependency.DepName == pkgbase.Name).order_by(
|
||||
Package.Name.asc())
|
||||
context["required_by"] = required_by
|
||||
|
||||
licenses = db.query(License).join(PackageLicense).join(Package).filter(
|
||||
PackageLicense.PackageID == pkgbase.packages.first().ID)
|
||||
context["licenses"] = licenses
|
||||
|
||||
conflicts = db.query(PackageRelation).join(RelationType).join(Package).join(PackageBase).filter(
|
||||
and_(RelationType.ID == CONFLICTS_ID,
|
||||
PackageBase.ID == pkgbase.ID))
|
||||
context["conflicts"] = conflicts
|
||||
|
||||
return render_template(request, "packages/show.html", context)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import copy
|
||||
import functools
|
||||
import os
|
||||
import zoneinfo
|
||||
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import jinja2
|
||||
|
@ -40,6 +42,31 @@ env.filters["captcha_cmdline"] = captcha.captcha_cmdline_filter
|
|||
env.filters["account_url"] = util.account_url
|
||||
|
||||
|
||||
def register_filter(name: str) -> Callable:
|
||||
""" A decorator that can be used to register a filter.
|
||||
|
||||
Example
|
||||
@register_filter("some_filter")
|
||||
def some_filter(some_value: str) -> str:
|
||||
return some_value.replace("-", "_")
|
||||
|
||||
Jinja2
|
||||
{{ 'blah-blah' | some_filter }}
|
||||
|
||||
:param name: Filter name
|
||||
:return: Callable used for filter
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
if name in env.filters:
|
||||
raise KeyError(f"Jinja already has a filter named '{name}'")
|
||||
env.filters[name] = wrapper
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def make_context(request: Request, title: str, next: str = None):
|
||||
""" Create a context for a jinja2 TemplateResponse. """
|
||||
|
||||
|
|
14
aurweb/testing/html.py
Normal file
14
aurweb/testing/html.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from io import StringIO
|
||||
|
||||
from lxml import etree
|
||||
|
||||
parser = etree.HTMLParser()
|
||||
|
||||
|
||||
def parse_root(html: str) -> etree.Element:
|
||||
""" Parse an lxml.etree.ElementTree root from html content.
|
||||
|
||||
:param html: HTML markup
|
||||
:return: etree.Element
|
||||
"""
|
||||
return etree.parse(StringIO(html), parser)
|
Loading…
Add table
Add a link
Reference in a new issue