363 lines
13 KiB
Python
363 lines
13 KiB
Python
# Society Self-Service
|
|
# Copyright © 2018-2019 Yingtong Li (RunasSudo)
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth.models import User
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.core.validators import validate_email
|
|
|
|
from django.db import transaction
|
|
from django.shortcuts import render, redirect
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.views import generic
|
|
|
|
from . import models
|
|
from ssmain.email import Emailer
|
|
|
|
import functools
|
|
import itertools
|
|
import json
|
|
|
|
@login_required
|
|
def index(request):
|
|
return render(request, 'sstreasury/index.html')
|
|
|
|
@login_required
|
|
def budget_list(request):
|
|
budgets_action = []
|
|
budgets_open = []
|
|
budgets_closed = []
|
|
|
|
for budget in models.Budget.objects.all():
|
|
revision = budget.budgetrevision_set.reverse()[0]
|
|
state = models.BudgetState(revision.state)
|
|
|
|
group = None
|
|
|
|
if request.user.groups.filter(name='Treasury').exists() and state == models.BudgetState.AWAIT_REVIEW:
|
|
group = budgets_action
|
|
elif request.user.groups.filter(name='Secretary').exists() and state == models.BudgetState.ENDORSED:
|
|
group = budgets_action
|
|
else:
|
|
if request.user in revision.contributors.all():
|
|
if state in [models.BudgetState.DRAFT, models.BudgetState.RESUBMIT]:
|
|
group = budgets_action
|
|
elif state in [models.BudgetState.AWAIT_REVIEW, models.BudgetState.ENDORSED]:
|
|
group = budgets_open
|
|
else:
|
|
group = budgets_closed
|
|
else:
|
|
if request.user in revision.contributors.all() or request.user.groups.filter(name='Treasury').exists() or request.user.groups.filter(name='Secretary').exists():
|
|
if state == models.BudgetState.APPROVED:
|
|
group = budgets_closed
|
|
else:
|
|
group = budgets_open
|
|
|
|
if group is not None:
|
|
group.append(revision)
|
|
|
|
return render(request, 'sstreasury/budget_list.html', {
|
|
'budgets_action': budgets_action,
|
|
'budgets_open': budgets_open,
|
|
'budgets_closed': budgets_closed
|
|
})
|
|
|
|
@login_required
|
|
def budget_view(request, id):
|
|
budget = models.Budget.objects.get(id=id)
|
|
|
|
if 'revision' in request.GET:
|
|
revision = budget.budgetrevision_set.get(id=int(request.GET['revision']))
|
|
else:
|
|
revision = budget.budgetrevision_set.reverse()[0]
|
|
|
|
if request.user not in revision.contributors.all():
|
|
if not request.user.groups.filter(name='Treasury').exists():
|
|
raise PermissionDenied
|
|
|
|
history = list(itertools.chain(budget.budgetrevision_set.all(), revision.budget.budgetcomment_set.all()))
|
|
history.sort(key=lambda x: x.time, reverse=True)
|
|
|
|
return render(request, 'sstreasury/budget_view.html', {
|
|
'revision': revision,
|
|
'history': history,
|
|
'is_latest': 'revision' not in request.GET
|
|
})
|
|
|
|
@login_required
|
|
def budget_print(request, id):
|
|
budget = models.Budget.objects.get(id=id)
|
|
|
|
if 'revision' in request.GET:
|
|
revision = budget.budgetrevision_set.get(id=int(request.GET['revision']))
|
|
else:
|
|
revision = budget.budgetrevision_set.reverse()[0]
|
|
|
|
if request.user not in revision.contributors.all():
|
|
if not request.user.groups.filter(name='Treasury').exists():
|
|
raise PermissionDenied
|
|
|
|
return render(request, 'sstreasury/budget_print.html', {
|
|
'revision': revision,
|
|
'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.state = models.BudgetState.DRAFT.value
|
|
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
|
|
def budget_new(request):
|
|
if request.method == 'POST':
|
|
with transaction.atomic():
|
|
budget = models.Budget()
|
|
budget.save()
|
|
revision = models.BudgetRevision()
|
|
revision.author = request.user
|
|
revision.time = timezone.now()
|
|
revision.action = models.BudgetAction.CREATE.value
|
|
revision = revision_from_form(budget, revision, request.POST)
|
|
|
|
if request.POST['submit'] == 'Save':
|
|
return redirect(reverse('budget_view', kwargs={'id': budget.id}))
|
|
else:
|
|
return redirect(reverse('budget_edit', kwargs={'id': budget.id}))
|
|
else:
|
|
budget = models.Budget()
|
|
revision = models.BudgetRevision()
|
|
revision.budget = budget
|
|
|
|
return render(request, 'sstreasury/budget_edit.html', {
|
|
'revision': revision,
|
|
'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 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'))
|
|
|
|
with transaction.atomic():
|
|
revision = models.BudgetRevision()
|
|
revision.author = request.user
|
|
revision.time = timezone.now()
|
|
revision.action = models.BudgetAction.EDIT.value
|
|
revision = revision_from_form(budget, revision, request.POST)
|
|
|
|
if request.POST['submit'] == 'Save':
|
|
return redirect(reverse('budget_view', kwargs={'id': budget.id}))
|
|
else:
|
|
return redirect(reverse('budget_edit', kwargs={'id': budget.id}))
|
|
else:
|
|
return render(request, 'sstreasury/budget_edit.html', {
|
|
'revision': revision,
|
|
'contributors': '\n'.join(revision.contributors.all().values_list('email', flat=True))
|
|
})
|
|
|
|
@login_required
|
|
@uses_budget
|
|
@budget_editable
|
|
def budget_action(request, budget, revision):
|
|
actions = request.POST['action'].split(',')
|
|
|
|
if 'Comment' in actions and request.POST.get('comment', None):
|
|
with transaction.atomic():
|
|
comment = models.BudgetComment()
|
|
comment.budget = budget
|
|
comment.author = request.user
|
|
comment.time = timezone.now()
|
|
comment.content = request.POST['comment']
|
|
comment.save()
|
|
|
|
emailer = Emailer()
|
|
for user in User.objects.filter(groups__name='Treasury'):
|
|
if user != request.user:
|
|
emailer.send_mail([user.email], 'New comment on budget: {}'.format(revision.name), 'sstreasury/email/commented.md', {'revision': revision, 'comment': comment})
|
|
for user in revision.contributors.all():
|
|
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 'Submit' in actions:
|
|
if revision.state != models.BudgetState.DRAFT.value and revision.state != models.BudgetState.RESUBMIT.value:
|
|
raise PermissionDenied
|
|
|
|
with transaction.atomic():
|
|
revision.copy()
|
|
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()
|
|
for user in User.objects.filter(groups__name='Treasury'):
|
|
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 '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
|
|
|
|
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
|
|
|
|
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 User.objects.filter(groups__name='Secretary'):
|
|
emailer.send_mail([user.email], 'Action required: Budget endorsed: {}'.format(revision.name), 'sstreasury/email/endorsed_secretary.md', {'revision': revision})
|
|
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})
|
|
|
|
if 'Approve' in actions:
|
|
if revision.state == models.BudgetState.APPROVED.value:
|
|
raise PermissionDenied
|
|
if not request.user.groups.filter(name='Secretary').exists():
|
|
raise PermissionDenied
|
|
|
|
with transaction.atomic():
|
|
revision.copy()
|
|
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()
|
|
for user in revision.contributors.all():
|
|
emailer.send_mail([user.email], 'Budget approved: {}'.format(revision.name), 'sstreasury/email/approved.md', {'revision': revision})
|
|
|
|
if 'CmteReturn' in actions:
|
|
if revision.state == models.BudgetState.APPROVED.value:
|
|
raise PermissionDenied
|
|
if not request.user.groups.filter(name='Secretary').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_committee.md', {'revision': revision})
|
|
|
|
return redirect(reverse('budget_view', kwargs={'id': budget.id}))
|