diff --git a/sstreasury/jinja2/sstreasury/budget_edit.html b/sstreasury/jinja2/sstreasury/budget_edit.html index 511f7b4..bd2aea3 100644 --- a/sstreasury/jinja2/sstreasury/budget_edit.html +++ b/sstreasury/jinja2/sstreasury/budget_edit.html @@ -24,6 +24,18 @@

{% if request.resolver_match.url_name == 'budget_new' %}New{% else %}Edit{% endif %} budget

+ {% if errors %} +
+
Invalid input
+

Unable to save the budget. Please correct the issues below and try again:

+ +
+ {% endif %} +
@@ -62,7 +74,7 @@
Estimated no. of attendees
-
+
@@ -178,10 +190,6 @@ $('.ui.form').form({ on: 'blur', keyboardShortcuts: false, - fields: { - name: 'empty', - contributors: 'empty' - }, onSuccess: function(event, fields) { var revenue_data = []; $('#revenue_grid .jsgrid-grid-body tr:not(.totalrow):not(.jsgrid-nodata-row)').each(function(i, el) { diff --git a/sstreasury/views.py b/sstreasury/views.py index bc55abc..65d0e0a 100644 --- a/sstreasury/views.py +++ b/sstreasury/views.py @@ -16,7 +16,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, ValidationError from django.core.validators import validate_email from django.conf import settings @@ -32,6 +32,7 @@ from . import models from . import xero from ssmain.email import Emailer +from datetime import datetime import functools import io import itertools @@ -88,14 +89,58 @@ def claim_editable(viewfunc): # HELPER FUNCTIONS +class FormValidationError(Exception): + def __init__(self, data, errors): + super().__init__(self) + self.data = data + self.errors = errors + def revision_from_form(budget, revision, form): + errors = [] + revision.budget = budget - revision.name = form['name'] - revision.date = form['date'] if form['date'] else None + if form['name']: + if len(form['name']) > 100: + errors.append('Title must be at most 100 characters') + revision.name = form['name'] + else: + errors.append('A title must be specified') - revision.event_dt = form['event_dt'] if form['event_dt'] else None - revision.event_attendees = form['event_attendees'] if form['event_attendees'] else None + if form['date']: + try: + form_date = datetime.strptime(form['date'], '%Y-%m-%d') + except ValueError: + errors.append('Due date is not a valid date') + revision.date = form['date'] + else: + errors.append('A due date must be specified') + + if form['event_dt']: + try: + form_event_dt = datetime.strptime(form['event_dt'], '%Y-%m-%d %H:%M') + except ValueError: + errors.append('Event date/time is not a valid date-time') + revision.event_dt = form['event_dt'] + else: + revision.event_dt = None + + if form['event_attendees']: + if len(form['event_attendees']) > 20: + errors.append('Event attendees must be at most 20 characters') + revision.event_attendees = form['event_attendees'] + else: + revision.event_attendees = None + + if form['contributors']: + contributors = form['contributors'].split('\n') + try: + for contributor in contributors: + validate_email(contributor.strip()) + except ValidationError: + errors.append('Contributors contains invalid data – type only valid email addresses, one per line') + else: + contributors = [] revision.comments = form['comments'] revision.revenue = json.loads(form['revenue']) @@ -104,11 +149,11 @@ def revision_from_form(budget, revision, form): revision.expense_comments = form['expense_comments'] revision.expense_no_emergency_fund = True if form.get('expense_no_emergency_fund', False) else False + if errors: + raise FormValidationError(revision, errors) + 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()) @@ -227,15 +272,22 @@ def budget_print(request, budget, 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.state = models.BudgetState.DRAFT.value - revision = revision_from_form(budget, revision, request.POST) + try: + 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.state = models.BudgetState.DRAFT.value + revision = revision_from_form(budget, revision, request.POST) + except FormValidationError as form_error: + return render(request, 'sstreasury/budget_edit.html', { + 'revision': form_error.data, + 'contributors': request.POST['contributors'], + 'errors': form_error.errors + }) if request.POST['submit'] == 'Save': return redirect(reverse('budget_view', kwargs={'id': budget.id})) @@ -248,7 +300,8 @@ def budget_new(request): return render(request, 'sstreasury/budget_edit.html', { 'revision': revision, - 'contributors': request.user.email + 'contributors': request.user.email, + 'errors': [] }) @login_required @@ -260,13 +313,20 @@ def budget_edit(request, budget, revision): budget.delete() return redirect(reverse('budget_list')) - with transaction.atomic(): - new_revision = models.BudgetRevision() - new_revision.author = request.user - new_revision.time = timezone.now() - new_revision.action = models.BudgetAction.EDIT.value - new_revision.state = revision.state - new_revision = revision_from_form(budget, new_revision, request.POST) + try: + with transaction.atomic(): + new_revision = models.BudgetRevision() + new_revision.author = request.user + new_revision.time = timezone.now() + new_revision.action = models.BudgetAction.EDIT.value + new_revision.state = revision.state + new_revision = revision_from_form(budget, new_revision, request.POST) + except FormValidationError as form_error: + return render(request, 'sstreasury/budget_edit.html', { + 'revision': form_error.data, + 'contributors': request.POST['contributors'], + 'errors': form_error.errors + }) if request.POST['submit'] == 'Save': return redirect(reverse('budget_view', kwargs={'id': budget.id})) @@ -275,7 +335,8 @@ def budget_edit(request, budget, revision): else: return render(request, 'sstreasury/budget_edit.html', { 'revision': revision, - 'contributors': '\n'.join(revision.contributors.all().values_list('email', flat=True)) + 'contributors': '\n'.join(revision.contributors.all().values_list('email', flat=True)), + 'errors': [] }) @login_required