From 49c5a3facf096e9b0a1905e5ee38fe8750a5bb63 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 8 Mar 2022 20:28:09 -0800 Subject: [PATCH 1/2] feat: display stats about total & active TUs on proposals This patch brings in two new features: - when viewing proposal listings, there is a new Statistics section, containing the total and active number of Trusted Users found in the database. - when viewing a proposal directly, the number of active trusted users assigned when the proposal was added is now displayed in the details section. Closes #323 Signed-off-by: Kevin Morris --- aurweb/routers/trusted_user.py | 20 +++++++++ po/aurweb.pot | 4 ++ templates/partials/tu/proposal/details.html | 5 +++ templates/tu/index.html | 16 +++++++ test/test_trusted_user_routes.py | 49 +++++++++++++++++++++ web/html/css/aurweb.css | 13 ++++++ 6 files changed, 107 insertions(+) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index cbe3e47d..3f0eb836 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -2,6 +2,7 @@ import html import typing from http import HTTPStatus +from typing import Any, Dict from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import RedirectResponse, Response @@ -33,6 +34,21 @@ ADDVOTE_SPECIFICS = { } +def populate_trusted_user_counts(context: Dict[str, Any]) -> None: + tu_query = db.query(User).filter( + or_(User.AccountTypeID == TRUSTED_USER_ID, + User.AccountTypeID == TRUSTED_USER_AND_DEV_ID) + ) + context["trusted_user_count"] = tu_query.count() + + # In case any records have a None InactivityTS. + active_tu_query = tu_query.filter( + or_(User.InactivityTS.is_(None), + User.InactivityTS == 0) + ) + context["active_trusted_user_count"] = active_tu_query.count() + + @router.get("/tu") @requires_auth async def trusted_user(request: Request, @@ -40,6 +56,8 @@ async def trusted_user(request: Request, cby: str = "desc", # current by poff: int = 0, # past offset pby: str = "desc"): # past by + """ Proposal listings. """ + if not request.user.has_credential(creds.TU_LIST_VOTES): return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) @@ -102,6 +120,8 @@ async def trusted_user(request: Request, context["current_by_next"] = "asc" if current_by == "desc" else "desc" context["past_by_next"] = "asc" if past_by == "desc" else "desc" + populate_trusted_user_counts(context) + context["q"] = { "coff": current_off, "cby": current_by, diff --git a/po/aurweb.pot b/po/aurweb.pot index bec1b672..e7c632e3 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -2334,3 +2334,7 @@ msgid "This action will close any pending package requests " "related to it. If %sComments%s are omitted, a closure " "comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" diff --git a/templates/partials/tu/proposal/details.html b/templates/partials/tu/proposal/details.html index f7a55148..4cbee9ad 100644 --- a/templates/partials/tu/proposal/details.html +++ b/templates/partials/tu/proposal/details.html @@ -21,6 +21,11 @@ +
+ {{ "Active" | tr }} {{ "Trusted Users" | tr }} {{ "assigned" | tr }}: + {{ voteinfo.ActiveTUs }} +
+ {% set submitter = voteinfo.Submitter.Username %} {% set submitter_uri = "/account/%s" | format(submitter) %} {% set submitter = '%s' | format(submitter_uri, submitter) %} diff --git a/templates/tu/index.html b/templates/tu/index.html index 5060e1f7..4c7a3c35 100644 --- a/templates/tu/index.html +++ b/templates/tu/index.html @@ -1,6 +1,22 @@ {% extends "partials/layout.html" %} {% block pageContent %} +
+

{{ "Statistics" | tr }}

+ + + + + + + + + + + +
{{ "Total" | tr }} {{ "Trusted Users" | tr }}:{{ trusted_user_count }}
{{ "Active" | tr }} {{ "Trusted Users" | tr }}:{{ active_trusted_user_count }}
+
+ {% with table_class = "current-votes", total_votes = current_votes_count, diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index a5c4c5e8..2e7dc193 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -267,6 +267,48 @@ def test_tu_index(client, tu_user): assert int(vote_id.text.strip()) == vote_records[1].ID +def test_tu_stats(client: TestClient, tu_user: User): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == HTTPStatus.OK + + root = parse_root(response.text) + stats = root.xpath('//table[@class="no-width"]')[0] + rows = stats.xpath("./tbody/tr") + + # We have one trusted user. + total = rows[0] + label, count = total.xpath("./td") + assert int(count.text.strip()) == 1 + + # And we have one active TU. + active = rows[1] + label, count = active.xpath("./td") + assert int(count.text.strip()) == 1 + + with db.begin(): + tu_user.InactivityTS = time.utcnow() + + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == HTTPStatus.OK + + root = parse_root(response.text) + stats = root.xpath('//table[@class="no-width"]')[0] + rows = stats.xpath("./tbody/tr") + + # We have one trusted user. + total = rows[0] + label, count = total.xpath("./td") + assert int(count.text.strip()) == 1 + + # But we have no more active TUs. + active = rows[1] + label, count = active.xpath("./td") + assert int(count.text.strip()) == 0 + + def test_tu_index_table_paging(client, tu_user): ts = time.utcnow() @@ -515,6 +557,8 @@ def test_tu_proposal_unauthorized(client: TestClient, user: User, def test_tu_running_proposal(client: TestClient, proposal: Tuple[User, User, TUVoteInfo]): tu_user, user, voteinfo = proposal + with db.begin(): + voteinfo.ActiveTUs = 1 # Initiate an authenticated GET request to /tu/{proposal_id}. proposal_id = voteinfo.ID @@ -536,6 +580,11 @@ def test_tu_running_proposal(client: TestClient, './div[contains(@class, "user")]/strong/a/text()')[0] assert username.strip() == user.Username + active = details.xpath('./div[contains(@class, "field")]')[1] + content = active.text.strip() + assert "Active Trusted Users assigned:" in content + assert "1" in content + submitted = details.xpath( './div[contains(@class, "submitted")]/text()')[0] assert re.match(r'^Submitted: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\) by$', diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index 22b5ac65..59ae7216 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -282,3 +282,16 @@ pre.traceback { white-space: -o-pre-wrap; word-wrap: break-all; } + +/* A text aligning alias. */ +.text-right { + text-align: right; +} + +/* By default, tables use 100% width, which we do not always want. */ +table.no-width { + width: auto; +} +table.no-width > tbody > tr > td { + padding-right: 2px; +} From d7cb04b93dcdad64b6ea8ad081f6dad6387545d0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 8 Mar 2022 20:35:21 -0800 Subject: [PATCH 2/2] upgrade: bump to v6.0.25 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 287152d4..9565b70c 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.24" +AURWEB_VERSION = "v6.0.25" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 7a2f6ca3..8b7a2e93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.24" +version = "v6.0.25" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org"