mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
add /tu/{proposal_id} (get, post) routes
This commit ports the `/tu/?id={proposal_id}` PHP routes to FastAPI into two individual GET and POST routes. With this port of the single proposal view and POST logic, several things have changed. - The only parameter used is now `decision`, which must contain `Yes`, `No`, or `Abstain` as a string. When an invalid value is given, a BAD_REQUEST response is returned in plaintext: Invalid 'decision' value. - The `doVote` parameter has been removed. - The details section has been rearranged into a set of divs with specific classes that can be used for testing. CSS has been added to persist the layout with the element changes. - Several errors that can be discovered in the POST path now trigger their own non-200 HTTPStatus codes. Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
parent
83c038a42a
commit
85ba4a33a8
7 changed files with 571 additions and 2 deletions
|
@ -1,7 +1,11 @@
|
||||||
|
import typing
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from http import HTTPStatus
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, Form, HTTPException, Request
|
||||||
|
from fastapi.responses import Response
|
||||||
from sqlalchemy import and_, or_
|
from sqlalchemy import and_, or_
|
||||||
|
|
||||||
from aurweb import db
|
from aurweb import db
|
||||||
|
@ -10,7 +14,7 @@ from aurweb.models.account_type import DEVELOPER, TRUSTED_USER, TRUSTED_USER_AND
|
||||||
from aurweb.models.tu_vote import TUVote
|
from aurweb.models.tu_vote import TUVote
|
||||||
from aurweb.models.tu_voteinfo import TUVoteInfo
|
from aurweb.models.tu_voteinfo import TUVoteInfo
|
||||||
from aurweb.models.user import User
|
from aurweb.models.user import User
|
||||||
from aurweb.templates import make_context, render_template
|
from aurweb.templates import make_context, make_variable_context, render_template
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@ -95,3 +99,122 @@ async def trusted_user(request: Request,
|
||||||
])
|
])
|
||||||
|
|
||||||
return render_template(request, "tu/index.html", context)
|
return render_template(request, "tu/index.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def render_proposal(request: Request,
|
||||||
|
context: dict,
|
||||||
|
proposal: int,
|
||||||
|
voteinfo: TUVoteInfo,
|
||||||
|
voters: typing.Iterable[User],
|
||||||
|
vote: TUVote,
|
||||||
|
status_code: HTTPStatus = HTTPStatus.OK):
|
||||||
|
""" Render a single TU proposal. """
|
||||||
|
context["proposal"] = proposal
|
||||||
|
context["voteinfo"] = voteinfo
|
||||||
|
context["voters"] = voters
|
||||||
|
|
||||||
|
participation = voteinfo.ActiveTUs / voteinfo.total_votes() \
|
||||||
|
if voteinfo.total_votes() else 0
|
||||||
|
context["participation"] = participation
|
||||||
|
|
||||||
|
accepted = (voteinfo.Yes > voteinfo.ActiveTUs / 2) or \
|
||||||
|
(participation > voteinfo.Quorum and voteinfo.Yes > voteinfo.No)
|
||||||
|
context["accepted"] = accepted
|
||||||
|
|
||||||
|
can_vote = voters.filter(TUVote.User == request.user).first() is None
|
||||||
|
context["can_vote"] = can_vote
|
||||||
|
|
||||||
|
if not voteinfo.is_running():
|
||||||
|
context["error"] = "Voting is closed for this proposal."
|
||||||
|
|
||||||
|
context["vote"] = vote
|
||||||
|
context["has_voted"] = vote is not None
|
||||||
|
|
||||||
|
return render_template(request, "tu/show.html", context,
|
||||||
|
status_code=status_code)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tu/{proposal}")
|
||||||
|
@auth_required(True, redirect="/")
|
||||||
|
@account_type_required(REQUIRED_TYPES)
|
||||||
|
async def trusted_user_proposal(request: Request, proposal: int):
|
||||||
|
context = await make_variable_context(request, "Trusted User")
|
||||||
|
proposal = int(proposal)
|
||||||
|
|
||||||
|
voteinfo = db.query(TUVoteInfo, TUVoteInfo.ID == proposal).first()
|
||||||
|
if not voteinfo:
|
||||||
|
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
|
||||||
|
|
||||||
|
voters = db.query(User).join(TUVote).filter(TUVote.VoteID == voteinfo.ID)
|
||||||
|
vote = db.query(TUVote, and_(TUVote.UserID == request.user.ID,
|
||||||
|
TUVote.VoteID == voteinfo.ID)).first()
|
||||||
|
|
||||||
|
if not request.user.is_trusted_user():
|
||||||
|
context["error"] = "Only Trusted Users are allowed to vote."
|
||||||
|
elif voteinfo.User == request.user.Username:
|
||||||
|
context["error"] = "You cannot vote in an proposal about you."
|
||||||
|
elif vote is not None:
|
||||||
|
context["error"] = "You've already voted for this proposal."
|
||||||
|
|
||||||
|
context["vote"] = vote
|
||||||
|
return render_proposal(request, context, proposal, voteinfo, voters, vote)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/tu/{proposal}")
|
||||||
|
@auth_required(True, redirect="/")
|
||||||
|
@account_type_required(REQUIRED_TYPES)
|
||||||
|
async def trusted_user_proposal_post(request: Request,
|
||||||
|
proposal: int,
|
||||||
|
decision: str = Form(...)):
|
||||||
|
context = await make_variable_context(request, "Trusted User")
|
||||||
|
proposal = int(proposal) # Make sure it's an int.
|
||||||
|
|
||||||
|
voteinfo = db.query(TUVoteInfo, TUVoteInfo.ID == proposal).first()
|
||||||
|
if not voteinfo:
|
||||||
|
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
|
||||||
|
|
||||||
|
voters = db.query(User).join(TUVote).filter(TUVote.VoteID == voteinfo.ID)
|
||||||
|
|
||||||
|
# status_code we'll use for responses later.
|
||||||
|
status_code = HTTPStatus.OK
|
||||||
|
|
||||||
|
if not request.user.is_trusted_user():
|
||||||
|
# Test: Create a proposal and view it as a "Developer". It
|
||||||
|
# should give us this error.
|
||||||
|
context["error"] = "Only Trusted Users are allowed to vote."
|
||||||
|
status_code = HTTPStatus.UNAUTHORIZED
|
||||||
|
elif voteinfo.User == request.user.Username:
|
||||||
|
context["error"] = "You cannot vote in an proposal about you."
|
||||||
|
status_code = HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
vote = db.query(TUVote, and_(TUVote.UserID == request.user.ID,
|
||||||
|
TUVote.VoteID == voteinfo.ID)).first()
|
||||||
|
|
||||||
|
if status_code != HTTPStatus.OK:
|
||||||
|
return render_proposal(request, context, proposal,
|
||||||
|
voteinfo, voters, vote,
|
||||||
|
status_code=status_code)
|
||||||
|
|
||||||
|
if vote is not None:
|
||||||
|
context["error"] = "You've already voted for this proposal."
|
||||||
|
status_code = HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
if status_code != HTTPStatus.OK:
|
||||||
|
return render_proposal(request, context, proposal,
|
||||||
|
voteinfo, voters, vote,
|
||||||
|
status_code=status_code)
|
||||||
|
|
||||||
|
if decision in {"Yes", "No", "Abstain"}:
|
||||||
|
# Increment whichever decision was given to us.
|
||||||
|
setattr(voteinfo, decision, getattr(voteinfo, decision) + 1)
|
||||||
|
else:
|
||||||
|
return Response("Invalid 'decision' value.",
|
||||||
|
status_code=int(HTTPStatus.BAD_REQUEST))
|
||||||
|
|
||||||
|
vote = db.create(TUVote, User=request.user, VoteInfo=voteinfo,
|
||||||
|
autocommit=False)
|
||||||
|
voteinfo.ActiveTUs += 1
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
context["error"] = "You've already voted for this proposal."
|
||||||
|
return render_proposal(request, context, proposal, voteinfo, voters, vote)
|
||||||
|
|
106
templates/partials/tu/proposal/details.html
Normal file
106
templates/partials/tu/proposal/details.html
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<h2>{% trans %}Proposal Details{% endtrans %}</h2>
|
||||||
|
|
||||||
|
{% if voteinfo.is_running() %}
|
||||||
|
<p class="vote-running" style="font-weight: bold; color: red">
|
||||||
|
{% trans %}This vote is still running.{% endtrans %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- The margin style here mimics the margin on the old <p> element. -->
|
||||||
|
<div class="proposal details">
|
||||||
|
<div class="field user">
|
||||||
|
{{ "User" | tr }}:
|
||||||
|
<strong>
|
||||||
|
{% if voteinfo.User %}
|
||||||
|
<a href="{{ '/packages/?K=%s&SeB=m' | format(voteinfo.User)}}">
|
||||||
|
{{ voteinfo.User }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set submitted = voteinfo.Submitted | dt | as_timezone(timezone) %}
|
||||||
|
{% set end = voteinfo.End | dt | as_timezone(timezone) %}
|
||||||
|
<div class="field submitted">
|
||||||
|
{{
|
||||||
|
"Submitted: %s by %s" | tr
|
||||||
|
| format(submitted.strftime("%Y-%m-%d %H:%M"),
|
||||||
|
voteinfo.Submitter.Username | e)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field end">
|
||||||
|
{{ "End" | tr }}:
|
||||||
|
<strong>
|
||||||
|
{{ end.strftime("%Y-%m-%d %H:%M") }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not voteinfo.is_running() %}
|
||||||
|
<div class="field result">
|
||||||
|
{{ "Result" | tr }}:
|
||||||
|
{% if not voteinfo.ActiveTUs %}
|
||||||
|
<span>{{ "unknown" | tr }}</span>
|
||||||
|
{% elif accepted %}
|
||||||
|
<span style="color: green; font-weight: bold">
|
||||||
|
{{ "Accepted" | tr }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: red; font-weight: bold">
|
||||||
|
{{ "Rejected" | tr }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="proposal agenda">
|
||||||
|
<p class="field agenda">
|
||||||
|
<!-- The `e` filter manually escapes content. -->
|
||||||
|
{{ voteinfo.Agenda | replace("\n", "<br />\n") | safe | e }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="vote-status">
|
||||||
|
<tr>
|
||||||
|
{% if not voteinfo.is_running() %}
|
||||||
|
<th>{{ "Yes" | tr }}</th>
|
||||||
|
<th>{{ "No" | tr }}</th>
|
||||||
|
<th>{{ "Abstain" | tr }}</th>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<th>{{ "Total" | tr }}</th>
|
||||||
|
<th>{{ "Voted" | tr }}</th>
|
||||||
|
<th>{{ "Participation" | tr }}</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
{% if not voteinfo.is_running() %}
|
||||||
|
<td>{{ voteinfo.Yes }}</td>
|
||||||
|
<td>{{ voteinfo.No }}</td>
|
||||||
|
<td>{{ voteinfo.Abstain }}</td>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<td>{{ voteinfo.total_votes() }}</td>
|
||||||
|
<td>
|
||||||
|
{% if not has_voted %}
|
||||||
|
<span style="color: red; font-weight: bold">
|
||||||
|
{{ "No" | tr }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: green; font-weight: bold">
|
||||||
|
{{ "Yes" | tr }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if voteinfo.ActiveTUs %}
|
||||||
|
{{ (participation * 100) | number_format(2) }}%
|
||||||
|
{% else %}
|
||||||
|
{{ "unknown" | tr }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
14
templates/partials/tu/proposal/form.html
Normal file
14
templates/partials/tu/proposal/form.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<form class="action-form" action="/tu/{{ proposal }}" method="POST">
|
||||||
|
<!-- Translate each button's text but leave the value alone. -->
|
||||||
|
<fieldset>
|
||||||
|
<button type="submit" class="button" name="decision" value="Yes">
|
||||||
|
{{ "Yes" | tr }}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="button" name="decision" value="No">
|
||||||
|
{{ "No" | tr }}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="button" name="decision" value="Abstain">
|
||||||
|
{{ "Abstain" | tr }}
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
10
templates/partials/tu/proposal/voters.html
Normal file
10
templates/partials/tu/proposal/voters.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<h2>{{ "Voters" | tr }}</h2>
|
||||||
|
<ul id="voters">
|
||||||
|
{% for voter in voters %}
|
||||||
|
<li>
|
||||||
|
<a href="/account/{{ voter.Username | urlencode }}">
|
||||||
|
{{ voter.Username | e }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
20
templates/tu/show.html
Normal file
20
templates/tu/show.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "partials/layout.html" %}
|
||||||
|
|
||||||
|
{% block pageContent %}
|
||||||
|
<div class="box">
|
||||||
|
{% include "partials/tu/proposal/details.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
{% include "partials/tu/proposal/voters.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
{% if error %}
|
||||||
|
<span class="status">{{ error | tr }}</span>
|
||||||
|
{% else %}
|
||||||
|
{% include "partials/tu/proposal/form.html" %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -18,6 +18,7 @@ from aurweb.testing import setup_test_db
|
||||||
from aurweb.testing.requests import Request
|
from aurweb.testing.requests import Request
|
||||||
|
|
||||||
DATETIME_REGEX = r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
|
DATETIME_REGEX = r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
|
||||||
|
PARTICIPATION_REGEX = r'^1?[0-9]{2}[%]$' # 0% - 100%
|
||||||
|
|
||||||
|
|
||||||
def parse_root(html):
|
def parse_root(html):
|
||||||
|
@ -103,6 +104,26 @@ def user():
|
||||||
AccountType=user_type)
|
AccountType=user_type)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def proposal(tu_user):
|
||||||
|
ts = int(datetime.utcnow().timestamp())
|
||||||
|
agenda = "Test proposal."
|
||||||
|
start = ts - 5
|
||||||
|
end = ts + 1000
|
||||||
|
|
||||||
|
user_type = db.query(AccountType,
|
||||||
|
AccountType.AccountType == "User").first()
|
||||||
|
user = db.create(User, Username="test", Email="test@example.org",
|
||||||
|
RealName="Test User", Passwd="testPassword",
|
||||||
|
AccountType=user_type)
|
||||||
|
|
||||||
|
voteinfo = db.create(TUVoteInfo,
|
||||||
|
Agenda=agenda, Quorum=0.0,
|
||||||
|
User=user.Username, Submitter=tu_user,
|
||||||
|
Submitted=start, End=end)
|
||||||
|
yield (tu_user, user, voteinfo)
|
||||||
|
|
||||||
|
|
||||||
def test_tu_index_guest(client):
|
def test_tu_index_guest(client):
|
||||||
with client as request:
|
with client as request:
|
||||||
response = request.get("/tu", allow_redirects=False)
|
response = request.get("/tu", allow_redirects=False)
|
||||||
|
@ -441,3 +462,270 @@ def test_tu_index_last_votes(client, tu_user, user):
|
||||||
|
|
||||||
assert user.text.strip() == tu_user.Username
|
assert user.text.strip() == tu_user.Username
|
||||||
assert int(vote_id.text.strip()) == voteinfo.ID
|
assert int(vote_id.text.strip()) == voteinfo.ID
|
||||||
|
|
||||||
|
|
||||||
|
def test_tu_proposal_not_found(client, tu_user):
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
response = request.get("/tu", params={"id": 1}, cookies=cookies)
|
||||||
|
assert response.status_code == int(HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tu_running_proposal(client, proposal):
|
||||||
|
tu_user, user, voteinfo = proposal
|
||||||
|
|
||||||
|
# Initiate an authenticated GET request to /tu/{proposal_id}.
|
||||||
|
proposal_id = voteinfo.ID
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
response = request.get(f"/tu/{proposal_id}", cookies=cookies)
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
# Alright, now let's continue on to verifying some markup.
|
||||||
|
# First, let's verify that the proposal details match.
|
||||||
|
root = parse_root(response.text)
|
||||||
|
details = root.xpath('//div[@class="proposal details"]')[0]
|
||||||
|
|
||||||
|
vote_running = root.xpath('//p[contains(@class, "vote-running")]')[0]
|
||||||
|
assert vote_running.text.strip() == "This vote is still running."
|
||||||
|
|
||||||
|
# Verify User field.
|
||||||
|
username = details.xpath(
|
||||||
|
'./div[contains(@class, "user")]/strong/a/text()')[0]
|
||||||
|
assert username.strip() == user.Username
|
||||||
|
|
||||||
|
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 .+$',
|
||||||
|
submitted.strip()) is not None
|
||||||
|
|
||||||
|
end = details.xpath('./div[contains(@class, "end")]')[0]
|
||||||
|
end_label = end.xpath("./text()")[0]
|
||||||
|
assert end_label.strip() == "End:"
|
||||||
|
|
||||||
|
end_datetime = end.xpath("./strong/text()")[0]
|
||||||
|
assert re.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
|
||||||
|
end_datetime.strip()) is not None
|
||||||
|
|
||||||
|
# We have not voted yet. Assert that our voting form is shown.
|
||||||
|
form = root.xpath('//form[contains(@class, "action-form")]')[0]
|
||||||
|
fields = form.xpath("./fieldset")[0]
|
||||||
|
buttons = fields.xpath('./button[@name="decision"]')
|
||||||
|
assert len(buttons) == 3
|
||||||
|
|
||||||
|
# Check the button names and values.
|
||||||
|
yes, no, abstain = buttons
|
||||||
|
|
||||||
|
# Yes
|
||||||
|
assert yes.attrib["name"] == "decision"
|
||||||
|
assert yes.attrib["value"] == "Yes"
|
||||||
|
|
||||||
|
# No
|
||||||
|
assert no.attrib["name"] == "decision"
|
||||||
|
assert no.attrib["value"] == "No"
|
||||||
|
|
||||||
|
# Abstain
|
||||||
|
assert abstain.attrib["name"] == "decision"
|
||||||
|
assert abstain.attrib["value"] == "Abstain"
|
||||||
|
|
||||||
|
# Create a vote.
|
||||||
|
db.create(TUVote, VoteInfo=voteinfo, User=tu_user)
|
||||||
|
voteinfo.ActiveTUs += 1
|
||||||
|
voteinfo.Yes += 1
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Make another request now that we've voted.
|
||||||
|
with client as request:
|
||||||
|
response = request.get(
|
||||||
|
"/tu", params={"id": voteinfo.ID}, cookies=cookies)
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
# Parse our new root.
|
||||||
|
root = parse_root(response.text)
|
||||||
|
|
||||||
|
# Check that we no longer have a voting form.
|
||||||
|
form = root.xpath('//form[contains(@class, "action-form")]')
|
||||||
|
assert not form
|
||||||
|
|
||||||
|
# Check that we're told we've voted.
|
||||||
|
status = root.xpath('//span[contains(@class, "status")]/text()')[0]
|
||||||
|
assert status == "You've already voted for this proposal."
|
||||||
|
|
||||||
|
|
||||||
|
def test_tu_ended_proposal(client, proposal):
|
||||||
|
tu_user, user, voteinfo = proposal
|
||||||
|
|
||||||
|
ts = int(datetime.utcnow().timestamp())
|
||||||
|
voteinfo.End = ts - 5 # 5 seconds ago.
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Initiate an authenticated GET request to /tu/{proposal_id}.
|
||||||
|
proposal_id = voteinfo.ID
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
response = request.get(f"/tu/{proposal_id}", cookies=cookies)
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
# Alright, now let's continue on to verifying some markup.
|
||||||
|
# First, let's verify that the proposal details match.
|
||||||
|
root = parse_root(response.text)
|
||||||
|
details = root.xpath('//div[@class="proposal details"]')[0]
|
||||||
|
|
||||||
|
vote_running = root.xpath('//p[contains(@class, "vote-running")]')
|
||||||
|
assert not vote_running
|
||||||
|
|
||||||
|
result_node = details.xpath('./div[contains(@class, "result")]')[0]
|
||||||
|
result_label = result_node.xpath("./text()")[0]
|
||||||
|
assert result_label.strip() == "Result:"
|
||||||
|
|
||||||
|
result = result_node.xpath("./span/text()")[0]
|
||||||
|
assert result.strip() == "unknown"
|
||||||
|
|
||||||
|
# Check that voting has ended.
|
||||||
|
form = root.xpath('//form[contains(@class, "action-form")]')
|
||||||
|
assert not form
|
||||||
|
|
||||||
|
# We should see a status about it.
|
||||||
|
status = root.xpath('//span[contains(@class, "status")]/text()')[0]
|
||||||
|
assert status == "Voting is closed for this proposal."
|
||||||
|
|
||||||
|
|
||||||
|
def test_tu_proposal_vote_not_found(client, tu_user):
|
||||||
|
""" Test POST request to a missing vote. """
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
data = {"decision": "Yes"}
|
||||||
|
response = request.post("/tu/1", cookies=cookies,
|
||||||
|
data=data, allow_redirects=False)
|
||||||
|
assert response.status_code == int(HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tu_proposal_vote(client, proposal):
|
||||||
|
tu_user, user, voteinfo = proposal
|
||||||
|
|
||||||
|
# Store the current related values.
|
||||||
|
yes = voteinfo.Yes
|
||||||
|
active_tus = voteinfo.ActiveTUs
|
||||||
|
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
data = {"decision": "Yes"}
|
||||||
|
response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies,
|
||||||
|
data=data)
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
# Check that the proposal record got updated.
|
||||||
|
assert voteinfo.Yes == yes + 1
|
||||||
|
assert voteinfo.ActiveTUs == active_tus + 1
|
||||||
|
|
||||||
|
# Check that the new TUVote exists.
|
||||||
|
vote = db.query(TUVote, TUVote.VoteInfo == voteinfo,
|
||||||
|
TUVote.User == tu_user).first()
|
||||||
|
assert vote is not None
|
||||||
|
|
||||||
|
root = parse_root(response.text)
|
||||||
|
|
||||||
|
# Check that we're told we've voted.
|
||||||
|
status = root.xpath('//span[contains(@class, "status")]/text()')[0]
|
||||||
|
assert status == "You've already voted for this proposal."
|
||||||
|
|
||||||
|
|
||||||
|
def test_tu_proposal_vote_unauthorized(client, proposal):
|
||||||
|
tu_user, user, voteinfo = proposal
|
||||||
|
|
||||||
|
dev_type = db.query(AccountType,
|
||||||
|
AccountType.AccountType == "Developer").first()
|
||||||
|
tu_user.AccountType = dev_type
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
data = {"decision": "Yes"}
|
||||||
|
response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies,
|
||||||
|
data=data, allow_redirects=False)
|
||||||
|
assert response.status_code == int(HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
|
root = parse_root(response.text)
|
||||||
|
status = root.xpath('//span[contains(@class, "status")]/text()')[0]
|
||||||
|
assert status == "Only Trusted Users are allowed to vote."
|
||||||
|
|
||||||
|
with client as request:
|
||||||
|
data = {"decision": "Yes"}
|
||||||
|
response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies,
|
||||||
|
data=data, allow_redirects=False)
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
root = parse_root(response.text)
|
||||||
|
status = root.xpath('//span[contains(@class, "status")]/text()')[0]
|
||||||
|
assert status == "Only Trusted Users are allowed to vote."
|
||||||
|
|
||||||
|
|
||||||
|
def test_tu_proposal_vote_cant_self_vote(client, proposal):
|
||||||
|
tu_user, user, voteinfo = proposal
|
||||||
|
|
||||||
|
# Update voteinfo.User.
|
||||||
|
voteinfo.User = tu_user.Username
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
data = {"decision": "Yes"}
|
||||||
|
response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies,
|
||||||
|
data=data, allow_redirects=False)
|
||||||
|
assert response.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
root = parse_root(response.text)
|
||||||
|
status = root.xpath('//span[contains(@class, "status")]/text()')[0]
|
||||||
|
assert status == "You cannot vote in an proposal about you."
|
||||||
|
|
||||||
|
with client as request:
|
||||||
|
data = {"decision": "Yes"}
|
||||||
|
response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies,
|
||||||
|
data=data, allow_redirects=False)
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
root = parse_root(response.text)
|
||||||
|
status = root.xpath('//span[contains(@class, "status")]/text()')[0]
|
||||||
|
assert status == "You cannot vote in an proposal about you."
|
||||||
|
|
||||||
|
|
||||||
|
def test_tu_proposal_vote_already_voted(client, proposal):
|
||||||
|
tu_user, user, voteinfo = proposal
|
||||||
|
|
||||||
|
db.create(TUVote, VoteInfo=voteinfo, User=tu_user)
|
||||||
|
voteinfo.Yes += 1
|
||||||
|
voteinfo.ActiveTUs += 1
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
data = {"decision": "Yes"}
|
||||||
|
response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies,
|
||||||
|
data=data, allow_redirects=False)
|
||||||
|
assert response.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
root = parse_root(response.text)
|
||||||
|
status = root.xpath('//span[contains(@class, "status")]/text()')[0]
|
||||||
|
assert status == "You've already voted for this proposal."
|
||||||
|
|
||||||
|
with client as request:
|
||||||
|
data = {"decision": "Yes"}
|
||||||
|
response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies,
|
||||||
|
data=data, allow_redirects=False)
|
||||||
|
assert response.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
root = parse_root(response.text)
|
||||||
|
status = root.xpath('//span[contains(@class, "status")]/text()')[0]
|
||||||
|
assert status == "You've already voted for this proposal."
|
||||||
|
|
||||||
|
|
||||||
|
def test_tu_proposal_vote_invalid_decision(client, proposal):
|
||||||
|
tu_user, user, voteinfo = proposal
|
||||||
|
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
data = {"decision": "EVIL"}
|
||||||
|
response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies,
|
||||||
|
data=data)
|
||||||
|
assert response.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||||
|
assert response.text == "Invalid 'decision' value."
|
||||||
|
|
|
@ -204,3 +204,11 @@ label.confirmation,
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: height 1s;
|
transition: height 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.proposal.details {
|
||||||
|
margin: .33em 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="submit"] {
|
||||||
|
padding: 0 0.6em;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue