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 sqlalchemy import Column, Integer
|
||||||
|
|
||||||
|
from aurweb import db
|
||||||
from aurweb.models.declarative import Base
|
from aurweb.models.declarative import Base
|
||||||
|
|
||||||
|
DEPENDS = "depends"
|
||||||
|
MAKEDEPENDS = "makedepends"
|
||||||
|
CHECKDEPENDS = "checkdepends"
|
||||||
|
OPTDEPENDS = "optdepends"
|
||||||
|
|
||||||
|
|
||||||
class DependencyType(Base):
|
class DependencyType(Base):
|
||||||
__tablename__ = "DependencyTypes"
|
__tablename__ = "DependencyTypes"
|
||||||
|
@ -12,3 +18,13 @@ class DependencyType(Base):
|
||||||
|
|
||||||
def __init__(self, Name: str = None):
|
def __init__(self, Name: str = None):
|
||||||
self.Name = Name
|
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
|
from aurweb.models.declarative import Base
|
||||||
|
|
||||||
|
OFFICIAL_BASE = "https://aur.archlinux.org"
|
||||||
|
|
||||||
|
|
||||||
class OfficialProvider(Base):
|
class OfficialProvider(Base):
|
||||||
__tablename__ = "OfficialProviders"
|
__tablename__ = "OfficialProviders"
|
||||||
|
|
|
@ -61,3 +61,12 @@ class PackageDependency(Base):
|
||||||
self.DepDesc = DepDesc
|
self.DepDesc = DepDesc
|
||||||
self.DepCondition = DepCondition
|
self.DepCondition = DepCondition
|
||||||
self.DepArch = DepArch
|
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 sqlalchemy import Column, Integer
|
||||||
|
|
||||||
|
from aurweb import db
|
||||||
from aurweb.models.declarative import Base
|
from aurweb.models.declarative import Base
|
||||||
|
|
||||||
|
CONFLICTS = "conflicts"
|
||||||
|
PROVIDES = "provides"
|
||||||
|
REPLACES = "replaces"
|
||||||
|
|
||||||
|
|
||||||
class RelationType(Base):
|
class RelationType(Base):
|
||||||
__tablename__ = "RelationTypes"
|
__tablename__ = "RelationTypes"
|
||||||
|
@ -12,3 +17,11 @@ class RelationType(Base):
|
||||||
|
|
||||||
def __init__(self, Name: str = None):
|
def __init__(self, Name: str = None):
|
||||||
self.Name = Name
|
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 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 fastapi.responses import RedirectResponse
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
import aurweb.models.package
|
|
||||||
import aurweb.models.package_comment
|
import aurweb.models.package_comment
|
||||||
import aurweb.models.package_keyword
|
import aurweb.models.package_keyword
|
||||||
|
import aurweb.packages.util
|
||||||
|
|
||||||
from aurweb import db
|
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_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
|
from aurweb.templates import make_variable_context, render_template
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/packages/{package}")
|
async def make_single_context(request: Request,
|
||||||
async def package_base(request: Request, package: str):
|
pkgbase: PackageBase) -> Dict[str, Any]:
|
||||||
package = db.query(PackageBase).filter(PackageBase.Name == package).first()
|
""" Make a basic context for package or pkgbase.
|
||||||
if not package:
|
|
||||||
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
|
|
||||||
|
|
||||||
context = await make_variable_context(request, package.Name)
|
:param request: FastAPI request
|
||||||
context["git_clone_uri_anon"] = aurweb.config.get("options", "git_clone_uri_anon")
|
:param pkgbase: PackageBase instance
|
||||||
context["git_clone_uri_priv"] = aurweb.config.get("options", "git_clone_uri_priv")
|
:return: A pkgbase context without specific differences
|
||||||
context["pkgbase"] = package
|
"""
|
||||||
context["packages"] = package.packages.all()
|
context = await make_variable_context(request, pkgbase.Name)
|
||||||
context["packages_count"] = package.packages.count()
|
context["git_clone_uri_anon"] = aurweb.config.get("options",
|
||||||
context["keywords"] = package.keywords.all()
|
"git_clone_uri_anon")
|
||||||
context["comments"] = package.comments.all()
|
context["git_clone_uri_priv"] = aurweb.config.get("options",
|
||||||
context["is_maintainer"] = request.user.is_authenticated() \
|
"git_clone_uri_priv")
|
||||||
and request.user.Username == package.Maintainer.Username
|
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}")
|
@router.get("/packages/{name}")
|
||||||
async def package_base_redirect(request: Request, package: str):
|
async def package(request: Request, name: str) -> Response:
|
||||||
return RedirectResponse(f"/packages/{package}")
|
# 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 copy
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Callable
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
|
@ -40,6 +42,31 @@ env.filters["captcha_cmdline"] = captcha.captcha_cmdline_filter
|
||||||
env.filters["account_url"] = util.account_url
|
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):
|
def make_context(request: Request, title: str, next: str = None):
|
||||||
""" Create a context for a jinja2 TemplateResponse. """
|
""" 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)
|
- comments (list)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated() %}
|
||||||
<div id="generic-form" class="box">
|
<div id="generic-form" class="box">
|
||||||
<h2>Add Comment</h2>
|
<h2>Add Comment</h2>
|
||||||
<form action="/pkgbase/{{ pkgname }}/" method="post">
|
<form action="/pkgbase/{{ pkgname }}/" method="post">
|
||||||
|
@ -31,11 +32,24 @@
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<input type="submit" value="{{ 'Add Comment' | tr }}"/>
|
<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>
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if comments.count() %}
|
||||||
<div class="comments package-comments">
|
<div class="comments package-comments">
|
||||||
<div class="comments-header">
|
<div class="comments-header">
|
||||||
<h3>
|
<h3>
|
||||||
|
@ -43,49 +57,78 @@
|
||||||
<span class="arrow"></span>
|
<span class="arrow"></span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{% for comment in comments %}
|
{% for comment in comments.all() %}
|
||||||
<h4 id="comment-{{ comment.ID }}" class="comment-header">
|
{% include "partials/packages/comment.html" %}
|
||||||
{% 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>
|
|
||||||
{% endfor %}
|
{% 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 optdepends.package_dependencies
|
||||||
assert pkgdep in package.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():
|
def test_package_dependencies_null_package_raises_exception():
|
||||||
from aurweb.db import session
|
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