mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
[FastAPI] Modularize homepage and add side panel
This puts one more toward completion of the homepage overall; we'll need to still implement the authenticated user dashboard after this. Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
parent
9e73936c4e
commit
d9cdd5faef
10 changed files with 500 additions and 102 deletions
|
@ -1,7 +1,10 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_, orm
|
||||||
|
|
||||||
from aurweb import db
|
from aurweb import db
|
||||||
from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider
|
from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider
|
||||||
|
@ -10,6 +13,7 @@ from aurweb.models.package_base import PackageBase
|
||||||
from aurweb.models.package_dependency import PackageDependency
|
from aurweb.models.package_dependency import PackageDependency
|
||||||
from aurweb.models.package_relation import PackageRelation
|
from aurweb.models.package_relation import PackageRelation
|
||||||
from aurweb.models.relation_type import PROVIDES_ID, RelationType
|
from aurweb.models.relation_type import PROVIDES_ID, RelationType
|
||||||
|
from aurweb.redis import redis_connection
|
||||||
from aurweb.templates import register_filter
|
from aurweb.templates import register_filter
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,3 +115,52 @@ def get_pkgbase(name: str) -> PackageBase:
|
||||||
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
|
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
|
||||||
|
|
||||||
return pkgbase
|
return pkgbase
|
||||||
|
|
||||||
|
|
||||||
|
@register_filter("out_of_date")
|
||||||
|
def out_of_date(packages: orm.Query) -> orm.Query:
|
||||||
|
return packages.filter(PackageBase.OutOfDateTS.isnot(None))
|
||||||
|
|
||||||
|
|
||||||
|
def updated_packages(limit: int = 0, cache_ttl: int = 600) -> List[Package]:
|
||||||
|
""" Return a list of valid Package objects ordered by their
|
||||||
|
ModifiedTS column in descending order from cache, after setting
|
||||||
|
the cache when no key yet exists.
|
||||||
|
|
||||||
|
:param limit: Optional record limit
|
||||||
|
:param cache_ttl: Cache expiration time (in seconds)
|
||||||
|
:return: A list of Packages
|
||||||
|
"""
|
||||||
|
redis = redis_connection()
|
||||||
|
packages = redis.get("package_updates")
|
||||||
|
if packages:
|
||||||
|
# If we already have a cache, deserialize it and return.
|
||||||
|
return orjson.loads(packages)
|
||||||
|
|
||||||
|
query = db.query(Package).join(PackageBase).filter(
|
||||||
|
PackageBase.PackagerUID.isnot(None)
|
||||||
|
).order_by(
|
||||||
|
PackageBase.ModifiedTS.desc()
|
||||||
|
)
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
query = query.limit(limit)
|
||||||
|
|
||||||
|
packages = []
|
||||||
|
for pkg in query:
|
||||||
|
# For each Package returned by the query, append a dict
|
||||||
|
# containing Package columns we're interested in.
|
||||||
|
packages.append({
|
||||||
|
"Name": pkg.Name,
|
||||||
|
"Version": pkg.Version,
|
||||||
|
"PackageBase": {
|
||||||
|
"ModifiedTS": pkg.PackageBase.ModifiedTS
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Store the JSON serialization of the package_updates key into Redis.
|
||||||
|
redis.set("package_updates", orjson.dumps(packages))
|
||||||
|
redis.expire("package_updates", cache_ttl)
|
||||||
|
|
||||||
|
# Return the deserialized list of packages.
|
||||||
|
return packages
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
""" AURWeb's primary routing module. Define all routes via @app.app.{get,post}
|
""" AURWeb's primary routing module. Define all routes via @app.app.{get,post}
|
||||||
decorators in some way; more complex routes should be defined in their
|
decorators in some way; more complex routes should be defined in their
|
||||||
own modules and imported here. """
|
own modules and imported here. """
|
||||||
|
from datetime import datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, HTTPException, Request
|
from fastapi import APIRouter, Form, HTTPException, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
from sqlalchemy import and_, or_
|
||||||
|
|
||||||
import aurweb.config
|
import aurweb.config
|
||||||
|
|
||||||
from aurweb import util
|
from aurweb import db, util
|
||||||
|
from aurweb.cache import db_count_cache
|
||||||
|
from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID
|
||||||
|
from aurweb.models.package_base import PackageBase
|
||||||
|
from aurweb.models.user import User
|
||||||
|
from aurweb.packages.util import updated_packages
|
||||||
from aurweb.templates import make_context, render_template
|
from aurweb.templates import make_context, render_template
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
@ -60,6 +67,71 @@ async def index(request: Request):
|
||||||
context = make_context(request, "Home")
|
context = make_context(request, "Home")
|
||||||
context['ssh_fingerprints'] = util.get_ssh_fingerprints()
|
context['ssh_fingerprints'] = util.get_ssh_fingerprints()
|
||||||
|
|
||||||
|
bases = db.query(PackageBase)
|
||||||
|
|
||||||
|
redis = aurweb.redis.redis_connection()
|
||||||
|
stats_expire = 300 # Five minutes.
|
||||||
|
updates_expire = 600 # Ten minutes.
|
||||||
|
|
||||||
|
# Package statistics.
|
||||||
|
query = bases.filter(PackageBase.PackagerUID.isnot(None))
|
||||||
|
context["package_count"] = await db_count_cache(
|
||||||
|
redis, "package_count", query, expire=stats_expire)
|
||||||
|
|
||||||
|
query = bases.filter(
|
||||||
|
and_(PackageBase.MaintainerUID.is_(None),
|
||||||
|
PackageBase.PackagerUID.isnot(None))
|
||||||
|
)
|
||||||
|
context["orphan_count"] = await db_count_cache(
|
||||||
|
redis, "orphan_count", query, expire=stats_expire)
|
||||||
|
|
||||||
|
query = db.query(User)
|
||||||
|
context["user_count"] = await db_count_cache(
|
||||||
|
redis, "user_count", query, expire=stats_expire)
|
||||||
|
|
||||||
|
query = query.filter(
|
||||||
|
or_(User.AccountTypeID == TRUSTED_USER_ID,
|
||||||
|
User.AccountTypeID == TRUSTED_USER_AND_DEV_ID))
|
||||||
|
context["trusted_user_count"] = await db_count_cache(
|
||||||
|
redis, "trusted_user_count", query, expire=stats_expire)
|
||||||
|
|
||||||
|
# Current timestamp.
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
|
||||||
|
seven_days = 86400 * 7 # Seven days worth of seconds.
|
||||||
|
seven_days_ago = now - seven_days
|
||||||
|
|
||||||
|
one_hour = 3600
|
||||||
|
updated = bases.filter(
|
||||||
|
and_(PackageBase.ModifiedTS - PackageBase.SubmittedTS >= one_hour,
|
||||||
|
PackageBase.PackagerUID.isnot(None))
|
||||||
|
)
|
||||||
|
|
||||||
|
query = bases.filter(
|
||||||
|
and_(PackageBase.SubmittedTS >= seven_days_ago,
|
||||||
|
PackageBase.PackagerUID.isnot(None))
|
||||||
|
)
|
||||||
|
context["seven_days_old_added"] = await db_count_cache(
|
||||||
|
redis, "seven_days_old_added", query, expire=stats_expire)
|
||||||
|
|
||||||
|
query = updated.filter(PackageBase.ModifiedTS >= seven_days_ago)
|
||||||
|
context["seven_days_old_updated"] = await db_count_cache(
|
||||||
|
redis, "seven_days_old_updated", query, expire=stats_expire)
|
||||||
|
|
||||||
|
year = seven_days * 52 # Fifty two weeks worth: one year.
|
||||||
|
year_ago = now - year
|
||||||
|
query = updated.filter(PackageBase.ModifiedTS >= year_ago)
|
||||||
|
context["year_old_updated"] = await db_count_cache(
|
||||||
|
redis, "year_old_updated", query, expire=stats_expire)
|
||||||
|
|
||||||
|
query = bases.filter(
|
||||||
|
PackageBase.ModifiedTS - PackageBase.SubmittedTS < 3600)
|
||||||
|
context["never_updated"] = await db_count_cache(
|
||||||
|
redis, "never_updated", query, expire=stats_expire)
|
||||||
|
|
||||||
|
# Get the 15 most recently updated packages.
|
||||||
|
context["package_updates"] = updated_packages(15, updates_expire)
|
||||||
|
|
||||||
return render_template(request, "index.html", context)
|
return render_template(request, "index.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
|
101
templates/home.html
Normal file
101
templates/home.html
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
<div id="intro" class="box">
|
||||||
|
<h2>AUR {% trans %}Home{% endtrans %}</h2>
|
||||||
|
<p>
|
||||||
|
{{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU Guidelines%s for more information."
|
||||||
|
| tr
|
||||||
|
| format('<a href="https://wiki.archlinux.org/title/AUR_User_Guidelines">', "</a>",
|
||||||
|
'<a href="https://wiki.archlinux.org/title/AUR_Trusted_User_Guidelines">', "</a>")
|
||||||
|
| safe
|
||||||
|
}}
|
||||||
|
{{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!"
|
||||||
|
| tr
|
||||||
|
| format("<strong>", "</strong>",
|
||||||
|
'<a href="https://wiki.archlinux.org/title/Arch_Packaging_Standards">',
|
||||||
|
"</a>")
|
||||||
|
| safe
|
||||||
|
}}
|
||||||
|
{% trans %}Remember to vote for your favourite packages!{% endtrans %}
|
||||||
|
{% trans %}Some packages may be provided as binaries in [community].{% endtrans %}
|
||||||
|
</p>
|
||||||
|
<p class="important">
|
||||||
|
{% trans %}DISCLAIMER{% endtrans %}:
|
||||||
|
{% trans %}AUR packages are user produced content. Any use of the provided files is at your own risk.{% endtrans %}
|
||||||
|
</p>
|
||||||
|
<p class="readmore"><a href="https://wiki.archlinux.org/title/AUR">{% trans %}Learn more...{% endtrans %}</a></p>
|
||||||
|
</div>
|
||||||
|
<div id="news">
|
||||||
|
<h3><a>{% trans %}Support{% endtrans %}</a><span class="arrow"></span></h3>
|
||||||
|
<h4>{% trans %}Package Requests{% endtrans %}</h4>
|
||||||
|
<div class="article-content">
|
||||||
|
<p>
|
||||||
|
{{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:"
|
||||||
|
| tr
|
||||||
|
| format("<var>", "</var>")
|
||||||
|
| safe
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><em>{% trans %}Orphan Request{% endtrans %}</em>: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}</li>
|
||||||
|
<li><em>{% trans %}Deletion Request{% endtrans %}</em>: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}</li>
|
||||||
|
<li><em>{% trans %}Merge Request{% endtrans %}</em>: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
{{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests."
|
||||||
|
| tr
|
||||||
|
| format('<a href="https://mailman.archlinux.org/mailman/listinfo/aur-requests">', "</a>")
|
||||||
|
| safe
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h4>{% trans %}Submitting Packages{% endtrans %}</h4>
|
||||||
|
<div class="article-content">
|
||||||
|
<p>
|
||||||
|
{{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details."
|
||||||
|
| tr
|
||||||
|
| format('<a href="https://wiki.archlinux.org/title/Arch_User_Repository#Submitting_packages">', "</a>")
|
||||||
|
| safe
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
{% if ssh_fingerprints %}
|
||||||
|
<p>
|
||||||
|
{% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %}
|
||||||
|
<p>
|
||||||
|
<ul>
|
||||||
|
{% for keytype in ssh_fingerprints %}
|
||||||
|
<li><code>{{ keytype }}</code>: <code>{{ ssh_fingerprints[keytype] }}</code>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<h4>{% trans %}Discussion{% endtrans %}</h4>
|
||||||
|
<div class="article-content">
|
||||||
|
<p>
|
||||||
|
{{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list."
|
||||||
|
| tr
|
||||||
|
| format('<a href="https://mailman.archlinux.org/mailman/listinfo/aur-general">', "</a>",
|
||||||
|
'<a href="https://mailman.archlinux.org/mailman/listinfo/aur-dev">', "</a>")
|
||||||
|
| safe
|
||||||
|
}}
|
||||||
|
<p>
|
||||||
|
</div>
|
||||||
|
<h4>{% trans %}Bug Reporting{% endtrans %}</h4>
|
||||||
|
<div class="article-content">
|
||||||
|
<p>
|
||||||
|
{{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page."
|
||||||
|
| tr
|
||||||
|
| format('<a href="https://bugs.archlinux.org/index.php?project=2">', "</a>",
|
||||||
|
"<strong>", "</strong>")
|
||||||
|
| safe
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap typeahead for the homepage. -->
|
||||||
|
<script type="text/javascript" src="/static/js/typeahead-home.js"></script>
|
||||||
|
|
||||||
|
<!-- Stub inline javascript with a nonce. Used for testing purposes. -->
|
||||||
|
<script type="text/javascript" nonce={{ request.user.nonce }}>
|
||||||
|
function NONEXISTENT() {}
|
||||||
|
NONEXISTENT();
|
||||||
|
</script>
|
|
@ -1,106 +1,15 @@
|
||||||
{% extends 'partials/layout.html' %}
|
{% extends 'partials/layout.html' %}
|
||||||
|
|
||||||
{% block pageContent %}
|
{% block pageContent %}
|
||||||
<div id="intro" class="box">
|
<div id="content-left-wrapper">
|
||||||
<h2>AUR {% trans %}Home{% endtrans %}</h2>
|
<div id="content-left">
|
||||||
<p>
|
{% include 'home.html' %}
|
||||||
{{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU Guidelines%s for more information."
|
|
||||||
| tr
|
|
||||||
| format('<a href="https://wiki.archlinux.org/title/AUR_User_Guidelines">', "</a>",
|
|
||||||
'<a href="https://wiki.archlinux.org/title/AUR_Trusted_User_Guidelines">', "</a>")
|
|
||||||
| safe
|
|
||||||
}}
|
|
||||||
{{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!"
|
|
||||||
| tr
|
|
||||||
| format("<strong>", "</strong>",
|
|
||||||
'<a href="https://wiki.archlinux.org/title/Arch_Packaging_Standards">',
|
|
||||||
"</a>")
|
|
||||||
| safe
|
|
||||||
}}
|
|
||||||
{% trans %}Remember to vote for your favourite packages!{% endtrans %}
|
|
||||||
{% trans %}Some packages may be provided as binaries in [community].{% endtrans %}
|
|
||||||
<p class="important">
|
|
||||||
{% trans %}DISCLAIMER{% endtrans %}:
|
|
||||||
{% trans %}AUR packages are user produced content. Any use of the provided files is at your own risk.{% endtrans %}
|
|
||||||
</p>
|
|
||||||
<p class="readmore"><a href="https://wiki.archlinux.org/title/AUR">{% trans %}Learn more...{% endtrans %}</a></p>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div id="news">
|
|
||||||
<h3><a>{% trans %}Support{% endtrans %}</a><span class="arrow"></span></h3>
|
|
||||||
<h4>{% trans %}Package Requests{% endtrans %}</h4>
|
|
||||||
<div class="article-content">
|
|
||||||
<p>
|
|
||||||
{{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:"
|
|
||||||
| tr
|
|
||||||
| format("<var>", "</var>")
|
|
||||||
| safe
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><em>{% trans %}Orphan Request{% endtrans %}</em>: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}</li>
|
|
||||||
<li><em>{% trans %}Deletion Request{% endtrans %}</em>: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}</li>
|
|
||||||
<li><em>{% trans %}Merge Request{% endtrans %}</em>: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
{{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests."
|
|
||||||
| tr
|
|
||||||
| format('<a href="https://mailman.archlinux.org/mailman/listinfo/aur-requests">', "</a>")
|
|
||||||
| safe
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<h4>{% trans %}Submitting Packages{% endtrans %}</h4>
|
|
||||||
<div class="article-content">
|
|
||||||
<p>
|
|
||||||
{{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details."
|
|
||||||
| tr
|
|
||||||
| format('<a href="https://wiki.archlinux.org/title/Arch_User_Repository#Submitting_packages">', "</a>")
|
|
||||||
| safe
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
{% if ssh_fingerprints %}
|
|
||||||
<p>
|
|
||||||
{% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %}
|
|
||||||
<p>
|
|
||||||
<ul>
|
|
||||||
{% for keytype in ssh_fingerprints %}
|
|
||||||
<li><code>{{ keytype }}</code>: <code>{{ ssh_fingerprints[keytype] }}</code>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<h4>{% trans %}Discussion{% endtrans %}</h4>
|
<div id="content-right">
|
||||||
<div class="article-content">
|
{% include 'partials/packages/widgets/search.html' %}
|
||||||
<p>
|
{% include 'partials/packages/widgets/updates.html' %}
|
||||||
{{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list."
|
{% include 'partials/packages/widgets/statistics.html' %}
|
||||||
| tr
|
|
||||||
| format('<a href="https://mailman.archlinux.org/mailman/listinfo/aur-general">', "</a>",
|
|
||||||
'<a href="https://mailman.archlinux.org/mailman/listinfo/aur-dev">', "</a>")
|
|
||||||
| safe
|
|
||||||
}}
|
|
||||||
<p>
|
|
||||||
</div>
|
</div>
|
||||||
<h4>{% trans %}Bug Reporting{% endtrans %}</h4>
|
|
||||||
<div class="article-content">
|
|
||||||
<p>
|
|
||||||
{{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page."
|
|
||||||
| tr
|
|
||||||
| format('<a href="https://bugs.archlinux.org/index.php?project=2">', "</a>",
|
|
||||||
"<strong>", "</strong>")
|
|
||||||
| safe
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bootstrap typeahead for the homepage. -->
|
|
||||||
<script type="text/javascript" src="/static/js/typeahead-home.js"></script>
|
|
||||||
|
|
||||||
<!-- Stub inline javascript with a nonce. Used for testing purposes. -->
|
|
||||||
<script type="text/javascript" nonce={{ request.user.nonce }}>
|
|
||||||
function NONEXISTENT() {}
|
|
||||||
NONEXISTENT();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
14
templates/partials/packages/widgets/search.html
Normal file
14
templates/partials/packages/widgets/search.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<div id="pkgsearch" class="widget">
|
||||||
|
<form id="pkgsearch-form" method="get" action="/packages/">
|
||||||
|
<fieldset>
|
||||||
|
<label for="pkgsearch-field">{{ "Package Search" | tr }}:</label>
|
||||||
|
<input type="hidden" name="O" value="0" />
|
||||||
|
<input id="pkgsearch-field"
|
||||||
|
type="text"
|
||||||
|
name="K"
|
||||||
|
size="30"
|
||||||
|
maxlength="35"
|
||||||
|
autocomplete="off" />
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
55
templates/partials/packages/widgets/statistics.html
Normal file
55
templates/partials/packages/widgets/statistics.html
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<div id="pkg-stats" class="widget box">
|
||||||
|
<h3>{{ "Statistics" | tr }}</h3>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td class="stat-desc">{{ "Packages" | tr }}</td>
|
||||||
|
<td>{{ package_count }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="stat-desc">{{ "Orphan Packages" | tr }}</td>
|
||||||
|
<td>{{ orphan_count }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="stat-desc">
|
||||||
|
{{ "Packages added in the past 7 days" | tr }}
|
||||||
|
</td>
|
||||||
|
<td>{{ seven_days_old_added }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="stat-desc">
|
||||||
|
{{ "Packages updated in the past 7 days" | tr }}
|
||||||
|
</td>
|
||||||
|
<td>{{ seven_days_old_updated }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="stat-desc">
|
||||||
|
{{ "Packages updated in the past year" | tr }}
|
||||||
|
</td>
|
||||||
|
<td>{{ year_old_updated }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="stat-desc">
|
||||||
|
{{ "Packages never updated" | tr }}
|
||||||
|
</td>
|
||||||
|
<td>{{ never_updated }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="stat-desc">
|
||||||
|
{{ "Registered Users" | tr }}
|
||||||
|
</td>
|
||||||
|
<td>{{ user_count }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="stat-desc">
|
||||||
|
{{ "Trusted Users" | tr }}
|
||||||
|
</td>
|
||||||
|
<td>{{ trusted_user_count }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated() %}
|
||||||
|
<!-- Include "My Statistics" -->
|
||||||
|
{% include 'partials/widgets/statistics.html' %}
|
||||||
|
{% endif %}
|
35
templates/partials/packages/widgets/updates.html
Normal file
35
templates/partials/packages/widgets/updates.html
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<div id="pkg-updates" class="widget box">
|
||||||
|
<h3>
|
||||||
|
{{ "Recent Updates" | tr }}
|
||||||
|
<span class="more">
|
||||||
|
(<a href="/packages/?SB=l&SO=d">{{ "more" | tr }}</a>)
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<a class="rss-icon latest" href="/rss/"
|
||||||
|
title="AUR Latest Packages RSS Feed">
|
||||||
|
<img src="/static/images/rss.svg" alt="RSS Feed" />
|
||||||
|
</a>
|
||||||
|
<a class="rss-icon" href="/rss/modified"
|
||||||
|
title="AUR Modified Packages RSS Feed">
|
||||||
|
<img src="/static/images/rss.svg" alt="RSS Feed" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{% for pkg in package_updates %}
|
||||||
|
<tr>
|
||||||
|
<td class="pkg-name">
|
||||||
|
<a href="/packages/{{ pkg.Name }}">
|
||||||
|
{{ pkg.Name }} {{ pkg.Version }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="pkg-date">
|
||||||
|
{% set modified = pkg.PackageBase.ModifiedTS | dt | as_timezone(timezone) %}
|
||||||
|
{{ modified.strftime("%Y-%m-%d %H:%M") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
27
templates/partials/widgets/statistics.html
Normal file
27
templates/partials/widgets/statistics.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<div id="my-stats" class="widget box">
|
||||||
|
<h3>{{ "My Statistics" | tr }}</h3>
|
||||||
|
|
||||||
|
{% set bases = request.user.maintained_bases %}
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/packages/?SeB=m&K={{ request.user.Username }}">
|
||||||
|
{{ "Packages" | tr }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ bases.count() }}</td>
|
||||||
|
</tr>
|
||||||
|
{% set out_of_date_packages = bases | out_of_date %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/packages/?SeB=m&outdated=on&K={{ request.user.Username }}">
|
||||||
|
{{ "Out of Date" | tr }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ out_of_date_packages.count() }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,13 +1,82 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from aurweb import db
|
||||||
from aurweb.asgi import app
|
from aurweb.asgi import app
|
||||||
|
from aurweb.models.account_type import USER_ID
|
||||||
|
from aurweb.models.package import Package
|
||||||
|
from aurweb.models.package_base import PackageBase
|
||||||
|
from aurweb.models.user import User
|
||||||
|
from aurweb.redis import redis_connection
|
||||||
|
from aurweb.testing import setup_test_db
|
||||||
|
from aurweb.testing.html import parse_root
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup():
|
||||||
|
yield setup_test_db(
|
||||||
|
User.__tablename__,
|
||||||
|
Package.__tablename__,
|
||||||
|
PackageBase.__tablename__
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user():
|
||||||
|
yield db.create(User, Username="test", Email="test@example.org",
|
||||||
|
Passwd="testPassword", AccountTypeID=USER_ID)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def redis():
|
||||||
|
redis = redis_connection()
|
||||||
|
|
||||||
|
def delete_keys():
|
||||||
|
# Cleanup keys if they exist.
|
||||||
|
for key in ("package_count", "orphan_count", "user_count",
|
||||||
|
"trusted_user_count", "seven_days_old_added",
|
||||||
|
"seven_days_old_updated", "year_old_updated",
|
||||||
|
"never_updated", "package_updates"):
|
||||||
|
if redis.get(key) is not None:
|
||||||
|
redis.delete(key)
|
||||||
|
|
||||||
|
delete_keys()
|
||||||
|
yield redis
|
||||||
|
delete_keys()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def packages(user):
|
||||||
|
""" Yield a list of num_packages Package objects maintained by user. """
|
||||||
|
num_packages = 50 # Tunable
|
||||||
|
|
||||||
|
# For i..num_packages, create a package named pkg_{i}.
|
||||||
|
pkgs = []
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
for i in range(num_packages):
|
||||||
|
pkgbase = db.create(PackageBase, Name=f"pkg_{i}",
|
||||||
|
Maintainer=user, Packager=user,
|
||||||
|
autocommit=False, SubmittedTS=now,
|
||||||
|
ModifiedTS=now)
|
||||||
|
pkg = db.create(Package, PackageBase=pkgbase,
|
||||||
|
Name=pkgbase.Name, autocommit=False)
|
||||||
|
pkgs.append(pkg)
|
||||||
|
now += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
yield pkgs
|
||||||
|
|
||||||
|
|
||||||
def test_homepage():
|
def test_homepage():
|
||||||
with client as request:
|
with client as request:
|
||||||
response = request.get("/")
|
response = request.get("/")
|
||||||
|
@ -34,3 +103,49 @@ def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock):
|
||||||
response = request.get("/")
|
response = request.get("/")
|
||||||
|
|
||||||
assert 'The following SSH fingerprints are used for the AUR' not in response.content.decode()
|
assert 'The following SSH fingerprints are used for the AUR' not in response.content.decode()
|
||||||
|
|
||||||
|
|
||||||
|
def test_homepage_stats(redis, packages):
|
||||||
|
with client as request:
|
||||||
|
response = request.get("/")
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
root = parse_root(response.text)
|
||||||
|
|
||||||
|
expectations = [
|
||||||
|
("Packages", r'\d+'),
|
||||||
|
("Orphan Packages", r'\d+'),
|
||||||
|
("Packages added in the past 7 days", r'\d+'),
|
||||||
|
("Packages updated in the past 7 days", r'\d+'),
|
||||||
|
("Packages updated in the past year", r'\d+'),
|
||||||
|
("Packages never updated", r'\d+'),
|
||||||
|
("Registered Users", r'\d+'),
|
||||||
|
("Trusted Users", r'\d+')
|
||||||
|
]
|
||||||
|
|
||||||
|
stats = root.xpath('//div[@id="pkg-stats"]//tr')
|
||||||
|
for i, expected in enumerate(expectations):
|
||||||
|
expected_key, expected_regex = expected
|
||||||
|
key, value = stats[i].xpath('./td')
|
||||||
|
assert key.text.strip() == expected_key
|
||||||
|
assert re.match(expected_regex, value.text.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def test_homepage_updates(redis, packages):
|
||||||
|
with client as request:
|
||||||
|
response = request.get("/")
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
# Run the request a second time to exercise the Redis path.
|
||||||
|
response = request.get("/")
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
root = parse_root(response.text)
|
||||||
|
|
||||||
|
# We expect to see the latest 15 packages, which happens to be
|
||||||
|
# pkg_49 .. pkg_34. So, create a list of expectations using a range
|
||||||
|
# starting at 49, stepping down to 49 - 15, -1 step at a time.
|
||||||
|
expectations = [f"pkg_{i}" for i in range(50 - 1, 50 - 1 - 15, -1)]
|
||||||
|
updates = root.xpath('//div[@id="pkg-updates"]/table/tbody/tr')
|
||||||
|
for i, expected in enumerate(expectations):
|
||||||
|
pkgname = updates[i].xpath('./td/a').pop(0)
|
||||||
|
assert pkgname.text.strip() == expected
|
||||||
|
|
|
@ -9,6 +9,7 @@ from aurweb.models.package import Package
|
||||||
from aurweb.models.package_base import PackageBase
|
from aurweb.models.package_base import PackageBase
|
||||||
from aurweb.models.user import User
|
from aurweb.models.user import User
|
||||||
from aurweb.packages import util
|
from aurweb.packages import util
|
||||||
|
from aurweb.redis import kill_redis
|
||||||
from aurweb.testing import setup_test_db
|
from aurweb.testing import setup_test_db
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,7 +34,8 @@ def maintainer() -> User:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def package(maintainer: User) -> Package:
|
def package(maintainer: User) -> Package:
|
||||||
pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=maintainer)
|
pkgbase = db.create(PackageBase, Name="test-pkg",
|
||||||
|
Packager=maintainer, Maintainer=maintainer)
|
||||||
yield db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase)
|
yield db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase)
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,3 +51,18 @@ def test_package_link(client: TestClient, maintainer: User, package: Package):
|
||||||
Provides=package.Name)
|
Provides=package.Name)
|
||||||
expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}"
|
expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}"
|
||||||
assert util.package_link(package) == expected
|
assert util.package_link(package) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_updated_packages(maintainer: User, package: Package):
|
||||||
|
expected = {
|
||||||
|
"Name": package.Name,
|
||||||
|
"Version": package.Version,
|
||||||
|
"PackageBase": {
|
||||||
|
"ModifiedTS": package.PackageBase.ModifiedTS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kill_redis() # Kill it here to ensure we're on a fake instance.
|
||||||
|
assert util.updated_packages(1, 0) == [expected]
|
||||||
|
assert util.updated_packages(1, 600) == [expected]
|
||||||
|
kill_redis() # Kill it again, in case other tests use a real instance.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue