# 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 . 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}))