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 <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2022-03-08 20:28:09 -08:00
parent 0afa07ed3b
commit 49c5a3facf
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
6 changed files with 107 additions and 0 deletions

View file

@ -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,

View file

@ -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 ""

View file

@ -21,6 +21,11 @@
</strong>
</div>
<div class="field">
{{ "Active" | tr }} {{ "Trusted Users" | tr }} {{ "assigned" | tr }}:
{{ voteinfo.ActiveTUs }}
</div>
{% set submitter = voteinfo.Submitter.Username %}
{% set submitter_uri = "/account/%s" | format(submitter) %}
{% set submitter = '<a href="%s">%s</a>' | format(submitter_uri, submitter) %}

View file

@ -1,6 +1,22 @@
{% extends "partials/layout.html" %}
{% block pageContent %}
<div class="box">
<h2>{{ "Statistics" | tr }}</h2>
<table class="no-width">
<tbody>
<tr>
<td class="text-right">{{ "Total" | tr }} {{ "Trusted Users" | tr }}:</td>
<td>{{ trusted_user_count }}</td>
</tr>
<tr>
<td class="text-right">{{ "Active" | tr }} {{ "Trusted Users" | tr }}:</td>
<td>{{ active_trusted_user_count }}</td>
</tr>
</tbody>
</table>
</div>
{%
with table_class = "current-votes",
total_votes = current_votes_count,

View file

@ -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$',

View file

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