diff --git a/sstreasury/jinja2/sstreasury/budget_view.html b/sstreasury/jinja2/sstreasury/budget_view.html index 3272b7f..4a279c6 100644 --- a/sstreasury/jinja2/sstreasury/budget_view.html +++ b/sstreasury/jinja2/sstreasury/budget_view.html @@ -259,73 +259,80 @@ {% if revision.state == import('sstreasury.models').BudgetState.ENDORSED.value %}

Committee voting

-

{{ dict(settings.AVAILABLE_APPROVERS)[revision.approver][1] }} votes in favour are required for approval.

- -
+ +

+ {{ 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() %} + + {% endif %} +

+ +
+
+
+
+
In favour ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).count() }})
+ {% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).exists() %} +
+
    + {% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value) %} +
  • {{ vote.voter.first_name }} {{ vote.voter.last_name }}
  • + {% endfor %} +
+
+ {% endif %} +
+ {% if is_latest and revision.can_vote(request.user) %} + + {% endif %} +
+
+
+
+
+
Against ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).count() }})
+ {% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).exists() %} +
+
    + {% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value) %} +
  • {{ vote.voter.first_name }} {{ vote.voter.last_name }}
  • + {% endfor %} +
+
+ {% endif %} +
+ {% if is_latest and revision.can_vote(request.user) %} + + {% endif %} +
+
+
+
+
+
Abstentions ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).count() }})
+ {% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).exists() %} +
+
    + {% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value) %} +
  • {{ vote.voter.first_name }} {{ vote.voter.last_name }}
  • + {% endfor %} +
+
+ {% endif %} +
+ {% if is_latest and revision.can_vote(request.user) %} + + {% endif %} +
+
+
-
-
-
-
In favour ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).count() }})
- {% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).exists() %} -
-
    - {% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value) %} -
  • {{ vote.voter.first_name }} {{ vote.voter.last_name }}
  • - {% endfor %} -
-
- {% endif %} -
- {% if is_latest and revision.can_vote(request.user) %} - - {% endif %} -
-
-
-
-
-
Against ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).count() }})
- {% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).exists() %} -
-
    - {% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value) %} -
  • {{ vote.voter.first_name }} {{ vote.voter.last_name }}
  • - {% endfor %} -
-
- {% endif %} -
- {% if is_latest and revision.can_vote(request.user) %} - - {% endif %} -
-
-
-
-
-
Abstentions ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).count() }})
- {% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).exists() %} -
-
    - {% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value) %} -
  • {{ vote.voter.first_name }} {{ vote.voter.last_name }}
  • - {% endfor %} -
-
- {% endif %} -
- {% if is_latest and revision.can_vote(request.user) %} - - {% endif %} -
-
{% endif %} diff --git a/sstreasury/jinja2/sstreasury/email/budget_vote_reminder.md b/sstreasury/jinja2/sstreasury/email/budget_vote_reminder.md new file mode 100644 index 0000000..e33232a --- /dev/null +++ b/sstreasury/jinja2/sstreasury/email/budget_vote_reminder.md @@ -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}) }} diff --git a/sstreasury/views.py b/sstreasury/views.py index 7f4ffdf..811d06c 100644 --- a/sstreasury/views.py +++ b/sstreasury/views.py @@ -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: