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;
+}