mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
Merge branch 'pu_tu_addvote' into pu
This commit is contained in:
commit
77d54b5e1b
3 changed files with 264 additions and 16 deletions
|
@ -1,3 +1,6 @@
|
|||
import html
|
||||
import logging
|
||||
import re
|
||||
import typing
|
||||
|
||||
from datetime import datetime
|
||||
|
@ -5,10 +8,10 @@ from http import HTTPStatus
|
|||
from urllib.parse import quote_plus
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, Request
|
||||
from fastapi.responses import Response
|
||||
from fastapi.responses import RedirectResponse, Response
|
||||
from sqlalchemy import and_, or_
|
||||
|
||||
from aurweb import db
|
||||
from aurweb import db, l10n
|
||||
from aurweb.auth import account_type_required, auth_required
|
||||
from aurweb.models.account_type import DEVELOPER, TRUSTED_USER, TRUSTED_USER_AND_DEV
|
||||
from aurweb.models.tu_vote import TUVote
|
||||
|
@ -17,6 +20,7 @@ from aurweb.models.user import User
|
|||
from aurweb.templates import make_context, make_variable_context, render_template
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Some TU route specific constants.
|
||||
ITEMS_PER_PAGE = 10 # Paged table size.
|
||||
|
@ -29,6 +33,17 @@ REQUIRED_TYPES = {
|
|||
TRUSTED_USER_AND_DEV
|
||||
}
|
||||
|
||||
ADDVOTE_SPECIFICS = {
|
||||
# This dict stores a vote duration and quorum for a proposal.
|
||||
# When a proposal is added, duration is added to the current
|
||||
# timestamp.
|
||||
# "addvote_type": (duration, quorum)
|
||||
"add_tu": (7 * 24 * 60 * 60, 0.66),
|
||||
"remove_tu": (7 * 24 * 60 * 60, 0.75),
|
||||
"remove_inactive_tu": (5 * 24 * 60 * 60, 0.66),
|
||||
"bylaws": (7 * 24 * 60 * 60, 0.75)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tu")
|
||||
@auth_required(True, redirect="/")
|
||||
|
@ -174,28 +189,17 @@ async def trusted_user_proposal_post(request: Request,
|
|||
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()
|
||||
|
||||
# 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:
|
||||
elif vote is not None:
|
||||
context["error"] = "You've already voted for this proposal."
|
||||
status_code = HTTPStatus.BAD_REQUEST
|
||||
|
||||
|
@ -218,3 +222,86 @@ async def trusted_user_proposal_post(request: Request,
|
|||
|
||||
context["error"] = "You've already voted for this proposal."
|
||||
return render_proposal(request, context, proposal, voteinfo, voters, vote)
|
||||
|
||||
|
||||
@router.get("/addvote")
|
||||
@auth_required(True)
|
||||
@account_type_required({"Trusted User", "Trusted User & Developer"})
|
||||
async def trusted_user_addvote(request: Request,
|
||||
user: str = str(),
|
||||
type: str = "add_tu",
|
||||
agenda: str = str()):
|
||||
context = await make_variable_context(request, "Add Proposal")
|
||||
|
||||
if type not in ADDVOTE_SPECIFICS:
|
||||
context["error"] = "Invalid type."
|
||||
type = "add_tu" # Default it.
|
||||
|
||||
context["user"] = user
|
||||
context["type"] = type
|
||||
context["agenda"] = agenda
|
||||
|
||||
return render_template(request, "addvote.html", context)
|
||||
|
||||
|
||||
@router.post("/addvote")
|
||||
@auth_required(True)
|
||||
@account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV})
|
||||
async def trusted_user_addvote_post(request: Request,
|
||||
user: str = Form(default=str()),
|
||||
type: str = Form(default=str()),
|
||||
agenda: str = Form(default=str())):
|
||||
# Build a context.
|
||||
context = await make_variable_context(request, "Add Proposal")
|
||||
|
||||
context["type"] = type
|
||||
context["user"] = user
|
||||
context["agenda"] = agenda
|
||||
|
||||
def render_addvote(context, status_code):
|
||||
""" Simplify render_template a bit for this test. """
|
||||
return render_template(request, "addvote.html", context, status_code)
|
||||
|
||||
# Alright, get some database records, if we can.
|
||||
if type != "bylaws":
|
||||
user_record = db.query(User, User.Username == user).first()
|
||||
if user_record is None:
|
||||
context["error"] = "Username does not exist."
|
||||
return render_addvote(context, HTTPStatus.NOT_FOUND)
|
||||
|
||||
voteinfo = db.query(TUVoteInfo, TUVoteInfo.User == user).count()
|
||||
if voteinfo:
|
||||
_ = l10n.get_translator_for_request(request)
|
||||
context["error"] = _(
|
||||
"%s already has proposal running for them.") % (
|
||||
html.escape(user),)
|
||||
return render_addvote(context, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
if type not in ADDVOTE_SPECIFICS:
|
||||
context["error"] = "Invalid type."
|
||||
context["type"] = type = "add_tu" # Default for rendering.
|
||||
return render_addvote(context, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
if not agenda:
|
||||
context["error"] = "Proposal cannot be empty."
|
||||
return render_addvote(context, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
# Gather some mapped constants and the current timestamp.
|
||||
duration, quorum = ADDVOTE_SPECIFICS.get(type)
|
||||
timestamp = int(datetime.utcnow().timestamp())
|
||||
|
||||
# Remove <script> and <style> tags.
|
||||
agenda = re.sub(r'<[/]?script.*>', '', agenda)
|
||||
agenda = re.sub(r'<[/]?style.*>', '', agenda)
|
||||
|
||||
# Create a new TUVoteInfo (proposal)!
|
||||
voteinfo = db.create(TUVoteInfo,
|
||||
User=user,
|
||||
Agenda=agenda,
|
||||
Submitted=timestamp, End=timestamp + duration,
|
||||
Quorum=quorum,
|
||||
Submitter=request.user)
|
||||
|
||||
# Redirect to the new proposal.
|
||||
return RedirectResponse(f"/tu/{voteinfo.ID}",
|
||||
status_code=int(HTTPStatus.SEE_OTHER))
|
||||
|
|
68
templates/addvote.html
Normal file
68
templates/addvote.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{% extends "partials/layout.html" %}
|
||||
|
||||
{% block pageContent %}
|
||||
{% if error %}
|
||||
<p class="pkgoutput error">{{ error | e }}</p></p>
|
||||
{% endif %}
|
||||
|
||||
<div class="box">
|
||||
<h2>{{ "Submit a proposal to vote on." | tr }}</h2>
|
||||
|
||||
<form action="/addvote/" method="post">
|
||||
|
||||
<p class="vote-applicant">
|
||||
<label for="id_user">{{ "Applicant/TU" | tr }}</label>
|
||||
<input type="text" name="user" id="id_user"
|
||||
value="{{ user }}" />
|
||||
{{ "(empty if not applicable)" | tr }}
|
||||
</p>
|
||||
<p class="vote-type">
|
||||
<label for="id_type">{{ "Type" | tr }}</label>
|
||||
<select name="type" id="id_type">
|
||||
<option value="add_tu"
|
||||
{% if "add_tu" == type %}
|
||||
selected
|
||||
{% endif %}
|
||||
>
|
||||
{{ "Addition of a TU" | tr }}
|
||||
</option>
|
||||
<option value="remove_tu"
|
||||
{% if "remove_tu" == type %}
|
||||
selected
|
||||
{% endif %}
|
||||
>
|
||||
{{ "Removal of a TU" | tr }}
|
||||
</option>
|
||||
<option value="remove_inactive_tu"
|
||||
{% if "remove_inactive_tu" == type %}
|
||||
selected
|
||||
{% endif %}
|
||||
>
|
||||
{{ "Removal of a TU (undeclared inactivity)" | tr }}
|
||||
</option>
|
||||
<option value="bylaws"
|
||||
{% if "bylaws" == type %}
|
||||
selected
|
||||
{% endif %}
|
||||
>
|
||||
{{ "Amendment of Bylaws" | tr }}
|
||||
</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p class="vote-agenda">
|
||||
<label for="id_agenda">{{ "Proposal" | tr }}</label><br />
|
||||
<textarea name="agenda"
|
||||
id="id_agenda"
|
||||
rows="15"
|
||||
cols="80">{{ agenda | e }}</textarea>
|
||||
<br />
|
||||
<button type="submit" class="button" value="Submit">
|
||||
{{ "Submit" | tr }}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -729,3 +729,96 @@ def test_tu_proposal_vote_invalid_decision(client, proposal):
|
|||
data=data)
|
||||
assert response.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||
assert response.text == "Invalid 'decision' value."
|
||||
|
||||
|
||||
def test_tu_addvote(client: TestClient, tu_user: User):
|
||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||
with client as request:
|
||||
response = request.get("/addvote", cookies=cookies)
|
||||
assert response.status_code == int(HTTPStatus.OK)
|
||||
|
||||
|
||||
def test_tu_addvote_invalid_type(client: TestClient, tu_user: User):
|
||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||
with client as request:
|
||||
response = request.get("/addvote", params={"type": "faketype"},
|
||||
cookies=cookies)
|
||||
assert response.status_code == int(HTTPStatus.OK)
|
||||
|
||||
root = parse_root(response.text)
|
||||
error = root.xpath('//*[contains(@class, "error")]/text()')[0]
|
||||
assert error.strip() == "Invalid type."
|
||||
|
||||
|
||||
def test_tu_addvote_post(client: TestClient, tu_user: User, user: User):
|
||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||
|
||||
data = {
|
||||
"user": user.Username,
|
||||
"type": "add_tu",
|
||||
"agenda": "Blah"
|
||||
}
|
||||
|
||||
with client as request:
|
||||
response = request.post("/addvote", cookies=cookies, data=data)
|
||||
assert response.status_code == int(HTTPStatus.SEE_OTHER)
|
||||
|
||||
voteinfo = db.query(TUVoteInfo, TUVoteInfo.Agenda == "Blah").first()
|
||||
assert voteinfo is not None
|
||||
|
||||
|
||||
def test_tu_addvote_post_cant_duplicate_username(client: TestClient,
|
||||
tu_user: User, user: User):
|
||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||
|
||||
data = {
|
||||
"user": user.Username,
|
||||
"type": "add_tu",
|
||||
"agenda": "Blah"
|
||||
}
|
||||
|
||||
with client as request:
|
||||
response = request.post("/addvote", cookies=cookies, data=data)
|
||||
assert response.status_code == int(HTTPStatus.SEE_OTHER)
|
||||
|
||||
voteinfo = db.query(TUVoteInfo, TUVoteInfo.Agenda == "Blah").first()
|
||||
assert voteinfo is not None
|
||||
|
||||
with client as request:
|
||||
response = request.post("/addvote", cookies=cookies, data=data)
|
||||
assert response.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||
|
||||
|
||||
def test_tu_addvote_post_invalid_username(client: TestClient, tu_user: User):
|
||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||
data = {"user": "fakeusername"}
|
||||
with client as request:
|
||||
response = request.post("/addvote", cookies=cookies, data=data)
|
||||
assert response.status_code == int(HTTPStatus.NOT_FOUND)
|
||||
|
||||
|
||||
def test_tu_addvote_post_invalid_type(client: TestClient, tu_user: User,
|
||||
user: User):
|
||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||
data = {"user": user.Username}
|
||||
with client as request:
|
||||
response = request.post("/addvote", cookies=cookies, data=data)
|
||||
assert response.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||
|
||||
|
||||
def test_tu_addvote_post_invalid_agenda(client: TestClient,
|
||||
tu_user: User, user: User):
|
||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||
data = {"user": user.Username, "type": "add_tu"}
|
||||
with client as request:
|
||||
response = request.post("/addvote", cookies=cookies, data=data)
|
||||
assert response.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||
|
||||
|
||||
def test_tu_addvote_post_bylaws(client: TestClient, tu_user: User):
|
||||
# Bylaws votes do not need a user specified.
|
||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||
data = {"type": "bylaws", "agenda": "Blah blah!"}
|
||||
with client as request:
|
||||
response = request.post("/addvote", cookies=cookies, data=data)
|
||||
assert response.status_code == int(HTTPStatus.SEE_OTHER)
|
||||
|
|
Loading…
Add table
Reference in a new issue