From 69a7c052c157a9951542c52859ef14caefe5f465 Mon Sep 17 00:00:00 2001 From: Yingtong Li Date: Thu, 20 Jun 2019 01:06:24 +1000 Subject: [PATCH] Treasury endorsement/return of budgets --- sstreasury/jinja2/sstreasury/budget_list.html | 8 +- sstreasury/jinja2/sstreasury/budget_view.html | 25 +++- .../jinja2/sstreasury/email/endorsed.md | 3 + .../jinja2/sstreasury/email/returned.md | 3 + sstreasury/views.py | 121 ++++++++++++++---- 5 files changed, 128 insertions(+), 32 deletions(-) create mode 100644 sstreasury/jinja2/sstreasury/email/endorsed.md create mode 100644 sstreasury/jinja2/sstreasury/email/returned.md diff --git a/sstreasury/jinja2/sstreasury/budget_list.html b/sstreasury/jinja2/sstreasury/budget_list.html index 54cef10..b46e00f 100644 --- a/sstreasury/jinja2/sstreasury/budget_list.html +++ b/sstreasury/jinja2/sstreasury/budget_list.html @@ -59,17 +59,13 @@ {% if budgets_open %}

Open budgets

- {% for budget in budgets_open %} - {{ budget.name }} - {% endfor %} + {{ listbudgets(budgets_open) }} {% endif %} {% if budgets_closed %}

Closed budgets

- {% for budget in budgets_closed %} - {{ budget.name }} - {% endfor %} + {{ listbudgets(budgets_closed) }} {% endif %} {% endblock %} diff --git a/sstreasury/jinja2/sstreasury/budget_view.html b/sstreasury/jinja2/sstreasury/budget_view.html index a09d1c6..deb9106 100644 --- a/sstreasury/jinja2/sstreasury/budget_view.html +++ b/sstreasury/jinja2/sstreasury/budget_view.html @@ -29,9 +29,24 @@ {% if revision.state == import('sstreasury.models').BudgetState.DRAFT.value or revision.state == import('sstreasury.models').BudgetState.RESUBMIT.value %} + {% elif revision.state == import('sstreasury.models').BudgetState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %} + + + + {% elif revision.state == import('sstreasury.models').BudgetState.ENDORSED.value and request.user.groups.filter(name='Secretary').exists() %} + {# TODO #} + {% elif revision.state == import('sstreasury.models').BudgetState.APPROVED.value %} + {# Blank #} + {% else %} + + {% endif %} + + {% if revision.state == import('sstreasury.models').BudgetState.DRAFT.value or revision.state == import('sstreasury.models').BudgetState.RESUBMIT.value or (revision.state != import('sstreasury.models').BudgetState.APPROVED.value and (request.user.groups.filter(name='Treasury').exists() or request.user.groups.filter(name='Secretary').exists())) %} Edit - {% elif revision.state != import('sstreasury.models').BudgetState.APPROVED.value %} - + {% else %} +
+

This budget has been submitted and is now awaiting approval. If you wish to edit this budget, you must first withdraw it. This will revert the budget to a draft.

+
{% endif %} @@ -126,6 +141,12 @@ + + {% if revision.state == import('sstreasury.models').BudgetState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %} + + + + {% endif %}
diff --git a/sstreasury/jinja2/sstreasury/email/endorsed.md b/sstreasury/jinja2/sstreasury/email/endorsed.md new file mode 100644 index 0000000..1003ce0 --- /dev/null +++ b/sstreasury/jinja2/sstreasury/email/endorsed.md @@ -0,0 +1,3 @@ +Your budget titled *{{ revision.name }}* has been reviewed and endorsed by Treasury, and referred to the committee. The committee will determine whether or not to approve the budget at its next meeting. + +{{ baseurl }}{{ url('budget_view', kwargs={'id': revision.budget.id}) }} diff --git a/sstreasury/jinja2/sstreasury/email/returned.md b/sstreasury/jinja2/sstreasury/email/returned.md new file mode 100644 index 0000000..7126511 --- /dev/null +++ b/sstreasury/jinja2/sstreasury/email/returned.md @@ -0,0 +1,3 @@ +Your budget titled *{{ revision.name }}* has been reviewed by Treasury and returned to you for re-drafting. You should make any requested changes and resubmit the budget. + +{{ baseurl }}{{ url('budget_view', kwargs={'id': revision.budget.id}) }} diff --git a/sstreasury/views.py b/sstreasury/views.py index 2e9a54e..0b06ed4 100644 --- a/sstreasury/views.py +++ b/sstreasury/views.py @@ -28,6 +28,7 @@ from django.views import generic from . import models from ssmain.email import Emailer +import functools import itertools import json @@ -44,12 +45,24 @@ def budget_list(request): for budget in models.Budget.objects.all(): revision = budget.budgetrevision_set.reverse()[0] state = models.BudgetState(revision.state) - if state in [models.BudgetState.DRAFT, models.BudgetState.RESUBMIT]: - budgets_action.append(revision) - elif state in [models.BudgetState.AWAIT_REVIEW, models.BudgetState.ENDORSED]: - budgets_open.append(revision) + + if request.user.groups.filter(name='Treasury').exists(): + if state == models.BudgetState.AWAIT_REVIEW: + budgets_action.append(revision) + elif state != models.BudgetState.APPROVED: + budgets_open.append(revision) + else: + budgets_closed.append(revision) else: - budgets_closed.append(revision) + if request.user not in revision.contributors.all(): + continue + + if state in [models.BudgetState.DRAFT, models.BudgetState.RESUBMIT]: + budgets_action.append(revision) + elif state in [models.BudgetState.AWAIT_REVIEW, models.BudgetState.ENDORSED]: + budgets_open.append(revision) + else: + budgets_closed.append(revision) return render(request, 'sstreasury/budget_list.html', { 'budgets_action': budgets_action, @@ -131,15 +144,46 @@ def budget_new(request): 'contributors': request.user.email }) -@login_required -def budget_edit(request, id): - if request.method == 'POST': +def uses_budget(viewfunc): + @functools.wraps(viewfunc) + def func(request, id): budget = models.Budget.objects.get(id=id) revision = budget.budgetrevision_set.reverse()[0] - + return viewfunc(request, budget, revision) + return func + +def budget_viewable(viewfunc): + @functools.wraps(viewfunc) + def func(request, budget, revision): if request.user not in revision.contributors.all(): + if not request.user.groups.filter(name='Treasury').exists(): + raise PermissionDenied + + return viewfunc(request, budget, revision) + return func + +def budget_editable(viewfunc): + @functools.wraps(viewfunc) + def func(request, budget, revision): + if revision.state == models.BudgetState.APPROVED.value: raise PermissionDenied + if revision.state != models.BudgetState.DRAFT.value and revision.state != models.BudgetState.RESUBMIT.value: + if not request.user.groups.filter(name='Treasury').exists(): + raise PermissionDenied + + if request.user not in revision.contributors.all(): + if not request.user.groups.filter(name='Treasury').exists(): + raise PermissionDenied + + return viewfunc(request, budget, revision) + return func + +@login_required +@uses_budget +@budget_editable +def budget_edit(request, budget, revision): + if request.method == 'POST': if request.POST['submit'] == 'Delete': budget.delete() return redirect(reverse('budget_list')) @@ -156,23 +200,18 @@ def budget_edit(request, id): else: return redirect(reverse('budget_edit', kwargs={'id': budget.id})) else: - budget = models.Budget.objects.get(id=id) - revision = budget.budgetrevision_set.reverse()[0] - return render(request, 'sstreasury/budget_edit.html', { 'revision': revision, 'contributors': '\n'.join(revision.contributors.all().values_list('email', flat=True)) }) @login_required -def budget_action(request, id): - budget = models.Budget.objects.get(id=id) - revision = budget.budgetrevision_set.reverse()[0] +@uses_budget +@budget_editable +def budget_action(request, budget, revision): + actions = request.POST['action'].split(',') - if request.user not in revision.contributors.all(): - raise PermissionDenied - - if request.POST['action'] == 'Comment': + if 'Comment' in actions and request.POST.get('comment', None): with transaction.atomic(): comment = models.BudgetComment() comment.budget = budget @@ -189,12 +228,11 @@ def budget_action(request, id): if user != request.user: emailer.send_mail([user.email], 'New comment on budget: {}'.format(revision.name), 'sstreasury/email/commented.md', {'revision': revision, 'comment': comment}) - if request.POST['action'] == 'Submit': + if 'Submit' in actions: if revision.state != models.BudgetState.DRAFT.value and revision.state != models.BudgetState.RESUBMIT.value: raise PermissionDenied with transaction.atomic(): - # Copy revision revision.copy() revision.author = request.user revision.time = timezone.now() @@ -204,15 +242,14 @@ def budget_action(request, id): emailer = Emailer() for user in User.objects.filter(groups__name='Treasury'): - emailer.send_mail([user.email], 'Budget submitted: {}'.format(revision.name), 'sstreasury/email/submitted_treasurer.md', {'revision': revision}) + emailer.send_mail([user.email], 'Action required: Budget submitted: {}'.format(revision.name), 'sstreasury/email/submitted_treasurer.md', {'revision': revision}) for user in revision.contributors.all(): emailer.send_mail([user.email], 'Budget submitted: {}'.format(revision.name), 'sstreasury/email/submitted_drafter.md', {'revision': revision}) - if request.POST['action'] == 'Withdraw': + if 'Withdraw' in actions: if revision.state == models.BudgetState.DRAFT.value or revision.state == models.BudgetState.RESUBMIT.value or revision.state == models.BudgetState.APPROVED.value: raise PermissionDenied - # Copy revision revision.copy() revision.author = request.user revision.time = timezone.now() @@ -220,4 +257,40 @@ def budget_action(request, id): revision.action = models.BudgetAction.UPDATE_STATE.value revision.save() + if 'Endorse' in actions: + if revision.state != models.BudgetState.AWAIT_REVIEW.value: + raise PermissionDenied + if not request.user.groups.filter(name='Treasury').exists(): + raise PermissionDenied + + with transaction.atomic(): + revision.copy() + revision.author = request.user + revision.time = timezone.now() + revision.state = models.BudgetState.ENDORSED.value + revision.action = models.BudgetAction.UPDATE_STATE.value + revision.save() + + emailer = Emailer() + for user in revision.contributors.all(): + emailer.send_mail([user.email], 'Budget endorsed, awaiting committee approval: {}'.format(revision.name), 'sstreasury/email/endorsed.md', {'revision': revision}) + + if 'Return' in actions: + if revision.state != models.BudgetState.AWAIT_REVIEW.value: + raise PermissionDenied + if not request.user.groups.filter(name='Treasury').exists(): + raise PermissionDenied + + with transaction.atomic(): + revision.copy() + revision.author = request.user + revision.time = timezone.now() + revision.state = models.BudgetState.RESUBMIT.value + revision.action = models.BudgetAction.UPDATE_STATE.value + revision.save() + + emailer = Emailer() + for user in revision.contributors.all(): + emailer.send_mail([user.email], 'Action required: Budget returned for re-drafting: {}'.format(revision.name), 'sstreasury/email/returned.md', {'revision': revision}) + return redirect(reverse('budget_view', kwargs={'id': budget.id}))