Implement sending urgent reminder emails to all members of the applicable committee for an endorsed budget

This commit is contained in:
Yingtong Li 2023-05-01 19:26:18 +10:00
parent 490faa8147
commit 8a3a09d8ab
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 91 additions and 67 deletions

View File

@ -259,73 +259,80 @@
{% if revision.state == import('sstreasury.models').BudgetState.ENDORSED.value %}
<h2>Committee voting</h2>
<p>{{ dict(settings.AVAILABLE_APPROVERS)[revision.approver][1] }} votes in favour are required for approval.</p>
<form action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
<p>
{{ dict(settings.AVAILABLE_APPROVERS)[revision.approver][1] }} votes in favour are required for approval.
{% if is_latest and request.user.groups.filter(name='Executive').exists() %}
<button style="margin-left: 1em;" class="ui small primary labeled icon button" type="submit" name="action" value="SendVotingReminders"><i class="envelope icon"></i>Send urgent reminder emails</button>
{% endif %}
</p>
<form class="ui three column grid" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
<div class="ui three column grid">
<div class="column">
<div class="ui fluid card">
<div class="content">
<div class="header">In favour ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).count() }})</div>
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).exists() %}
<div class="description">
<ul style="margin-bottom:0">
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value) %}
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% if is_latest and revision.can_vote(request.user) %}
<button class="ui bottom attached positive button" type="submit" name="action" value="VoteInFavour">
Vote in favour
</button>
{% endif %}
</div>
</div>
<div class="column">
<div class="ui fluid card">
<div class="content">
<div class="header">Against ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).count() }})</div>
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).exists() %}
<div class="description">
<ul style="margin-bottom:0">
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value) %}
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% if is_latest and revision.can_vote(request.user) %}
<button class="ui bottom attached negative button" type="submit" name="action" value="VoteAgainst">
Vote against
</button>
{% endif %}
</div>
</div>
<div class="column">
<div class="ui fluid card">
<div class="content">
<div class="header">Abstentions ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).count() }})</div>
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).exists() %}
<div class="description">
<ul style="margin-bottom:0">
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value) %}
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% if is_latest and revision.can_vote(request.user) %}
<button class="ui bottom attached secondary button" type="submit" name="action" value="VoteAbstain">
Abstain
</button>
{% endif %}
</div>
</div>
</div>
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<div class="column">
<div class="ui fluid card">
<div class="content">
<div class="header">In favour ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).count() }})</div>
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).exists() %}
<div class="description">
<ul style="margin-bottom:0">
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value) %}
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% if is_latest and revision.can_vote(request.user) %}
<button class="ui bottom attached positive button" type="submit" name="action" value="VoteInFavour">
Vote in favour
</button>
{% endif %}
</div>
</div>
<div class="column">
<div class="ui fluid card">
<div class="content">
<div class="header">Against ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).count() }})</div>
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).exists() %}
<div class="description">
<ul style="margin-bottom:0">
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value) %}
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% if is_latest and revision.can_vote(request.user) %}
<button class="ui bottom attached negative button" type="submit" name="action" value="VoteAgainst">
Vote against
</button>
{% endif %}
</div>
</div>
<div class="column">
<div class="ui fluid card">
<div class="content">
<div class="header">Abstentions ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).count() }})</div>
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).exists() %}
<div class="description">
<ul style="margin-bottom:0">
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value) %}
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% if is_latest and revision.can_vote(request.user) %}
<button class="ui bottom attached secondary button" type="submit" name="action" value="VoteAbstain">
Abstain
</button>
{% endif %}
</div>
</div>
</form>
{% endif %}

View File

@ -0,0 +1,3 @@
This is an urgent reminder from {{ requester.first_name }} {{ requester.last_name }} to vote on a budget titled *{{ revision.name }}* (BU-{{ revision.budget.id }}), which has been endorsed by Treasury and is awaiting committee review.
{{ baseurl }}{{ url('budget_view', kwargs={'id': revision.budget.id}) }}

View File

@ -477,6 +477,20 @@ def budget_action(request, budget, revision):
with transaction.atomic():
revision.update_state(request.user, models.BudgetState.CANCELLED)
if 'SendVotingReminders' in actions:
if revision.state != models.BudgetState.ENDORSED.value:
raise PermissionDenied
if not request.user.groups.filter(name='Executive').exists():
# TODO: Make this group configurable
raise PermissionDenied
# Send emails
emailer = Emailer()
for user in User.objects.filter(groups__name=revision.approver):
# Email only if not voted yet
if not revision.budgetvote_set.filter(voter=user).exists():
emailer.send_mail([user.email], 'URGENT action required: {} (BU-{})'.format(revision.name, budget.id), 'sstreasury/email/budget_vote_reminder.md', {'revision': revision, 'requester': request.user})
if 'VoteInFavour' in actions or 'VoteAgainst' in actions or 'VoteAbstain' in actions:
if not revision.can_vote(request.user):
raise PermissionDenied
@ -489,7 +503,7 @@ def budget_action(request, budget, revision):
vote_type = models.BudgetVoteType.ABSTAIN
# Already exists?
if revision.budgetvote_set.filter(is_current=True, voter=request.user, vote_type=vote_type.value):
if revision.budgetvote_set.filter(is_current=True, voter=request.user, vote_type=vote_type.value).exists():
# No need to create new vote
pass
else: