Treasury endorsement/return of budgets

This commit is contained in:
Yingtong Li 2019-06-20 01:06:24 +10:00
parent a0d6164d95
commit 69a7c052c1
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
5 changed files with 128 additions and 32 deletions

View File

@ -59,17 +59,13 @@
{% if budgets_open %} {% if budgets_open %}
<h2>Open budgets</h2> <h2>Open budgets</h2>
{% for budget in budgets_open %} {{ listbudgets(budgets_open) }}
{{ budget.name }}
{% endfor %}
{% endif %} {% endif %}
{% if budgets_closed %} {% if budgets_closed %}
<h2>Closed budgets</h2> <h2>Closed budgets</h2>
{% for budget in budgets_closed %} {{ listbudgets(budgets_closed) }}
{{ budget.name }}
{% endfor %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -29,9 +29,24 @@
{% if revision.state == import('sstreasury.models').BudgetState.DRAFT.value or revision.state == import('sstreasury.models').BudgetState.RESUBMIT.value %} {% if revision.state == import('sstreasury.models').BudgetState.DRAFT.value or revision.state == import('sstreasury.models').BudgetState.RESUBMIT.value %}
<button class="ui mini labeled primary icon button" type="submit" name="action" value="Submit" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to submit this budget for Treasury approval? You will not be able to make any additional changes without withdrawing the budget.');"><i class="paper plane icon"></i> Submit</button> <button class="ui mini labeled primary icon button" type="submit" name="action" value="Submit" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to submit this budget for Treasury approval? You will not be able to make any additional changes without withdrawing the budget.');"><i class="paper plane icon"></i> Submit</button>
{% elif revision.state == import('sstreasury.models').BudgetState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %}
<button class="ui mini labeled positive icon button" type="submit" name="action" value="Endorse" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to give this budget Treasury endorsement?');"><i class="check icon"></i> Endorse</button>
<button class="ui mini labeled basic negative icon button" type="submit" name="action" value="Return" onclick="return confirm('Are you sure you want to refuse this budget Treasury endorsement and return it for re-drafting?');"><i class="undo icon"></i> Return for re-drafting</button>
{% 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 %}
<button class="ui mini labeled basic negative icon button" type="submit" name="action" value="Withdraw" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to withdraw this budget from being considered for approval? The budget will be reverted to a draft.');"><i class="undo icon"></i> Withdraw</button>
{% 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())) %}
<a class="ui mini labeled right floated icon button" href="{{ url('budget_edit', kwargs={'id': revision.budget.id}) }}"><i class="edit icon"></i> Edit</a> <a class="ui mini labeled right floated icon button" href="{{ url('budget_edit', kwargs={'id': revision.budget.id}) }}"><i class="edit icon"></i> Edit</a>
{% elif revision.state != import('sstreasury.models').BudgetState.APPROVED.value %} {% else %}
<button class="ui mini labeled basic red icon button" type="submit" name="action" value="Withdraw" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to withdraw this budget from being considered for approval? The budget will be reverted to a draft.');"><i class="undo icon"></i> Withdraw</button> <div class="ui message">
<p>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.</p>
</div>
{% endif %} {% endif %}
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}"> <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
@ -126,6 +141,12 @@
</div> </div>
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}"> <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<input class="ui primary button" type="submit" name="action" value="Comment"> <input class="ui primary button" type="submit" name="action" value="Comment">
{% if revision.state == import('sstreasury.models').BudgetState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %}
<button class="ui labeled positive icon button" type="submit" name="action" value="Comment,Endorse" onclick="return confirm('Are you sure you want to give this budget Treasury endorsement?');"><i class="check icon"></i> Comment and endorse</button>
<button class="ui labeled basic negative icon button" type="submit" name="action" value="Comment,Return" onclick="return confirm('Are you sure you want to refuse this budget Treasury endorsement and return it for re-drafting?');"><i class="undo icon"></i> Comment and return for re-drafting</button>
{% endif %}
</form> </form>
<div class="ui feed"> <div class="ui feed">

View File

@ -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}) }}

View File

@ -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}) }}

View File

@ -28,6 +28,7 @@ from django.views import generic
from . import models from . import models
from ssmain.email import Emailer from ssmain.email import Emailer
import functools
import itertools import itertools
import json import json
@ -44,6 +45,18 @@ def budget_list(request):
for budget in models.Budget.objects.all(): for budget in models.Budget.objects.all():
revision = budget.budgetrevision_set.reverse()[0] revision = budget.budgetrevision_set.reverse()[0]
state = models.BudgetState(revision.state) state = models.BudgetState(revision.state)
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:
if request.user not in revision.contributors.all():
continue
if state in [models.BudgetState.DRAFT, models.BudgetState.RESUBMIT]: if state in [models.BudgetState.DRAFT, models.BudgetState.RESUBMIT]:
budgets_action.append(revision) budgets_action.append(revision)
elif state in [models.BudgetState.AWAIT_REVIEW, models.BudgetState.ENDORSED]: elif state in [models.BudgetState.AWAIT_REVIEW, models.BudgetState.ENDORSED]:
@ -131,15 +144,46 @@ def budget_new(request):
'contributors': request.user.email 'contributors': request.user.email
}) })
@login_required def uses_budget(viewfunc):
def budget_edit(request, id): @functools.wraps(viewfunc)
if request.method == 'POST': def func(request, id):
budget = models.Budget.objects.get(id=id) budget = models.Budget.objects.get(id=id)
revision = budget.budgetrevision_set.reverse()[0] 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 request.user not in revision.contributors.all():
if not request.user.groups.filter(name='Treasury').exists():
raise PermissionDenied 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': if request.POST['submit'] == 'Delete':
budget.delete() budget.delete()
return redirect(reverse('budget_list')) return redirect(reverse('budget_list'))
@ -156,23 +200,18 @@ def budget_edit(request, id):
else: else:
return redirect(reverse('budget_edit', kwargs={'id': budget.id})) return redirect(reverse('budget_edit', kwargs={'id': budget.id}))
else: else:
budget = models.Budget.objects.get(id=id)
revision = budget.budgetrevision_set.reverse()[0]
return render(request, 'sstreasury/budget_edit.html', { return render(request, 'sstreasury/budget_edit.html', {
'revision': revision, 'revision': revision,
'contributors': '\n'.join(revision.contributors.all().values_list('email', flat=True)) 'contributors': '\n'.join(revision.contributors.all().values_list('email', flat=True))
}) })
@login_required @login_required
def budget_action(request, id): @uses_budget
budget = models.Budget.objects.get(id=id) @budget_editable
revision = budget.budgetrevision_set.reverse()[0] def budget_action(request, budget, revision):
actions = request.POST['action'].split(',')
if request.user not in revision.contributors.all(): if 'Comment' in actions and request.POST.get('comment', None):
raise PermissionDenied
if request.POST['action'] == 'Comment':
with transaction.atomic(): with transaction.atomic():
comment = models.BudgetComment() comment = models.BudgetComment()
comment.budget = budget comment.budget = budget
@ -189,12 +228,11 @@ def budget_action(request, id):
if user != request.user: if user != request.user:
emailer.send_mail([user.email], 'New comment on budget: {}'.format(revision.name), 'sstreasury/email/commented.md', {'revision': revision, 'comment': comment}) 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: if revision.state != models.BudgetState.DRAFT.value and revision.state != models.BudgetState.RESUBMIT.value:
raise PermissionDenied raise PermissionDenied
with transaction.atomic(): with transaction.atomic():
# Copy revision
revision.copy() revision.copy()
revision.author = request.user revision.author = request.user
revision.time = timezone.now() revision.time = timezone.now()
@ -204,15 +242,14 @@ def budget_action(request, id):
emailer = Emailer() emailer = Emailer()
for user in User.objects.filter(groups__name='Treasury'): 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(): for user in revision.contributors.all():
emailer.send_mail([user.email], 'Budget submitted: {}'.format(revision.name), 'sstreasury/email/submitted_drafter.md', {'revision': revision}) 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: if revision.state == models.BudgetState.DRAFT.value or revision.state == models.BudgetState.RESUBMIT.value or revision.state == models.BudgetState.APPROVED.value:
raise PermissionDenied raise PermissionDenied
# Copy revision
revision.copy() revision.copy()
revision.author = request.user revision.author = request.user
revision.time = timezone.now() revision.time = timezone.now()
@ -220,4 +257,40 @@ def budget_action(request, id):
revision.action = models.BudgetAction.UPDATE_STATE.value revision.action = models.BudgetAction.UPDATE_STATE.value
revision.save() 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})) return redirect(reverse('budget_view', kwargs={'id': budget.id}))