From 490faa81474804d4f7a24ddce907d3796e9cb623 Mon Sep 17 00:00:00 2001 From: Yingtong Li Date: Mon, 1 May 2023 18:47:56 +1000 Subject: [PATCH] Automatically approve budget when reached threshold for approval --- selfserv/settings.example.py | 4 +- sstreasury/jinja2/sstreasury/budget_edit.html | 2 +- sstreasury/jinja2/sstreasury/budget_view.html | 157 +++++++++--------- sstreasury/models.py | 1 + sstreasury/views.py | 22 ++- 5 files changed, 106 insertions(+), 80 deletions(-) diff --git a/selfserv/settings.example.py b/selfserv/settings.example.py index 902036e..329c657 100644 --- a/selfserv/settings.example.py +++ b/selfserv/settings.example.py @@ -34,8 +34,8 @@ PROMO_LOGO_LINK = 'https://example.com' PROMO_GROUPS_MANDATORY = ['All Years'] AVAILABLE_APPROVERS = [ - # Tuples (committee name, description) - ('Committee', 'Management Committee'), + # Tuples (committee name, (description, votes required to approve)) + ('Committee', ('Management Committee', 10)), ] BUDGET_ENABLE_VOTING = True diff --git a/sstreasury/jinja2/sstreasury/budget_edit.html b/sstreasury/jinja2/sstreasury/budget_edit.html index 2a87852..0b3f3e2 100644 --- a/sstreasury/jinja2/sstreasury/budget_edit.html +++ b/sstreasury/jinja2/sstreasury/budget_edit.html @@ -84,7 +84,7 @@
diff --git a/sstreasury/jinja2/sstreasury/budget_view.html b/sstreasury/jinja2/sstreasury/budget_view.html index f378f21..3272b7f 100644 --- a/sstreasury/jinja2/sstreasury/budget_view.html +++ b/sstreasury/jinja2/sstreasury/budget_view.html @@ -115,7 +115,7 @@ Responsible committee - {{ dict(settings.AVAILABLE_APPROVERS)[revision.approver] }} + {{ dict(settings.AVAILABLE_APPROVERS)[revision.approver][0] }} Comments @@ -212,15 +212,16 @@
- {% if item.action == import('sstreasury.models').BudgetAction.CREATE.value %} - {{ item.author.first_name }} {{ item.author.last_name }} created the budget (view) + {{ item.author.first_name }} {{ item.author.last_name }} created the budget (view) {% elif item.action == import('sstreasury.models').BudgetAction.EDIT.value %} - {{ item.author.first_name }} {{ item.author.last_name }} edited the budget (view) + {{ item.author.first_name }} {{ item.author.last_name }} edited the budget (view) {% elif item.action == import('sstreasury.models').BudgetAction.UPDATE_STATE.value %} - {{ item.author.first_name }} {{ item.author.last_name }} changed the state to: {{ item.get_state_display() }} (view) + {{ item.author.first_name }} {{ item.author.last_name }} changed the state to: {{ item.get_state_display() }} (view) + {% elif item.action == import('sstreasury.models').BudgetAction.AUTO_APPROVE.value %} + System changed the state to: {{ item.get_state_display() }} (view) {% else %} - {{ item.author.first_name }} {{ item.author.last_name }} modified the budget (view) + {{ item.author.first_name }} {{ item.author.last_name }} modified the budget (view) {% endif %}
{{ localtime(item.time) }} @@ -253,78 +254,82 @@ {% endif %} {% endfor %}
+ {% endif %} + + {% if revision.state == import('sstreasury.models').BudgetState.ENDORSED.value %} +

Committee voting

- {% if revision.state == import('sstreasury.models').BudgetState.ENDORSED.value %} -

Committee voting

- -
- -
-
-
-
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 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 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 revision.can_vote(request.user) %} - - {% endif %} -
-
-
- {% endif %} +

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

+
+ +
+
+
+
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 %} + + {% if is_latest %} {% if claims is not none %}

Reimbursement claims

diff --git a/sstreasury/models.py b/sstreasury/models.py index 8b7a5ef..3ab704d 100644 --- a/sstreasury/models.py +++ b/sstreasury/models.py @@ -57,6 +57,7 @@ class BudgetAction(DescriptionEnum): CREATE = 5, 'Created' EDIT = 10, 'Edited' UPDATE_STATE = 20, 'Updated state' + AUTO_APPROVE = 30, 'Automatically approved' class BudgetRevision(models.Model): budget = models.ForeignKey(Budget, on_delete=models.CASCADE) diff --git a/sstreasury/views.py b/sstreasury/views.py index e6b3e92..7f4ffdf 100644 --- a/sstreasury/views.py +++ b/sstreasury/views.py @@ -508,7 +508,27 @@ def budget_action(request, budget, revision): vote.vote_type = vote_type.value vote.save() - # TODO: Check for vote threshold + # Check if threshold for automatic approval is reached + votes_in_favour = revision.budgetvote_set.filter(is_current=True, vote_type=models.BudgetVoteType.IN_FAVOUR.value).count() + if votes_in_favour >= dict(settings.AVAILABLE_APPROVERS)[revision.approver][1]: + # Automatically approve + revision.copy() + revision.time = timezone.now() + revision.state = models.BudgetState.APPROVED.value + revision.action = models.BudgetAction.AUTO_APPROVE.value + revision.save() + + # Send emails + users_to_email = set() + + for user in revision.contributors.all(): + users_to_email.add(user.email) + for user in User.objects.filter(groups__name=revision.approver): + users_to_email.add(user.email) + + emailer = Emailer() + for email in users_to_email: + emailer.send_mail([email], 'Budget approved: {} (BU-{})'.format(revision.name, budget.id), 'sstreasury/email/budget_approved.md', {'revision': revision}) return redirect(reverse('budget_view', kwargs={'id': budget.id}))