Verified Commit 49c5a3fa authored by Kevin Morris's avatar Kevin Morris
Browse files

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's avatarKevin Morris <kevr@0cost.org>
parent 0afa07ed
...@@ -2,6 +2,7 @@ import html ...@@ -2,6 +2,7 @@ import html
import typing import typing
from http import HTTPStatus from http import HTTPStatus
from typing import Any, Dict
from fastapi import APIRouter, Form, HTTPException, Request from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import RedirectResponse, Response from fastapi.responses import RedirectResponse, Response
...@@ -33,6 +34,21 @@ ADDVOTE_SPECIFICS = { ...@@ -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") @router.get("/tu")
@requires_auth @requires_auth
async def trusted_user(request: Request, async def trusted_user(request: Request,
...@@ -40,6 +56,8 @@ async def trusted_user(request: Request, ...@@ -40,6 +56,8 @@ async def trusted_user(request: Request,
cby: str = "desc", # current by cby: str = "desc", # current by
poff: int = 0, # past offset poff: int = 0, # past offset
pby: str = "desc"): # past by pby: str = "desc"): # past by
""" Proposal listings. """
if not request.user.has_credential(creds.TU_LIST_VOTES): if not request.user.has_credential(creds.TU_LIST_VOTES):
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
...@@ -102,6 +120,8 @@ async def trusted_user(request: Request, ...@@ -102,6 +120,8 @@ async def trusted_user(request: Request,
context["current_by_next"] = "asc" if current_by == "desc" else "desc" context["current_by_next"] = "asc" if current_by == "desc" else "desc"
context["past_by_next"] = "asc" if past_by == "desc" else "desc" context["past_by_next"] = "asc" if past_by == "desc" else "desc"
populate_trusted_user_counts(context)
context["q"] = { context["q"] = {
"coff": current_off, "coff": current_off,
"cby": current_by, "cby": current_by,
......
...@@ -2334,3 +2334,7 @@ msgid "This action will close any pending package requests " ...@@ -2334,3 +2334,7 @@ msgid "This action will close any pending package requests "
"related to it. If %sComments%s are omitted, a closure " "related to it. If %sComments%s are omitted, a closure "
"comment will be autogenerated." "comment will be autogenerated."
msgstr "" msgstr ""
#: templates/partials/tu/proposal/details.html
msgid "assigned"
msgstr ""
...@@ -21,6 +21,11 @@ ...@@ -21,6 +21,11 @@
</strong> </strong>
</div> </div>
<div class="field">
{{ "Active" | tr }} {{ "Trusted Users" | tr }} {{ "assigned" | tr }}:
{{ voteinfo.ActiveTUs }}
</div>
{% set submitter = voteinfo.Submitter.Username %} {% set submitter = voteinfo.Submitter.Username %}
{% set submitter_uri = "/account/%s" | format(submitter) %} {% set submitter_uri = "/account/%s" | format(submitter) %}
{% set submitter = '<a href="%s">%s</a>' | format(submitter_uri, submitter) %} {% set submitter = '<a href="%s">%s</a>' | format(submitter_uri, submitter) %}
......
{% extends "partials/layout.html" %} {% extends "partials/layout.html" %}
{% block pageContent %} {% 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", with table_class = "current-votes",
total_votes = current_votes_count, total_votes = current_votes_count,
......
...@@ -267,6 +267,48 @@ def test_tu_index(client, tu_user): ...@@ -267,6 +267,48 @@ def test_tu_index(client, tu_user):
assert int(vote_id.text.strip()) == vote_records[1].ID 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): def test_tu_index_table_paging(client, tu_user):
ts = time.utcnow() ts = time.utcnow()
...@@ -515,6 +557,8 @@ def test_tu_proposal_unauthorized(client: TestClient, user: User, ...@@ -515,6 +557,8 @@ def test_tu_proposal_unauthorized(client: TestClient, user: User,
def test_tu_running_proposal(client: TestClient, def test_tu_running_proposal(client: TestClient,
proposal: Tuple[User, User, TUVoteInfo]): proposal: Tuple[User, User, TUVoteInfo]):
tu_user, user, voteinfo = proposal tu_user, user, voteinfo = proposal
with db.begin():
voteinfo.ActiveTUs = 1
# Initiate an authenticated GET request to /tu/{proposal_id}. # Initiate an authenticated GET request to /tu/{proposal_id}.
proposal_id = voteinfo.ID proposal_id = voteinfo.ID
...@@ -536,6 +580,11 @@ def test_tu_running_proposal(client: TestClient, ...@@ -536,6 +580,11 @@ def test_tu_running_proposal(client: TestClient,
'./div[contains(@class, "user")]/strong/a/text()')[0] './div[contains(@class, "user")]/strong/a/text()')[0]
assert username.strip() == user.Username 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( submitted = details.xpath(
'./div[contains(@class, "submitted")]/text()')[0] './div[contains(@class, "submitted")]/text()')[0]
assert re.match(r'^Submitted: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\) by$', assert re.match(r'^Submitted: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\) by$',
......
...@@ -282,3 +282,16 @@ pre.traceback { ...@@ -282,3 +282,16 @@ pre.traceback {
white-space: -o-pre-wrap; white-space: -o-pre-wrap;
word-wrap: break-all; 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;
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment