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)
|
23
templates/packages/show.html
Normal file
23
templates/packages/show.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% 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 %}
|
162
templates/partials/packages/actions.html
Normal file
162
templates/partials/packages/actions.html
Normal file
|
@ -0,0 +1,162 @@
|
|||
<!--
|
||||
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>
|
48
templates/partials/packages/comment.html
Normal file
48
templates/partials/packages/comment.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
<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 }}
|
||||
{% else %}
|
||||
{{ comment.Comments }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -5,6 +5,7 @@
|
|||
- comments (list)
|
||||
-->
|
||||
|
||||
{% if request.user.is_authenticated() %}
|
||||
<div id="generic-form" class="box">
|
||||
<h2>Add Comment</h2>
|
||||
<form action="/pkgbase/{{ pkgname }}/" method="post">
|
||||
|
@ -31,61 +32,103 @@
|
|||
</p>
|
||||
<p>
|
||||
<input type="submit" value="{{ 'Add Comment' | tr }}"/>
|
||||
{% if not notifications_enabled %}
|
||||
<span class="comment-enable-notifications">
|
||||
<input id="id_enable_notifications"
|
||||
type="checkbox"
|
||||
name="enable_notifications"
|
||||
/>
|
||||
<label for="id_enable_notifications">
|
||||
{{ "Enable notifications" | tr }}
|
||||
</label>
|
||||
</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="comments package-comments">
|
||||
{% if comments.count() %}
|
||||
<div class="comments package-comments">
|
||||
<div class="comments-header">
|
||||
<h3>
|
||||
<span class="text">{{ "Latest Comments" | tr }}</span>
|
||||
<span class="arrow"></span>
|
||||
</h3>
|
||||
</div>
|
||||
{% for comment in comments %}
|
||||
<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
|
||||
),
|
||||
'<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>
|
||||
<p>{{ comment.RenderedComment | safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% for comment in comments.all() %}
|
||||
{% include "partials/packages/comment.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script type="text/javascript" nonce="{{ request.user.nonce }}">
|
||||
function add_busy_indicator(sibling) {
|
||||
const img = document.createElement('img');
|
||||
img.src = "/images/ajax-loader.gif";
|
||||
img.classList.add('ajax-loader');
|
||||
img.style.height = 11;
|
||||
img.style.width = 16;
|
||||
img.alt = "Busy…";
|
||||
|
||||
sibling.insertAdjacentElement('afterend', img);
|
||||
}
|
||||
|
||||
function remove_busy_indicator(sibling) {
|
||||
const elem = sibling.nextElementSibling;
|
||||
elem.parentNode.removeChild(elem);
|
||||
}
|
||||
|
||||
function getParentsUntil(elem, className) {
|
||||
// Limit to 10 depth
|
||||
for ( ; elem && elem !== document; elem = elem.parentNode) {
|
||||
if (elem.matches(className)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
function handleEditCommentClick(event) {
|
||||
event.preventDefault();
|
||||
const parent_element = getParentsUntil(event.target, '.comment-header');
|
||||
const parent_id = parent_element.id;
|
||||
const comment_id = parent_id.substr(parent_id.indexOf('-') + 1);
|
||||
// The div class="article-content" which contains the comment
|
||||
const edit_form = parent_element.nextElementSibling;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
type: "get-comment-form",
|
||||
arg: comment_id,
|
||||
base_id: {{ pkgbase.ID }},
|
||||
pkgbase_name: {{ pkgbase.Name }}
|
||||
});
|
||||
|
||||
const url = '/rpc?' + params.toString();
|
||||
|
||||
add_busy_indicator(event.target);
|
||||
|
||||
fetch(url, {
|
||||
method: 'GET'
|
||||
})
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(data) {
|
||||
remove_busy_indicator(event.target);
|
||||
if (data.success) {
|
||||
edit_form.innerHTML = data.form;
|
||||
edit_form.querySelector('textarea').focus();
|
||||
} else {
|
||||
alert(data.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const divs = document.querySelectorAll('.edit-comment');;
|
||||
for (let div of divs) {
|
||||
div.addEventListener('click', handleEditCommentClick);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
145
templates/partials/packages/details.html
Normal file
145
templates/partials/packages/details.html
Normal file
|
@ -0,0 +1,145 @@
|
|||
<table id="pkginfo">
|
||||
<tr>
|
||||
<th>{{ "Git Clone URL" | tr }}:</th>
|
||||
<td>
|
||||
<a class="copy" href="{{ git_clone_uri_anon | format(pkgbase.Name) }}">{{ git_clone_uri_anon | format(pkgbase.Name) }}</a> ({{ "read-only" | tr }}, {{ "click to copy" | tr }})
|
||||
{% if is_maintainer %}
|
||||
<br /> <a class="copy" href="{{ git_clone_uri_priv | format(pkgbase.Name) }}">{{ git_clone_uri_priv | format(pkgbase.Name) }}</a> ({{ "click to copy" | tr }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if show_package_details | default(False) %}
|
||||
<tr>
|
||||
<th>{{ "Package Base" | tr }}:</th>
|
||||
<td class="wrap">
|
||||
<a href="/pkgbase/{{ pkgbase.Name }}">
|
||||
{{ pkgbase.Name }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Description" | tr }}:</th>
|
||||
<td class="wrap">{{ pkgbase.packages.first().Description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Upstream URL" | tr }}:</th>
|
||||
<td class="wrap">
|
||||
{% set pkg = pkgbase.packages.first() %}
|
||||
{% if pkg.URL %}
|
||||
<a href="{{ pkg.URL }}">{{ pkg.URL }}</a>
|
||||
{% else %}
|
||||
{{ "None" | tr }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if pkgbase.keywords.count() %}
|
||||
<tr>
|
||||
<th>{{ "Keywords" | tr }}:</th>
|
||||
{% if is_maintainer %}
|
||||
<td>
|
||||
<form method="update"
|
||||
action="/pkgbase/{{ pkgbase.Name }}/keywords/"
|
||||
>
|
||||
<div>
|
||||
<input type="text"
|
||||
name="keywords"
|
||||
value="{{ pkgbase.keywords | join(' ', attribute='Keyword') }}"
|
||||
/>
|
||||
<input type="submit" value="{{ 'Update' | tr }}"/>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
{% for keyword in pkgbase.keywords %}
|
||||
<a class="keyword"
|
||||
href="/packages/?K={{ keyword.Keyword }}&SB=p"
|
||||
>
|
||||
{{ keyword.Keyword }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if licenses and licenses.count() and show_package_details | default(False) %}
|
||||
<tr>
|
||||
<th>{{ "Licenses" | tr }}:</th>
|
||||
<td>{{ licenses | join(', ', attribute='Name') | default('None' | tr) }} </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if show_package_details | default(False) %}
|
||||
<tr>
|
||||
<th>{{ "Conflicts" | tr }}:</th>
|
||||
<td class="wrap">
|
||||
{{ conflicts | join(', ', attribute='RelName') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>{{ "Submitter" | tr }}:</th>
|
||||
<td>
|
||||
{% if request.user.is_authenticated() %}
|
||||
<a href="/account/{{ pkgbase.Submitter.Username }}">
|
||||
{{ pkgbase.Submitter.Username | default("None" | tr) }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ pkgbase.Submitter.Username | default("None" | tr) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Maintainer" | tr }}:</th>
|
||||
<td>
|
||||
{% if request.user.is_authenticated() %}
|
||||
<a href="/account/{{ pkgbase.Maintainer.Username }}">
|
||||
{{ pkgbase.Maintainer.Username | default("None" | tr) }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ pkgbase.Maintainer.Username | default("None" | tr) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Last Packager" | tr }}:</th>
|
||||
<td>
|
||||
{% if request.user.is_authenticated() %}
|
||||
<a href="/account/{{ pkgbase.Packager.Username }}">
|
||||
{{ pkgbase.Packager.Username | default("None" | tr) }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ pkgbase.Packager.Username | default("None" | tr) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Votes" | tr }}:</th>
|
||||
{% if not is_maintainer %}
|
||||
<td>{{ pkgbase.package_votes.count() }}</td>
|
||||
{% else %}
|
||||
<td>
|
||||
<a href="/pkgbase/{{ pkgbase.Name }}/voters">
|
||||
{{ pkgbase.package_votes.count() }}
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Popularity" | tr }}:</th>
|
||||
<td>{{ pkgbase.Popularity | number_format(6 if pkgbase.Popularity <= 0.2 else 2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% set submitted = pkgbase.SubmittedTS | dt | as_timezone(timezone) %}
|
||||
<th>{{ "First Submitted" | tr }}:</th>
|
||||
<td>{{ "%s" | format(submitted.strftime("%Y-%m-%d %H:%M")) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Last Updated" | tr }}:</th>
|
||||
{% set updated = pkgbase.ModifiedTS | dt | as_timezone(timezone) %}
|
||||
<td>{{ "%s" | format(updated.strftime("%Y-%m-%d %H:%M")) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<script type="text/javascript" src="/static/js/copy.js"></script>
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
<!--
|
||||
This partial requires pkgname 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={{ pkgname }}">
|
||||
{{ "View PKGBUILD" | tr }}
|
||||
</a>
|
||||
/
|
||||
<a href="/cgit/aur.git/log/?h={{ pkgname }}">
|
||||
{{ "View Changes" | tr }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/cgit/aur.git/snapshot/{{ pkgname }}.tar.gz">
|
||||
{{ "Download snapshot" | tr }}
|
||||
</a>
|
||||
<li>
|
||||
<a href="https://wiki.archlinux.org/title/Special:Search?search={{ pkgname }}">
|
||||
{{ "Search wiki" | tr }}
|
||||
</a>
|
||||
</li>
|
||||
<li><span class="flagged"></span></li>
|
||||
<li>
|
||||
<a href="/pkgbase/{{ pkgname }}/flag/">
|
||||
{{ "Flag package out-of-date" | tr }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<form action="/pkgbase/{{ pkgname }}/vote/" method="post">
|
||||
<input type="submit"
|
||||
class="button text-button"
|
||||
name="do_Vote"
|
||||
value="{{ 'Vote for this package' | tr }}" />
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form action="/pkgbase/{{ pkgname }}/unnotify/" method="post">
|
||||
<input type="submit"
|
||||
class="button text-button"
|
||||
name="do_UnNotify"
|
||||
value="{{ 'Disable notifications' | tr }}"
|
||||
/>
|
||||
</form>
|
||||
</li>
|
||||
{% if is_maintainer %}
|
||||
<li>
|
||||
<a href="/pkgbase/{{ pkgname }}/comaintainers/">
|
||||
{{ "Manage Co-Maintainers" | tr }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li><span class="flagged"></span></li>
|
||||
{% if request.user.is_authenticated() %}
|
||||
<li>
|
||||
<a href="/pkgbase/{{ pkgname }}/request/">
|
||||
{{ "Submit Request" | tr }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_maintainer %}
|
||||
<li>
|
||||
<a href="/pkgbase/{{ pkgname }}/delete/">
|
||||
{{ "Delete Package" | tr }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/pkgbase/{{ pkgname }}/merge/">
|
||||
{{ "Merge Package" | tr }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<form action="/pkgbase/{{ pkgname }}/disown/" method="post">
|
||||
<input type="submit"
|
||||
class="button text-button"
|
||||
name="do_Disown"
|
||||
value="{{ 'Disown Package' | tr }}"
|
||||
/>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
54
templates/partials/packages/package_metadata.html
Normal file
54
templates/partials/packages/package_metadata.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
<div id="pkgdeps" class="listing">
|
||||
<h3>Dependencies ({{ dependencies.count() }})</h3>
|
||||
<ul id="pkgdepslist">
|
||||
{% for dep in dependencies.all() %}
|
||||
<li>
|
||||
{% set broken = not dep.is_package() %}
|
||||
{% if broken %}
|
||||
<span class="broken">
|
||||
{% else %}
|
||||
<a href="{{ dep.DepName | pkgname_link }}">
|
||||
{% endif %}
|
||||
{{ dep.DepName }}
|
||||
{% if broken %}
|
||||
</span>
|
||||
{% else %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ dep.Package | provides_list(dep.DepName) | safe }}
|
||||
{% set extra = dep | dep_extra %}
|
||||
{% if extra %}
|
||||
<em>{{ dep | dep_extra_desc }}</em>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="pkgreqs" class="listing">
|
||||
<h3>Required by ({{ required_by.count() }})</h3>
|
||||
<ul id="pkgreqslist">
|
||||
{% for depender in required_by.all() %}
|
||||
<li>
|
||||
<a href="{{ depender.Package | package_link }}">
|
||||
{{ depender.Package.Name }}
|
||||
</a>
|
||||
<em>{{ depender | dep_extra }}</em>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="pkgfiles" class="listing">
|
||||
<h3>Sources ({{ sources.count() }})</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul id="pkgsrcslist">
|
||||
{% for source in sources.all() %}
|
||||
<li>
|
||||
<a href="{{ source.Source }}">{{ source.Source }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
|
@ -1,95 +0,0 @@
|
|||
{% extends "partials/layout.html" %}
|
||||
|
||||
{% block pageContent %}
|
||||
{% include "partials/packages/search.html" %}
|
||||
<div id="pkgdetails" class="box">
|
||||
<h2>Package Details: {{ pkgbase.Name }}</h2>
|
||||
|
||||
{% set result = pkgbase %}
|
||||
{% set pkgname = "result.Name" %}
|
||||
{% include "partials/packages/package_actions.html" %}
|
||||
|
||||
<table id="pkginfo">
|
||||
<tr>
|
||||
<th>{{ "Git Clone URL" | tr }}:</th>
|
||||
<td>
|
||||
<a class="copy" href="{{ git_clone_uri_anon | format(pkgbase.Name) }}">{{ git_clone_uri_anon | format(pkgbase.Name) }}</a> ({{ "read-only" | tr }}, {{ "click to copy" | tr }})
|
||||
{% if is_maintainer %}
|
||||
<br /> <a class="copy" href="{{ git_clone_uri_priv | format(pkgbase.Name) }}">{{ git_clone_uri_priv | format(pkgbase.Name) }}</a> ({{ "click to copy" | tr }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Keywords" | tr }}:</th>
|
||||
{% if is_maintainer %}
|
||||
<td>
|
||||
<form method="post" action="/pkgbase/{{ pkgbase.Name }}/">
|
||||
<div>
|
||||
<input type="hidden" name="action" value="do_SetKeywords" />
|
||||
<input type="text" name="keywords" value="{{ pkgbase.keywords | join(' ', attribute='Keyword') }}"/>
|
||||
<input type="submit" value="Update"/>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
{% for item in pkgbase.keywords %}
|
||||
<a class="keyword" href="/packages/?K={{ item.Keyword }}&SB=p">{{ item.Keyword }}</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Submitter" | tr }}:</th>
|
||||
<td>{{ pkgbase.Submitter.Username | default("None") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Maintainer" | tr }}:</th>
|
||||
<td>{{ pkgbase.Maintainer.Username | default("None") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Last Packager" | tr }}:</th>
|
||||
<td>{{ pkgbase.Packager.Username | default("None") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Votes" | tr }}:</th>
|
||||
<td>{{ pkgbase.NumVotes }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Popularity" | tr }}:</th>
|
||||
<td>{{ '%0.2f' % pkgbase.Popularity | float }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% set submitted = pkgbase.SubmittedTS | dt | as_timezone(timezone) %}
|
||||
<th>{{ "First Submitted" | tr }}:</th>
|
||||
<td>{{ "%s" | tr | format(submitted.strftime("%Y-%m-%d %H:%M")) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Last Updated" | tr }}:</th>
|
||||
{% set updated = pkgbase.ModifiedTS | dt | as_timezone(timezone) %}
|
||||
<td>{{ "%s" | tr | format(updated.strftime("%Y-%m-%d %H:%M")) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div id="metadata">
|
||||
<div id="pkgs" class="listing">
|
||||
<!-- This needs to be replaced with the real implementation. -->
|
||||
<h3>Packages ({{ packages_count }})</h3>
|
||||
<ul>
|
||||
{% for result in packages %}
|
||||
<li>
|
||||
<a href="/packages/{{ result.Name }}/"
|
||||
title="{{ 'View packages details for' | tr }} {{ result.Name }}">
|
||||
{{ result.Name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% set pkgname = result.Name %}
|
||||
{% set pkgbase_id = result.ID %}
|
||||
{% set comments = comments %}
|
||||
{% include "partials/packages/comments.html" %}
|
||||
{% endblock %}
|
|
@ -77,6 +77,13 @@ def test_package_dependencies():
|
|||
assert pkgdep in optdepends.package_dependencies
|
||||
assert pkgdep in package.package_dependencies
|
||||
|
||||
assert not pkgdep.is_package()
|
||||
|
||||
base = create(PackageBase, Name=pkgdep.DepName, Maintainer=user)
|
||||
create(Package, PackageBase=base, Name=pkgdep.DepName)
|
||||
|
||||
assert pkgdep.is_package()
|
||||
|
||||
|
||||
def test_package_dependencies_null_package_raises_exception():
|
||||
from aurweb.db import session
|
||||
|
|
283
test/test_packages_routes.py
Normal file
283
test/test_packages_routes.py
Normal file
|
@ -0,0 +1,283 @@
|
|||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from aurweb import asgi, db
|
||||
from aurweb.models.account_type import USER_ID, AccountType
|
||||
from aurweb.models.dependency_type import DependencyType
|
||||
from aurweb.models.official_provider import OfficialProvider
|
||||
from aurweb.models.package import Package
|
||||
from aurweb.models.package_base import PackageBase
|
||||
from aurweb.models.package_comment import PackageComment
|
||||
from aurweb.models.package_dependency import PackageDependency
|
||||
from aurweb.models.package_keyword import PackageKeyword
|
||||
from aurweb.models.package_relation import PackageRelation
|
||||
from aurweb.models.relation_type import PROVIDES_ID, RelationType
|
||||
from aurweb.models.user import User
|
||||
from aurweb.testing import setup_test_db
|
||||
from aurweb.testing.html import parse_root
|
||||
from aurweb.testing.requests import Request
|
||||
|
||||
|
||||
def package_endpoint(package: Package) -> str:
|
||||
return f"/packages/{package.Name}"
|
||||
|
||||
|
||||
def create_package(pkgname: str, maintainer: User,
|
||||
autocommit: bool = True) -> Package:
|
||||
pkgbase = db.create(PackageBase,
|
||||
Name=pkgname,
|
||||
Maintainer=maintainer,
|
||||
autocommit=False)
|
||||
return db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase,
|
||||
autocommit=autocommit)
|
||||
|
||||
|
||||
def create_package_dep(package: Package, depname: str,
|
||||
dep_type_name: str = "depends",
|
||||
autocommit: bool = True) -> PackageDependency:
|
||||
dep_type = db.query(DependencyType,
|
||||
DependencyType.Name == dep_type_name).first()
|
||||
return db.create(PackageDependency,
|
||||
DependencyType=dep_type,
|
||||
Package=package,
|
||||
DepName=depname,
|
||||
autocommit=autocommit)
|
||||
|
||||
|
||||
def create_package_rel(package: Package,
|
||||
relname: str,
|
||||
autocommit: bool = True) -> PackageRelation:
|
||||
rel_type = db.query(RelationType,
|
||||
RelationType.ID == PROVIDES_ID).first()
|
||||
return db.create(PackageRelation,
|
||||
RelationType=rel_type,
|
||||
Package=package,
|
||||
RelName=relname)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup():
|
||||
setup_test_db(
|
||||
User.__tablename__,
|
||||
Package.__tablename__,
|
||||
PackageBase.__tablename__,
|
||||
PackageDependency.__tablename__,
|
||||
PackageRelation.__tablename__,
|
||||
PackageKeyword.__tablename__,
|
||||
OfficialProvider.__tablename__
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client() -> TestClient:
|
||||
""" Yield a FastAPI TestClient. """
|
||||
yield TestClient(app=asgi.app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user() -> User:
|
||||
""" Yield a user. """
|
||||
account_type = db.query(AccountType, AccountType.ID == USER_ID).first()
|
||||
yield db.create(User, Username="test",
|
||||
Email="test@example.org",
|
||||
Passwd="testPassword",
|
||||
AccountType=account_type)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def maintainer() -> User:
|
||||
""" Yield a specific User used to maintain packages. """
|
||||
account_type = db.query(AccountType, AccountType.ID == USER_ID).first()
|
||||
yield db.create(User, Username="test_maintainer",
|
||||
Email="test_maintainer@example.org",
|
||||
Passwd="testPassword",
|
||||
AccountType=account_type)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package(maintainer: User) -> Package:
|
||||
""" Yield a Package created by user. """
|
||||
pkgbase = db.create(PackageBase,
|
||||
Name="test-package",
|
||||
Maintainer=maintainer)
|
||||
yield db.create(Package,
|
||||
PackageBase=pkgbase,
|
||||
Name=pkgbase.Name)
|
||||
|
||||
|
||||
def test_package_not_found(client: TestClient):
|
||||
with client as request:
|
||||
resp = request.get("/packages/not_found")
|
||||
assert resp.status_code == int(HTTPStatus.NOT_FOUND)
|
||||
|
||||
|
||||
def test_package_official_not_found(client: TestClient, package: Package):
|
||||
""" When a Package has a matching OfficialProvider record, it is not
|
||||
hosted on AUR, but in the official repositories. Getting a package
|
||||
with this kind of record should return a status code 404. """
|
||||
db.create(OfficialProvider,
|
||||
Name=package.Name,
|
||||
Repo="core",
|
||||
Provides=package.Name)
|
||||
|
||||
with client as request:
|
||||
resp = request.get(package_endpoint(package))
|
||||
assert resp.status_code == int(HTTPStatus.NOT_FOUND)
|
||||
|
||||
|
||||
def test_package(client: TestClient, package: Package):
|
||||
""" Test a single /packages/{name} route. """
|
||||
with client as request:
|
||||
|
||||
resp = request.get(package_endpoint(package))
|
||||
assert resp.status_code == int(HTTPStatus.OK)
|
||||
|
||||
root = parse_root(resp.text)
|
||||
h2 = root.find('.//div[@id="pkgdetails"]/h2')
|
||||
|
||||
sections = h2.text.split(":")
|
||||
assert sections[0] == "Package Details"
|
||||
|
||||
name, version = sections[1].lstrip().split(" ")
|
||||
assert name == package.Name
|
||||
version == package.Version
|
||||
|
||||
rows = root.findall('.//table[@id="pkginfo"]//tr')
|
||||
row = rows[1] # Second row is our target.
|
||||
|
||||
pkgbase = row.find("./td/a")
|
||||
assert pkgbase.text.strip() == package.PackageBase.Name
|
||||
|
||||
|
||||
def test_package_comments(client: TestClient, user: User, package: Package):
|
||||
now = (datetime.utcnow().timestamp())
|
||||
comment = db.create(PackageComment, PackageBase=package.PackageBase,
|
||||
User=user, Comments="Test comment", CommentTS=now)
|
||||
|
||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||
with client as request:
|
||||
resp = request.get(package_endpoint(package), cookies=cookies)
|
||||
assert resp.status_code == int(HTTPStatus.OK)
|
||||
|
||||
root = parse_root(resp.text)
|
||||
expected = [
|
||||
comment.Comments
|
||||
]
|
||||
comments = root.xpath('.//div[contains(@class, "package-comments")]'
|
||||
'/div[@class="article-content"]/div/text()')
|
||||
for i, row in enumerate(expected):
|
||||
assert comments[i].strip() == row
|
||||
|
||||
|
||||
def test_package_authenticated(client: TestClient, user: User,
|
||||
package: Package):
|
||||
""" We get the same here for either authenticated or not
|
||||
authenticated. Form inputs are presented to maintainers.
|
||||
This process also occurs when pkgbase.html is rendered. """
|
||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||
with client as request:
|
||||
resp = request.get(package_endpoint(package), cookies=cookies)
|
||||
assert resp.status_code == int(HTTPStatus.OK)
|
||||
|
||||
expected = [
|
||||
"View PKGBUILD",
|
||||
"View Changes",
|
||||
"Download snapshot",
|
||||
"Search wiki",
|
||||
"Flag package out-of-date",
|
||||
"Vote for this package",
|
||||
"Enable notifications",
|
||||
"Submit Request"
|
||||
]
|
||||
for expected_text in expected:
|
||||
assert expected_text in resp.text
|
||||
|
||||
|
||||
def test_package_authenticated_maintainer(client: TestClient,
|
||||
maintainer: User,
|
||||
package: Package):
|
||||
cookies = {"AURSID": maintainer.login(Request(), "testPassword")}
|
||||
with client as request:
|
||||
resp = request.get(package_endpoint(package), cookies=cookies)
|
||||
assert resp.status_code == int(HTTPStatus.OK)
|
||||
|
||||
expected = [
|
||||
"View PKGBUILD",
|
||||
"View Changes",
|
||||
"Download snapshot",
|
||||
"Search wiki",
|
||||
"Flag package out-of-date",
|
||||
"Vote for this package",
|
||||
"Enable notifications",
|
||||
"Manage Co-Maintainers",
|
||||
"Submit Request",
|
||||
"Delete Package",
|
||||
"Merge Package",
|
||||
"Disown Package"
|
||||
]
|
||||
for expected_text in expected:
|
||||
assert expected_text in resp.text
|
||||
|
||||
|
||||
def test_package_dependencies(client: TestClient, maintainer: User,
|
||||
package: Package):
|
||||
# Create a normal dependency of type depends.
|
||||
dep_pkg = create_package("test-dep-1", maintainer, autocommit=False)
|
||||
dep = create_package_dep(package, dep_pkg.Name, autocommit=False)
|
||||
|
||||
# Also, create a makedepends.
|
||||
make_dep_pkg = create_package("test-dep-2", maintainer, autocommit=False)
|
||||
make_dep = create_package_dep(package, make_dep_pkg.Name,
|
||||
dep_type_name="makedepends",
|
||||
autocommit=False)
|
||||
|
||||
# And... a checkdepends!
|
||||
check_dep_pkg = create_package("test-dep-3", maintainer, autocommit=False)
|
||||
check_dep = create_package_dep(package, check_dep_pkg.Name,
|
||||
dep_type_name="checkdepends",
|
||||
autocommit=False)
|
||||
|
||||
# Geez. Just stop. This is optdepends.
|
||||
opt_dep_pkg = create_package("test-dep-4", maintainer, autocommit=False)
|
||||
opt_dep = create_package_dep(package, opt_dep_pkg.Name,
|
||||
dep_type_name="optdepends",
|
||||
autocommit=False)
|
||||
|
||||
broken_dep = create_package_dep(package, "test-dep-5",
|
||||
dep_type_name="depends",
|
||||
autocommit=False)
|
||||
|
||||
# Create an official provider record.
|
||||
db.create(OfficialProvider, Name="test-dep-99",
|
||||
Repo="core", Provides="test-dep-99",
|
||||
autocommit=False)
|
||||
official_dep = create_package_dep(package, "test-dep-99",
|
||||
autocommit=False)
|
||||
|
||||
# Also, create a provider who provides our test-dep-99.
|
||||
provider = create_package("test-provider", maintainer, autocommit=False)
|
||||
create_package_rel(provider, dep.DepName)
|
||||
|
||||
with client as request:
|
||||
resp = request.get(package_endpoint(package))
|
||||
assert resp.status_code == int(HTTPStatus.OK)
|
||||
|
||||
root = parse_root(resp.text)
|
||||
|
||||
expected = [
|
||||
dep.DepName,
|
||||
make_dep.DepName,
|
||||
check_dep.DepName,
|
||||
opt_dep.DepName,
|
||||
official_dep.DepName
|
||||
]
|
||||
pkgdeps = root.findall('.//ul[@id="pkgdepslist"]/li/a')
|
||||
for i, expectation in enumerate(expected):
|
||||
assert pkgdeps[i].text.strip() == expectation
|
||||
|
||||
broken_node = root.find('.//ul[@id="pkgdepslist"]/li/span')
|
||||
assert broken_node.text.strip() == broken_dep.DepName
|
51
test/test_packages_util.py
Normal file
51
test/test_packages_util.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
import pytest
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from aurweb import asgi, db
|
||||
from aurweb.models.account_type import USER_ID, AccountType
|
||||
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.user import User
|
||||
from aurweb.packages import util
|
||||
from aurweb.testing import setup_test_db
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup():
|
||||
setup_test_db(
|
||||
User.__tablename__,
|
||||
Package.__tablename__,
|
||||
PackageBase.__tablename__,
|
||||
OfficialProvider.__tablename__
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def maintainer() -> User:
|
||||
account_type = db.query(AccountType, AccountType.ID == USER_ID).first()
|
||||
yield db.create(User, Username="test_maintainer",
|
||||
Email="test_maintainer@examepl.org",
|
||||
Passwd="testPassword",
|
||||
AccountType=account_type)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package(maintainer: User) -> Package:
|
||||
pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=maintainer)
|
||||
yield db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client() -> TestClient:
|
||||
yield TestClient(app=asgi.app)
|
||||
|
||||
|
||||
def test_package_link(client: TestClient, maintainer: User, package: Package):
|
||||
db.create(OfficialProvider,
|
||||
Name=package.Name,
|
||||
Repo="core",
|
||||
Provides=package.Name)
|
||||
expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}"
|
||||
assert util.package_link(package) == expected
|
15
test/test_templates.py
Normal file
15
test/test_templates.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import pytest
|
||||
|
||||
from aurweb.templates import register_filter
|
||||
|
||||
|
||||
@register_filter("func")
|
||||
def func(): pass
|
||||
|
||||
|
||||
def test_register_filter_exists_key_error():
|
||||
""" Most instances of register_filter are tested through module
|
||||
imports or template renders, so we only test failures here. """
|
||||
with pytest.raises(KeyError):
|
||||
@register_filter("func")
|
||||
def some_func(): pass
|
6
web/html/js/copy.js
Normal file
6
web/html/js/copy.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelector('.copy').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(event.target.text);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue