Commit ae3d302c authored by Kevin Morris's avatar Kevin Morris
Browse files

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's avatarKevin Morris <kevr@0cost.org>
parent 2d3d03e0
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
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
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
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
@router.get("/pkgbase/{package}")
async def package_base_redirect(request: Request, package: str):
return RedirectResponse(f"/packages/{package}")
context["notifications_enabled"] = db.query(
PackageNotification).join(PackageBase).filter(
PackageBase.ID == pkgbase.ID).count() > 0
return context
@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)
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. """
......
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)
{% extends "partials/layout.html" %}
{% block pageContent %}
{% include "partials/packages/search.html" %}
<div id="pkgdetails" class="box">
<h2>{{ 'Package Details' | tr }}: {{ pkgbase.Name }} {{ pkgbase.packages.first().Version }}</h2>
{% set result = pkgbase %}
{% include "partials/packages/actions.html" %}
{% set show_package_details = True %}
{% include "partials/packages/details.html" %}
<div id="metadata">
{% include "partials/packages/package_metadata.html" %}
</div>
</div>
{% set pkgname = result.Name %}
{% set pkgbase_id = result.ID %}
{% set comments = comments %}
{% include "partials/packages/comments.html" %}
{% endblock %}
<!--
This partial requires result.Name to render
-->
<div id="detailslinks" class="listing">
<div id="actionlist">
<h4>{{ "Package Actions" | tr }}</h4>
<ul class="small">
<li>
<a href="/cgit/aur.git/tree/PKGBUILD?h={{ result.Name }}">
{{ "View PKGBUILD" | tr }}
</a>
/
<a href="/cgit/aur.git/log/?h={{ result.Name }}">
{{ "View Changes" | tr }}
</a>
</li>
<li>
<a href="/cgit/aur.git/snapshot/{{ result.Name }}.tar.gz">
{{ "Download snapshot" | tr }}
</a>
<li>
<a href="https://wiki.archlinux.org/title/Special:Search?search={{ result.Name }}">
{{ "Search wiki" | tr }}
</a>
</li>
{% if not request.user.is_authenticated() %}
{% if not out_of_date %}
<li>
<a href="/pkgbase/{{ result.Name }}/flag/">
{{ "Flag package out-of-date" | tr }}
</a>
</li>
{% else %}
<li>
<span class="flagged">
{% set ood_ts = result.OutOfDateTS | dt | as_timezone(timezone) %}
{{
"Flagged out-of-date (%s)"
| tr | format(ood_ts.strftime("%Y-%m-%d"))
}}
</span>
</li>
{% endif %}
<li>
<a href="/login?next={{ request.url.path | urlencode }}">
{{ "Vote for this package" | tr }}
</a>
</li>
<li>
<a href="/login?next={{ request.url.path | urlencode }}">
{{ "Enable notifications" | tr }}
</a>
</li>
{% else %}
{% if not out_of_date %}
<li>
<a href="/pkgbase/{{ result.Name }}/flag/">
{{ "Flag package out-of-date" | tr }}
</a>
</li>
{% else %}
<li>
<span class="flagged">
{% set ood_ts = result.OutOfDateTS | dt | as_timezone(timezone) %}
{{
"Flagged out-of-date (%s)"
| tr | format(ood_ts.strftime("%Y-%m-%d"))
}}
</span>
</li>
<li>
<form action="/pkgbase/{{ result.Name }}/unflag" method="post">
<input class="button text-button"
type="submit"
name="do_UnFlag"
value="{{ 'Unflag package' | tr }}"
/>
</form>
</li>
{% endif %}
<li>
{% if not voted %}
<form action="/pkgbase/{{ result.Name }}/vote/" method="post">
<input type="submit"
class="button text-button"
name="do_Vote"
value="{{ 'Vote for this package' | tr }}" />
</form>
{% else %}
<form action="/pkgbase/{{ result.Name }}/unvote/" method="post">
<input type="submit"
class="button text-button"
name="do_UnVote"
value="{{ 'Remove vote' | tr }}" />
</form>
{% endif %}
</li>
<li>
{% if notified %}
<form action="/pkgbase/{{ result.Name }}/unnotify/" method="post">
<input type="submit"
class="button text-button"
name="do_UnNotify"
value="{{ 'Disable notifications' | tr }}"
/>
</form>
{% else %}
<form action="/pkgbase/{{ result.Name }}/notify/" method="post">
<input type="submit"
class="button text-button"
name="do_Notify"
value="{{ 'Enable notifications' | tr }}"
/>
</form>
{% endif %}
</li>
{% endif %}
</form>
{% if is_maintainer %}
<li>
<a href="/pkgbase/{{ result.Name }}/comaintainers/">
{{ "Manage Co-Maintainers" | tr }}
</a>
</li>
{% endif %}
<li><span class="flagged"></span></li>
<li>
{% if not request.user.is_authenticated() %}
<a href="/login?next={{ '/pkgbase/%s/request' | format(result.Name) | urlencode }}">
{{ "Submit Request" | tr }}
</a>
{% else %}
<a href="/pkgbase/{{ result.Name }}/request/">
{{ "Submit Request" | tr }}
</a>
{% endif %}
</li>
{% if is_maintainer %}
<li>
<a href="/pkgbase/{{ result.Name }}/delete/">
{{ "Delete Package" | tr }}
</a>
</li>
<li>
<a href="/pkgbase/{{ result.Name }}/merge/">
{{ "Merge Package" | tr }}
</a>
</li>
<li>
<form action="/pkgbase/{{ result.Name }}/disown/" method="post">
<input type="submit"
class="button text-button"
name="do_Disown"
value="{{ 'Disown Package' | tr }}"
/>
</form>
</li>
{% endif %}
</ul>
</div>
</div>
<h4 id="comment-{{ comment.ID }}" class="comment-header">
{% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %}
{% set view_account_info = 'View account information for %s' | tr | format(comment.User.Username) %}
{{
"%s commented on %s" | tr | format(
('<a href="/account/%s" title="%s">%s</a>' | format(
comment.User.Username,
view_account_info,
comment.User.Username
)) if request.user.is_authenticated() else
(comment.User.Username),
'<a href="#comment-%s" class="date">%s</a>' | format(
comment.ID,
commented_at.strftime("%Y-%m-%d %H:%M")
)
)
| safe
}}
{% if is_maintainer %}
<form class="delete-comment-form" method="post" action="/pkgbase/{{ pkgname }}/">
<fieldset style="display:inline;">
<input type="hidden" name="action" value="do_DeleteComment" />
<input type="hidden" name="comment_id" value="{{ comment.ID }}"/>
<input type="hidden" name="return_to" value="/pkgbase/{{ pkgname }}/"/>
<input type="image" class="delete-comment" src="/images/x.min.svg" width="11" height="11" alt="{{ 'Delete comment' | tr }}" title="{{ 'Delete comment' | tr }}" name="submit" value="1" />
</fieldset>
</form>
<a href="/pkgbase/{{ pkgname }}/edit-comment/?comment_id={{ comment.ID }}" class="edit-comment" title="Edit comment"><img src="/images/pencil.min.svg" alt="Edit comment" width="11" height="11"></a>
{% endif %}
<form class="pin-comment-form" method="post" action="/pkgbase/{{ pkgname }}/">
<fieldset style="display:inline;">
<input type="hidden" name="action" value="do_PinComment"/>
<input type="hidden" name="comment_id" value="{{ comment.ID }}"/>
<input type="hidden" name="package_base" value="{{ pkgbase_id }}"/>
<input type="hidden" name="return_to" value="/pkgbase/{{ pkgname }}/"/>
<input type="image" class="pin-comment" src="/images/pin.min.svg" width="11" height="11" alt="{{ 'Pin comment' | tr }}" title="{{ 'Pin comment' | tr }}" name="submit" value="1"/>
</fieldset>
</form>
</h4>
<div id="comment-{{ comment.ID }}-content" class="article-content">
<div>
{% if comment.RenderedComment %}
{{ comment.RenderedComment | safe }}