summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYingtong Li <runassudo@yingtongli.me>2020-02-12 09:53:36 +1100
committerYingtong Li <runassudo@yingtongli.me>2020-02-12 10:01:49 +1100
commit1975c482738bbdfe4e4dd4fde1ec5cc678bfffbf (patch)
treeba81e3c3e9d9714ece66e31be021658908f06838
parent0da9bebcaea22dc29cf6ff4edc0fb27552d80fed (diff)
Validate budget input
-rw-r--r--sstreasury/jinja2/sstreasury/budget_edit.html18
-rw-r--r--sstreasury/views.py113
2 files changed, 100 insertions, 31 deletions
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 @@
<h1>{% if request.resolver_match.url_name == 'budget_new' %}New{% else %}Edit{% endif %} budget</h1>
<form class="ui form" method="POST">
+ {% if errors %}
+ <div class="ui visible error message">
+ <div class="header">Invalid input</div>
+ <p>Unable to save the budget. Please correct the issues below and try again:</p>
+ <ul>
+ {% for error in errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ </div>
+ {% endif %}
+
<div class="ui disabled inline grid field">
<label class="three wide column">ID</label>
<input class="eleven wide column" type="text" name="id" value="{{ 'BU-{}'.format(revision.budget.id) if revision.budget.id != None else '' }}">
@@ -62,7 +74,7 @@
<div class="four wide column">Estimated no. of attendees</div>
<input class="seven wide column" type="text" name="event_attendees" value="{{ revision.event_attendees or '' }}">
</div>
- <div class="ui required inline grid field">
+ <div class="ui inline grid field">
<label class="three wide column">Contributors <span data-tooltip="To share this budget with other contributors, enter their email addresses, one per line"><i class="grey question circle icon"></i></span></label>
<textarea class="eleven wide column" rows="2" name="contributors" style="font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;">{{ contributors }}</textarea>
</div>
@@ -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')
+
+ 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
- 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['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
Contact (issues, pull requests, etc.) at git@yingtongli.me. Generated by cgit.