mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
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:
parent
0afa07ed3b
commit
49c5a3facf
6 changed files with 107 additions and 0 deletions
|
@ -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 = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
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,
|
||||||
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 "
|
||||||
"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 @@
|
||||||
</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) %}
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
{% 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):
|
||||||
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,
|
||||||
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,
|
||||||
'./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 {
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue