Centralise access control logic

This commit is contained in:
Yingtong Li 2019-12-29 00:30:30 +11:00
parent b70ef9b8f6
commit c4eb45514f
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 303 additions and 261 deletions

View File

@ -27,36 +27,30 @@
<span class="ui header">Status: {{ revision.get_state_display() }}</span> <span class="ui header">Status: {{ revision.get_state_display() }}</span>
{% if is_latest %} {% if is_latest %}
{% if revision.state == import('sstreasury.models').BudgetState.DRAFT.value or revision.state == import('sstreasury.models').BudgetState.RESUBMIT.value %} {% if revision.can_submit(request.user) %}
<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>
{% endif %}
{#{% if request.user.groups.filter(name='Secretary').exists() %} {% if revision.can_endorse(request.user) %}
<button class="ui mini labeled positive icon button" type="submit" name="action" value="Approve" onclick="return confirm('Are you sure you want to mark this budget as committee-approved?');"><i class="check icon"></i> Approve</button>
<button class="ui mini labeled basic negative icon button" type="submit" name="action" value="CmteReturn" onclick="return confirm('Are you sure you want to refuse this budget committee approval and return it for re-drafting?');"><i class="undo icon"></i> Return for re-drafting</button>
{% endif %}#}
{% 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 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> <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.APPROVED.value and request.user.groups.filter(name='Secretary').exists() %} {% endif %}
{% if revision.can_approve(request.user) %}
<button class="ui mini labeled positive icon button" type="submit" name="action" value="Approve" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to mark this budget as committee-approved?');"><i class="check icon"></i> Approve</button> <button class="ui mini labeled positive icon button" type="submit" name="action" value="Approve" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to mark this budget as committee-approved?');"><i class="check icon"></i> Approve</button>
<button class="ui mini labeled basic negative icon button" type="submit" name="action" value="CmteReturn" onclick="return confirm('Are you sure you want to refuse this budget committee approval and return it for re-drafting?');"><i class="undo icon"></i> Return for re-drafting</button> <button class="ui mini labeled basic negative icon button" type="submit" name="action" value="CmteReturn" onclick="return confirm('Are you sure you want to refuse this budget committee approval and return it for re-drafting?');"><i class="undo icon"></i> Return for re-drafting</button>
{% elif revision.state == import('sstreasury.models').BudgetState.APPROVED.value %} {% endif %}
{# Blank #} {% if revision.can_withdraw(request.user) %}
{% 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> <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 %} {% 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())) %} {% if revision.can_edit(request.user) %}
<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>
<a class="ui mini labeled right floated icon button" href="{{ url('budget_print', kwargs={'id': revision.budget.id}) }}" target="_blank"><i class="print icon"></i> Print</a> {% endif %}
{% elif revision.state == import('sstreasury.models').BudgetState.APPROVED.value %}
<a class="ui mini labeled right floated icon button" href="{{ url('budget_print', kwargs={'id': revision.budget.id}) }}" target="_blank"><i class="print icon"></i> Print</a>
{% else %}
<a class="ui mini labeled right floated icon button" href="{{ url('budget_print', kwargs={'id': revision.budget.id}) }}" target="_blank"><i class="print icon"></i> Print</a> <a class="ui mini labeled right floated icon button" href="{{ url('budget_print', kwargs={'id': revision.budget.id}) }}" target="_blank"><i class="print icon"></i> Print</a>
{% if not revision.can_edit(request.user) and revision.can_withdraw(request.user) %}
<div class="ui message"> <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> <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> </div>

View File

@ -26,26 +26,25 @@
<form class="ui form" action="{{ url('claim_action', kwargs={'id': claim.id}) }}" method="POST"> <form class="ui form" action="{{ url('claim_action', kwargs={'id': claim.id}) }}" method="POST">
<span class="ui header">Status: {{ claim.get_state_display() }}</span> <span class="ui header">Status: {{ claim.get_state_display() }}</span>
{% if claim.state == import('sstreasury.models').ClaimState.DRAFT.value or claim.state == import('sstreasury.models').ClaimState.RESUBMIT.value %} {% if claim.can_submit(request.user) %}
<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 claim for Treasury approval? You will not be able to make any additional changes without withdrawing the claim.');"><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 claim for Treasury approval? You will not be able to make any additional changes without withdrawing the claim.');"><i class="paper plane icon"></i> Submit</button>
{% elif claim.state == import('sstreasury.models').ClaimState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %} {% endif %}
{% if claim.can_approve(request.user) %}
<button class="ui mini labeled positive icon button" type="submit" name="action" value="Approve" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to approve this claim?');"><i class="check icon"></i> Approve</button> <button class="ui mini labeled positive icon button" type="submit" name="action" value="Approve" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to approve this claim?');"><i class="check icon"></i> Approve</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 claim and return it for re-drafting?');"><i class="undo icon"></i> Return for re-drafting</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 claim and return it for re-drafting?');"><i class="undo icon"></i> Return for re-drafting</button>
{% elif claim.state == import('sstreasury.models').ClaimState.APPROVED.value or claim.state == import('sstreasury.models').ClaimState.PAID.value %} {% endif %}
{# Blank #} {% if claim.can_withdraw(request.user) %}
{% 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> <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 %} {% endif %}
{% if claim.state == import('sstreasury.models').ClaimState.DRAFT.value or claim.state == import('sstreasury.models').ClaimState.RESUBMIT.value or (claim.state != import('sstreasury.models').ClaimState.APPROVED.value and claim.state != import('sstreasury.models').ClaimState.PAID.value and request.user.groups.filter(name='Treasury').exists()) %} {% if claim.can_edit(request.user) %}
<a class="ui mini labeled right floated icon button" href="{{ url('claim_edit', kwargs={'id': claim.id}) }}"><i class="edit icon"></i> Edit</a> <a class="ui mini labeled right floated icon button" href="{{ url('claim_edit', kwargs={'id': claim.id}) }}"><i class="edit icon"></i> Edit</a>
<a class="ui mini labeled right floated icon button" href="{{ url('claim_print', kwargs={'id': claim.id}) }}" target="_blank"><i class="print icon"></i> Print</a> {% endif %}
{% elif claim.state == import('sstreasury.models').ClaimState.APPROVED.value or claim.state == import('sstreasury.models').ClaimState.PAID.value %}
<a class="ui mini labeled right floated icon button" href="{{ url('claim_print', kwargs={'id': claim.id}) }}" target="_blank"><i class="print icon"></i> Print</a>
{% else %}
<a class="ui mini labeled right floated icon button" href="{{ url('claim_print', kwargs={'id': claim.id}) }}" target="_blank"><i class="print icon"></i> Print</a> <a class="ui mini labeled right floated icon button" href="{{ url('claim_print', kwargs={'id': claim.id}) }}" target="_blank"><i class="print icon"></i> Print</a>
{% if not claim.can_edit(request.user) and claim.can_withdraw(request.user) %}
<div class="ui message"> <div class="ui message">
<p>This claim has been submitted and is now awaiting processing. If you wish to edit this claim, you must first withdraw it. This will revert the claim to a draft.</p> <p>This claim has been submitted and is now awaiting processing. If you wish to edit this claim, you must first withdraw it. This will revert the claim to a draft.</p>
</div> </div>
@ -111,7 +110,7 @@
<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 claim.state == import('sstreasury.models').ClaimState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %} {% if claim.can_approve(request.user) %}
<button class="ui right floated labeled basic negative icon button" type="submit" name="action" value="Comment,Return" onclick="return confirm('Are you sure you want to refuse this claim and return it for re-drafting?');"><i class="undo icon"></i> Comment and return for re-drafting</button> <button class="ui right floated labeled basic negative icon button" type="submit" name="action" value="Comment,Return" onclick="return confirm('Are you sure you want to refuse this claim and return it for re-drafting?');"><i class="undo icon"></i> Comment and return for re-drafting</button>
<button class="ui right floated labeled positive icon button" type="submit" name="action" value="Comment,Approve" onclick="return confirm('Are you sure you want to approve this claim?');"><i class="check icon"></i> Comment and approve</button> <button class="ui right floated labeled positive icon button" type="submit" name="action" value="Comment,Approve" onclick="return confirm('Are you sure you want to approve this claim?');"><i class="check icon"></i> Comment and approve</button>

View File

@ -17,6 +17,7 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.utils import timezone
from jsonfield import JSONField from jsonfield import JSONField
from enum import Enum from enum import Enum
@ -74,12 +75,25 @@ class BudgetRevision(models.Model):
action = models.IntegerField(choices=[(v.value, v.description) for v in BudgetAction]) action = models.IntegerField(choices=[(v.value, v.description) for v in BudgetAction])
class Meta:
ordering = ['id']
def copy(self): def copy(self):
contributors = list(self.contributors.all()) contributors = list(self.contributors.all())
self.pk, self.id = None, None self.pk, self.id = None, None
self.save() self.save()
self.contributors.add(*contributors) self.contributors.add(*contributors)
def update_state(self, user, state):
self.copy()
self.author = user
self.time = timezone.now()
self.state = state.value
self.action = BudgetAction.UPDATE_STATE.value
self.save()
# Access control
def can_view(self, user): def can_view(self, user):
if user == self.author: if user == self.author:
return True return True
@ -87,12 +101,74 @@ class BudgetRevision(models.Model):
return True return True
if user.groups.filter(name='Treasury').exists(): if user.groups.filter(name='Treasury').exists():
return True return True
if (self.state == BudgetState.ENDORSED or self.state == BudgetState.APPROVED) and user.groups.filter(name='Committee').exists(): if (self.state == BudgetState.ENDORSED.value or self.state == BudgetState.APPROVED.value) and user.groups.filter(name='Committee').exists():
return True
return False
def can_edit(self, user):
# Cannot edit if cannot view
if not self.can_view(user):
return False
# No one can edit if already approved
if self.state == BudgetState.APPROVED.value:
return False
# Only Treasurer or Secretary may edit if submitted
if self.state != BudgetState.DRAFT.value and self.state != BudgetState.RESUBMIT.value:
if user.groups.filter(name='Treasury').exists() or user.groups.filter(name='Secretary').exists():
return True return True
return False return False
class Meta: # Otherwise the submitter may edit
ordering = ['id'] if user == self.author:
return True
if user in self.contributors.all():
return True
if user.groups.filter(name='Treasury').exists():
return True
# Otherwise cannot edit
return False
def can_submit(self, user):
if not self.can_edit(user):
return False
if self.state == BudgetState.DRAFT.value or self.state == BudgetState.RESUBMIT.value:
return True
return False
def can_withdraw(self, user):
if not self.can_edit(user):
return False
if self.state == BudgetState.AWAIT_REVIEW.value or self.state == BudgetState.ENDORSED.value:
return True
return False
def can_endorse(self, user):
if not self.can_edit(user):
return False
if not user.groups.filter(name='Treasury').exists():
return False
if self.state == BudgetState.AWAIT_REVIEW.value:
return True
return False
def can_return(self, user):
return self.can_endorse(user)
def can_approve(self, user):
if not self.can_edit(user):
return False
if not user.groups.filter(name='Secretary').exists():
return False
if self.state == BudgetState.ENDORSED.value:
return True
return False
def can_cmtereturn(self, user):
return self.can_approve(user)
class ClaimState(DescriptionEnum): class ClaimState(DescriptionEnum):
DRAFT = 10, 'Draft' DRAFT = 10, 'Draft'
@ -119,6 +195,20 @@ class ReimbursementClaim(models.Model):
items = JSONField(default=[]) items = JSONField(default=[])
def update_state(self, user, state):
self.state = state.value
self.save()
claim_history = ClaimHistory()
claim_history.claim = self
claim_history.author = user
claim_history.state = self.state
claim_history.time = timezone.now()
claim_history.action = ClaimAction.UPDATE_STATE.value
claim_history.save()
# Access control
def can_view(self, user): def can_view(self, user):
if user == self.author: if user == self.author:
return True return True
@ -126,6 +216,58 @@ class ReimbursementClaim(models.Model):
return True return True
return False return False
def can_edit(self, user):
# Cannot edit if cannot view
if not self.can_view(user):
return False
# No one can edit if already paid
if self.state == ClaimState.PAID.value:
return False
# Only Treasurer may edit if submitted
if self.state != ClaimState.DRAFT.value and self.state != ClaimState.RESUBMIT.value:
if user.groups.filter(name='Treasury').exists():
return True
return False
# Otherwise the submitter or Treasurer may edit
if user == self.author:
return True
if user in self.contributors.all():
return True
if user.groups.filter(name='Treasury').exists():
return True
# Otherwise cannot edit
return False
def can_submit(self, user):
if not self.can_edit(user):
return False
if self.state == ClaimState.DRAFT.value or self.state == ClaimState.RESUBMIT.value:
return True
return False
def can_withdraw(self, user):
if not self.can_edit(user):
return False
if self.state == ClaimState.AWAIT_REVIEW.value or self.state == ClaimState.APPROVED.value:
return True
return False
def can_approve(self, user):
if not self.can_edit(user):
return False
if not user.groups.filter(name='Treasury').exists():
return False
if self.state == ClaimState.AWAIT_REVIEW.value:
return True
return False
def can_return(self, user):
return self.can_approve(user)
class ClaimReceipt(models.Model): class ClaimReceipt(models.Model):
claim = models.ForeignKey(ReimbursementClaim, on_delete=models.CASCADE) claim = models.ForeignKey(ReimbursementClaim, on_delete=models.CASCADE)
uploaded_file = models.FileField() uploaded_file = models.FileField()

View File

@ -32,10 +32,107 @@ import functools
import itertools import itertools
import json import json
# HELPER DECORATORS
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 not revision.can_view(request.user):
raise PermissionDenied
return viewfunc(request, budget, revision)
return func
def budget_editable(viewfunc):
@functools.wraps(viewfunc)
def func(request, budget, revision):
if not revision.can_edit(request.user):
raise PermissionDenied
return viewfunc(request, budget, revision)
return func
def uses_claim(viewfunc):
@functools.wraps(viewfunc)
def func(request, id):
claim = models.ReimbursementClaim.objects.get(id=id)
return viewfunc(request, claim)
return func
def claim_viewable(viewfunc):
@functools.wraps(viewfunc)
def func(request, claim):
if not claim.can_view(request.user):
raise PermissionDenied
return viewfunc(request, claim)
return func
def claim_editable(viewfunc):
@functools.wraps(viewfunc)
def func(request, claim):
if not claim.can_edit(request.user):
raise PermissionDenied
return viewfunc(request, claim)
return func
# HELPER FUNCTIONS
def revision_from_form(budget, revision, form):
revision.budget = budget
revision.name = form['name']
revision.date = form['date'] if form['date'] else None
revision.comments = form['comments']
revision.revenue = json.loads(form['revenue'])
revision.revenue_comments = form['revenue_comments']
revision.expense = json.loads(form['expense'])
revision.expense_comments = form['expense_comments']
revision.expense_no_emergency_fund = True if form.get('expense_no_emergency_fund', False) else False
revision.save()
contributors = form['contributors'].split('\n')
for contributor in contributors:
validate_email(contributor.strip())
for contributor in contributors:
try:
user = User.objects.get(email=contributor.strip())
except User.DoesNotExist:
user = User.objects.create_user(contributor.strip().split('@')[0], contributor.strip())
revision.contributors.add(user)
revision.save()
return revision
def claim_from_form(claim, form):
claim.purpose = form['purpose']
claim.date = form['date'] if form['date'] else None
claim.budget_id = form['budget_id']
claim.comments = form['comments']
claim.state = models.ClaimState.DRAFT.value
claim.items = json.loads(form['items'])
claim.save()
return claim
# INDEX VIEW
@login_required @login_required
def index(request): def index(request):
return render(request, 'sstreasury/index.html') return render(request, 'sstreasury/index.html')
# BUDGET VIEWS
@login_required @login_required
def budget_list(request): def budget_list(request):
budgets_action = [] budgets_action = []
@ -77,13 +174,10 @@ def budget_list(request):
}) })
@login_required @login_required
def budget_view(request, id): @uses_budget
budget = models.Budget.objects.get(id=id) def budget_view(request, budget, revision):
if 'revision' in request.GET: if 'revision' in request.GET:
revision = budget.budgetrevision_set.get(id=int(request.GET['revision'])) revision = budget.budgetrevision_set.get(id=int(request.GET['revision']))
else:
revision = budget.budgetrevision_set.reverse()[0]
if not revision.can_view(request.user): if not revision.can_view(request.user):
raise PermissionDenied raise PermissionDenied
@ -98,13 +192,10 @@ def budget_view(request, id):
}) })
@login_required @login_required
def budget_print(request, id): @uses_budget
budget = models.Budget.objects.get(id=id) def budget_print(request, budget, revision):
if 'revision' in request.GET: if 'revision' in request.GET:
revision = budget.budgetrevision_set.get(id=int(request.GET['revision'])) revision = budget.budgetrevision_set.get(id=int(request.GET['revision']))
else:
revision = budget.budgetrevision_set.reverse()[0]
if not revision.can_view(request.user): if not revision.can_view(request.user):
raise PermissionDenied raise PermissionDenied
@ -114,35 +205,6 @@ def budget_print(request, id):
'is_latest': 'revision' not in request.GET 'is_latest': 'revision' not in request.GET
}) })
def revision_from_form(budget, revision, form):
revision.budget = budget
revision.name = form['name']
revision.date = form['date'] if form['date'] else None
revision.comments = form['comments']
revision.revenue = json.loads(form['revenue'])
revision.revenue_comments = form['revenue_comments']
revision.expense = json.loads(form['expense'])
revision.expense_comments = form['expense_comments']
revision.expense_no_emergency_fund = True if form.get('expense_no_emergency_fund', False) else False
revision.save()
contributors = form['contributors'].split('\n')
for contributor in contributors:
validate_email(contributor.strip())
for contributor in contributors:
try:
user = User.objects.get(email=contributor.strip())
except User.DoesNotExist:
user = User.objects.create_user(contributor.strip().split('@')[0], contributor.strip())
revision.contributors.add(user)
revision.save()
return revision
@login_required @login_required
def budget_new(request): def budget_new(request):
if request.method == 'POST': if request.method == 'POST':
@ -170,39 +232,6 @@ def budget_new(request):
'contributors': request.user.email 'contributors': request.user.email
}) })
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 not revision.can_view(request.user):
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 not revision.can_view(request.user):
raise PermissionDenied
return viewfunc(request, budget, revision)
return func
@login_required @login_required
@uses_budget @uses_budget
@budget_editable @budget_editable
@ -253,16 +282,11 @@ def budget_action(request, budget, revision):
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 'Submit' in actions: if 'Submit' in actions:
if revision.state != models.BudgetState.DRAFT.value and revision.state != models.BudgetState.RESUBMIT.value: if not revision.can_submit(request.user):
raise PermissionDenied raise PermissionDenied
with transaction.atomic(): with transaction.atomic():
revision.copy() revision.update_state(request.user, models.BudgetState.AWAIT_REVIEW)
revision.author = request.user
revision.time = timezone.now()
revision.state = models.BudgetState.AWAIT_REVIEW.value
revision.action = models.BudgetAction.UPDATE_STATE.value
revision.save()
emailer = Emailer() emailer = Emailer()
for user in User.objects.filter(groups__name='Treasury'): for user in User.objects.filter(groups__name='Treasury'):
@ -271,29 +295,18 @@ def budget_action(request, budget, revision):
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 'Withdraw' in actions: 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 not revision.can_withdraw(user):
raise PermissionDenied
revision.copy()
revision.author = request.user
revision.time = timezone.now()
revision.state = models.BudgetState.DRAFT.value
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 raise PermissionDenied
with transaction.atomic(): with transaction.atomic():
revision.copy() revision.update_state(request.user, models.BudgetState.DRAFT)
revision.author = request.user
revision.time = timezone.now() if 'Endorse' in actions:
revision.state = models.BudgetState.ENDORSED.value if not revision.can_endorse(user):
revision.action = models.BudgetAction.UPDATE_STATE.value raise PermissionDenied
revision.save()
with transaction.atomic():
revision.update_state(request.user, models.BudgetState.ENDORSED)
emailer = Emailer() emailer = Emailer()
for user in User.objects.filter(groups__name='Secretary'): for user in User.objects.filter(groups__name='Secretary'):
@ -302,54 +315,33 @@ def budget_action(request, budget, revision):
emailer.send_mail([user.email], 'Budget endorsed, awaiting committee approval: {}'.format(revision.name), 'sstreasury/email/endorsed.md', {'revision': revision}) emailer.send_mail([user.email], 'Budget endorsed, awaiting committee approval: {}'.format(revision.name), 'sstreasury/email/endorsed.md', {'revision': revision})
if 'Return' in actions: if 'Return' in actions:
if revision.state != models.BudgetState.AWAIT_REVIEW.value: if not revision.can_return(user):
raise PermissionDenied
if not request.user.groups.filter(name='Treasury').exists():
raise PermissionDenied raise PermissionDenied
with transaction.atomic(): with transaction.atomic():
revision.copy() revision.update_state(request.user, models.BudgetState.RESUBMIT)
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() emailer = Emailer()
for user in revision.contributors.all(): 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}) emailer.send_mail([user.email], 'Action required: Budget returned for re-drafting: {}'.format(revision.name), 'sstreasury/email/returned.md', {'revision': revision})
if 'Approve' in actions: if 'Approve' in actions:
if revision.state == models.BudgetState.APPROVED.value: if not revision.can_approve(user):
raise PermissionDenied return PermissionDenied
if not request.user.groups.filter(name='Secretary').exists():
raise PermissionDenied
with transaction.atomic(): with transaction.atomic():
revision.copy() revision.update_state(request.user, models.BudgetState.APPROVED)
revision.author = request.user
revision.time = timezone.now()
revision.state = models.BudgetState.APPROVED.value
revision.action = models.BudgetAction.UPDATE_STATE.value
revision.save()
emailer = Emailer() emailer = Emailer()
for user in revision.contributors.all(): for user in revision.contributors.all():
emailer.send_mail([user.email], 'Budget approved: {}'.format(revision.name), 'sstreasury/email/approved.md', {'revision': revision}) emailer.send_mail([user.email], 'Budget approved: {}'.format(revision.name), 'sstreasury/email/approved.md', {'revision': revision})
if 'CmteReturn' in actions: if 'CmteReturn' in actions:
if revision.state == models.BudgetState.APPROVED.value: if not revision.can_cmtereturn(user):
raise PermissionDenied return PermissionDenied
if not request.user.groups.filter(name='Secretary').exists():
raise PermissionDenied
with transaction.atomic(): with transaction.atomic():
revision.copy() revision.update_state(request.user, models.BudgetState.RESUBMIT)
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() emailer = Emailer()
for user in revision.contributors.all(): for user in revision.contributors.all():
@ -392,19 +384,6 @@ def claim_list(request):
'claims_closed': claims_closed 'claims_closed': claims_closed
}) })
def claim_from_form(claim, form):
claim.purpose = form['purpose']
claim.date = form['date'] if form['date'] else None
claim.budget_id = form['budget_id']
claim.comments = form['comments']
claim.state = models.ClaimState.DRAFT.value
claim.items = json.loads(form['items'])
claim.save()
return claim
@login_required @login_required
def claim_new(request): def claim_new(request):
if request.method == 'POST': if request.method == 'POST':
@ -412,7 +391,6 @@ def claim_new(request):
claim = models.ReimbursementClaim() claim = models.ReimbursementClaim()
claim.author = request.user claim.author = request.user
claim.time = timezone.now() claim.time = timezone.now()
#revision.action = models.BudgetAction.CREATE.value
claim.state = models.BudgetState.DRAFT.value claim.state = models.BudgetState.DRAFT.value
claim = claim_from_form(claim, request.POST) claim = claim_from_form(claim, request.POST)
@ -437,38 +415,6 @@ def claim_new(request):
'claim': claim 'claim': claim
}) })
def uses_claim(viewfunc):
@functools.wraps(viewfunc)
def func(request, id):
claim = models.ReimbursementClaim.objects.get(id=id)
return viewfunc(request, claim)
return func
def claim_viewable(viewfunc):
@functools.wraps(viewfunc)
def func(request, claim):
if not claim.can_view(request.user):
raise PermissionDenied
return viewfunc(request, claim)
return func
def claim_editable(viewfunc):
@functools.wraps(viewfunc)
def func(request, claim):
if claim.state == models.ClaimState.PAID.value:
raise PermissionDenied
if claim.state != models.ClaimState.DRAFT.value and claim.state != models.ClaimState.RESUBMIT.value:
if not request.user.groups.filter(name='Treasury').exists():
raise PermissionDenied
if not claim.can_view(request.user):
raise PermissionDenied
return viewfunc(request, claim)
return func
@login_required @login_required
@uses_claim @uses_claim
@claim_viewable @claim_viewable
@ -541,20 +487,11 @@ def claim_action(request, claim):
emailer.send_mail([comment.author], 'New comment on reimbursement claim: {}'.format(revision.name), 'sstreasury/email/claim_commented.md', {'claim': claim, 'comment': comment}) emailer.send_mail([comment.author], 'New comment on reimbursement claim: {}'.format(revision.name), 'sstreasury/email/claim_commented.md', {'claim': claim, 'comment': comment})
if 'Submit' in actions: if 'Submit' in actions:
if claim.state != models.ClaimState.DRAFT.value and claim.state != models.ClaimState.RESUBMIT.value: if not claim.can_submit(request.user):
raise PermissionDenied raise PermissionDenied
with transaction.atomic(): with transaction.atomic():
claim.state = models.ClaimState.AWAIT_REVIEW.value claim.update_state(request.user, models.ClaimState.AWAIT_REVIEW)
claim.save()
claim_history = models.ClaimHistory()
claim_history.claim = claim
claim_history.author = request.user
claim_history.state = claim.state
claim_history.time = timezone.now()
claim_history.action = models.ClaimAction.UPDATE_STATE.value
claim_history.save()
emailer = Emailer() emailer = Emailer()
for user in User.objects.filter(groups__name='Treasury'): for user in User.objects.filter(groups__name='Treasury'):
@ -562,58 +499,28 @@ def claim_action(request, claim):
emailer.send_mail([claim.author.email], 'Reimbursement claim submitted: {}'.format(claim.purpose), 'sstreasury/email/claim_submitted_drafter.md', {'claim': claim}) emailer.send_mail([claim.author.email], 'Reimbursement claim submitted: {}'.format(claim.purpose), 'sstreasury/email/claim_submitted_drafter.md', {'claim': claim})
if 'Withdraw' in actions: if 'Withdraw' in actions:
if claim.state == models.ClaimState.DRAFT.value or claim.state == models.ClaimState.RESUBMIT.value or claim.state == models.ClaimState.PAID.value: if not claim.can_withdraw(request.user):
raise PermissionDenied
claim.state = models.ClaimState.DRAFT.value
claim.save()
claim_history = models.ClaimHistory()
claim_history.claim = claim
claim_history.author = request.user
claim_history.state = claim.state
claim_history.time = timezone.now()
claim_history.action = models.ClaimAction.UPDATE_STATE.value
claim_history.save()
if 'Approve' in actions:
if claim.state != models.ClaimState.AWAIT_REVIEW.value:
raise PermissionDenied
if not request.user.groups.filter(name='Treasury').exists():
raise PermissionDenied raise PermissionDenied
with transaction.atomic(): with transaction.atomic():
claim.state = models.ClaimState.APPROVED.value claim.update_state(request.user, models.ClaimState.DRAFT)
claim.save()
claim_history = models.ClaimHistory() if 'Approve' in actions:
claim_history.claim = claim if not claim.can_approve(request.user):
claim_history.author = request.user raise PermissionDenied
claim_history.state = claim.state
claim_history.time = timezone.now() with transaction.atomic():
claim_history.action = models.ClaimAction.UPDATE_STATE.value claim.update_state(request.user, models.ClaimState.APPROVED)
claim_history.save()
emailer = Emailer() emailer = Emailer()
emailer.send_mail([claim.author.email], 'Claim approved, awaiting payment: {}'.format(claim.purpose), 'sstreasury/email/claim_approved.md', {'claim': claim}) emailer.send_mail([claim.author.email], 'Claim approved, awaiting payment: {}'.format(claim.purpose), 'sstreasury/email/claim_approved.md', {'claim': claim})
if 'Return' in actions: if 'Return' in actions:
if claim.state != models.ClaimState.AWAIT_REVIEW.value: if not claim.can_approve(request.user):
raise PermissionDenied
if not request.user.groups.filter(name='Treasury').exists():
raise PermissionDenied raise PermissionDenied
with transaction.atomic(): with transaction.atomic():
claim.state = models.ClaimState.RESUBMIT.value claim.update_state(request.user, models.ClaimState.RESUBMIT)
claim.save()
claim_history = models.ClaimHistory()
claim_history.claim = claim
claim_history.author = request.user
claim_history.state = claim.state
claim_history.time = timezone.now()
claim_history.action = models.ClaimAction.UPDATE_STATE.value
claim_history.save()
emailer = Emailer() emailer = Emailer()
emailer.send_mail([claim.author.email], 'Action required: Reimbursement claim returned for re-drafting: {}'.format(claim.purpose), 'sstreasury/email/claim_returned.md', {'claim': claim}) emailer.send_mail([claim.author.email], 'Action required: Reimbursement claim returned for re-drafting: {}'.format(claim.purpose), 'sstreasury/email/claim_returned.md', {'claim': claim})