Laid the ground work for client-side input validation by setting up a fn that's triggered before the form is submitted. The vote start and end date time is now being validated both server side and client side and now includes UTC offsets

This commit is contained in:
vince0656 2018-06-13 13:01:55 +01:00
parent a0863e4ade
commit 6dcafb2e9a
4 changed files with 157 additions and 37 deletions

View file

@ -1,5 +1,7 @@
from datetime import datetime
from django.utils.dateparse import parse_datetime
from allauthdemo.polls.models import Event
from allauthdemo.polls.models import Poll
from allauthdemo.polls.models import PollOption
@ -54,11 +56,29 @@ class CreateNewEventModelAdaptor:
self.identifier = self.form_data.pop('identifier-input')[0]
# Extract start and end times as string and convert to datetime
# The UTC offset comes with a colon i.e. '+01:00' which needs to be removed
starts_at = self.form_data.pop('vote-start-input')[0]
self.starts_at = datetime.strptime(starts_at, '%Y-%m-%d %H:%M')
starts_at_offset_index = starts_at.find('+')
if starts_at_offset_index != -1:
starts_at_time = starts_at[0: starts_at_offset_index-1].replace(' ', 'T')
starts_at_offset = starts_at[starts_at_offset_index:].replace(':', '')
starts_at = starts_at_time + starts_at_offset
self.starts_at = parse_datetime(starts_at)
else:
self.starts_at = datetime.strptime(starts_at, '%Y-%m-%d %H:%M')
ends_at = self.form_data.pop('vote-end-input')[0]
self.ends_at = datetime.strptime(ends_at, '%Y-%m-%d %H:%M')
ends_at_offset_index = ends_at.find('+')
if ends_at_offset_index != -1:
ends_at_time = ends_at[0:ends_at_offset_index-1].replace(' ', 'T')
ends_at_offset = ends_at[ends_at_offset_index:].replace(':', '')
ends_at = ends_at_time + ends_at_offset
self.ends_at = parse_datetime(ends_at)
else:
self.ends_at = datetime.strptime(ends_at, '%Y-%m-%d %H:%M')
# Extract the list of organisers
organisers_list = self.form_data.pop('organiser-email-input')

View file

@ -302,7 +302,7 @@ def create_event(request):
# TODO: Based on whether validation was successful within update model and whether
# TODO: data was actually persisted, either perform a redirect (success) or flag an error
return HttpResponseRedirect("/event")
return HttpResponseRedirect("/event/")
elif request.method == "GET":
#form = EventForm()
#organiser_formset = OrganiserFormSet(prefix="formset_organiser", initial=[{'email': request.user.email }])

View file

@ -22,7 +22,7 @@
<div class="form-group"> <!-- Excluded class(missing %s): { if election_form.name.errors }has-error{ endif } -->
<label for="name-input" class="col-sm-3 col-md-2 control-label">Name:</label> <!-- This text can be a template variable -->
<div class="col-sm-9 col-md-10">
<input type="text" class="form-control" id="name-input" placeholder="Example: My poll" name="name-input" maxlength="255">
<input type="text" class="form-control input-control" id="name-input" placeholder="Example: My poll" name="name-input" maxlength="255">
<span id="name-input-help-block" class="help-block">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
A short and clear name.
@ -34,7 +34,7 @@
<div class="form-group"> <!-- Excluded class(missing %s): { if election_form.slug.errors }has-error{ endif } -->
<label for="identifier-input" class="col-sm-3 col-md-2 control-label">Identifier:</label> <!-- This text can be a template variable -->
<div class="col-sm-9 col-md-10">
<input type="text" class="form-control" id="identifier-input" placeholder="Example: My-poll" name="identifier-input" maxlength="255">
<input type="text" class="form-control input-control" id="identifier-input" placeholder="Example: My-poll" name="identifier-input" maxlength="255">
<span id="identifier-input-help-block" class="help-block">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
Used in the election URL, it must only consist of letters, numbers, underscores or hyphens; no whitespace is permitted.
@ -47,7 +47,7 @@
<label for="vote-start-input" class="col-sm-3 col-md-2 control-label">Voting starts at:</label>
<div class="col-sm-9 col-md-10">
<div class="input-group date">
<input type="text" class="form-control" data-date-format="YYYY-MM-DD H:mm" id="vote-start-input" name="vote-start-input">
<input type="text" class="form-control input-control" data-date-format="YYYY-MM-DD H:mm Z" id="vote-start-input" name="vote-start-input">
<span class="input-group-addon btn">
<i class="fa fa-calendar" aria-hidden="true"></i>
/
@ -56,7 +56,7 @@
</div>
<span id="vote-start-input-help-block" class="help-block">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
Date and time when registered voters can commence voting.
Date and time when registered voters can commence voting. This includes the UTC offset starting with '+'.
</span>
</div>
</div>
@ -65,7 +65,7 @@
<label for="vote-end-input" class="col-sm-3 col-md-2 control-label">Voting ends at:</label>
<div class="col-sm-9 col-md-10">
<div class="input-group date">
<input type="text" class="form-control" data-date-format="YYYY-MM-DD H:mm" id="vote-end-input" name="vote-end-input">
<input type="text" class="form-control input-control" data-date-format="YYYY-MM-DD H:mm Z" id="vote-end-input" name="vote-end-input">
<span class="input-group-addon btn">
<i class="fa fa-calendar" aria-hidden="true"></i>
/
@ -74,7 +74,7 @@
</div>
<span id="vote-end-input-help-block" class="help-block">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
Date and time when registered voters can no longer vote.
Date and time when registered voters can no longer vote. This includes the UTC offset starting with '+'.
</span>
</div>
</div>
@ -82,7 +82,7 @@
<div class="form-group">
<label for="question-input" class="col-sm-3 col-md-2 control-label">Question / Statement:</label> <!-- This text can be a template variable -->
<div class="col-sm-9 col-md-10">
<input type="text" class="form-control" id="question-input" placeholder="Example: Elections for the European Parliament" name="question-input" maxlength="200">
<input type="text" class="form-control input-control" id="question-input" placeholder="Example: Elections for the European Parliament" name="question-input" maxlength="200">
<span id="question-input-help-block" class="help-block">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
Question / Statement that will be put forward to voters along with the below options.
@ -115,7 +115,7 @@
<td>
<div> <!-- Has error conditional class removed -->
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
<input type="text" class="form-control input-sm" placeholder="Example: Candidate 1" id="option-name-input" name="option-name-input" maxlength="200">
<input type="text" class="form-control input-sm input-control" placeholder="Example: Candidate 1" id="option-name-input" name="option-name-input" maxlength="200">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</td>
@ -136,7 +136,7 @@
<td>
<div> <!-- Has error conditional class removed -->
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
<input type="text" class="form-control input-sm" placeholder="Example: Candidate 2" id="option-name-input" name="option-name-input" maxlength="200">
<input type="text" class="form-control input-sm input-control" placeholder="Example: Candidate 2" id="option-name-input" name="option-name-input" maxlength="200">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</td>
@ -157,7 +157,7 @@
<td>
<div> <!-- Has error conditional class removed -->
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
<input type="text" class="form-control input-sm" placeholder="Example: Candidate X" id="option-name-input" name="option-name-input" maxlength="200">
<input type="text" class="form-control input-sm input-control" placeholder="Example: Candidate X" id="option-name-input" name="option-name-input" maxlength="200">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</td>
@ -190,12 +190,12 @@
<div class="row">
<div class="col-xs-6">
<label class="sr-only" for="minimum-input">Minimum</label>
<input type="number" class="form-control" id="minimum-input" placeholder="Minimum" value="" name="minimum-input" min="0"> <!-- TODO: Max should be set to the number of options -->
<input type="number" class="form-control input-control" id="minimum-input" placeholder="Minimum" value="" name="minimum-input" min="0"> <!-- TODO: Max should be set to the number of options -->
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
<div class="col-xs-6">
<label class="sr-only" for="maximum-input">Maximum</label>
<input type="number" class="form-control" id="maximum-input" placeholder="Maximum" value="" name="maximum-input" min="1"> <!-- TODO: Max should be set to the number of options -->
<input type="number" class="form-control input-control" id="maximum-input" placeholder="Maximum" value="" name="maximum-input" min="1"> <!-- TODO: Max should be set to the number of options -->
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</div>
@ -231,7 +231,7 @@
<!-- Email -->
<div> <!-- Has error conditional class removed -->
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
<input type="text" class="form-control input-sm" placeholder="Example: organiser@example.com" id="organiser-email-input" name="organiser-email-input" value="{{ user_email }}" maxlength="255">
<input type="text" class="form-control input-sm input-control" placeholder="Example: organiser@example.com" id="organiser-email-input" name="organiser-email-input" value="{{ user_email }}" maxlength="255">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</td>
@ -252,7 +252,7 @@
<!-- Email -->
<div> <!-- Has error conditional class removed -->
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
<input type="text" class="form-control input-sm" placeholder="Example: organiser@example.com" id="organiser-email-input" name="organiser-email-input" maxlength="255">
<input type="text" class="form-control input-sm input-control" placeholder="Example: organiser@example.com" id="organiser-email-input" name="organiser-email-input" maxlength="255">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</td>
@ -272,7 +272,7 @@
<!-- Email -->
<div> <!-- Has error conditional class removed -->
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
<input type="text" class="form-control input-sm" placeholder="Example: organiser@example.com" id="organiser-email-input" name="organiser-email-input" maxlength="255">
<input type="text" class="form-control input-sm input-control" placeholder="Example: organiser@example.com" id="organiser-email-input" name="organiser-email-input" maxlength="255">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</td>
@ -322,7 +322,7 @@
<!-- Email -->
<div> <!-- Has error conditional class removed -->
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
<input type="text" class="form-control input-sm" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" value="{{ user_email }}" maxlength="255">
<input type="text" class="form-control input-sm input-control" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" value="{{ user_email }}" maxlength="255">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</td>
@ -343,7 +343,7 @@
<!-- Email -->
<div> <!-- Has error conditional class removed -->
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
<input type="text" class="form-control input-sm" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" maxlength="255">
<input type="text" class="form-control input-sm input-control" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" maxlength="255">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</td>
@ -363,7 +363,7 @@
<!-- Email -->
<div> <!-- Has error conditional class removed -->
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
<input type="text" class="form-control input-sm" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" maxlength="255">
<input type="text" class="form-control input-sm input-control" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" maxlength="255">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
</div>
</td>
@ -392,7 +392,7 @@
<div class="form-group">
<label for="voters-list-input" class="col-sm-3 col-md-2 control-label">Voters List:</label> <!-- This text can be a template variable -->
<div class="col-sm-9 col-md-10">
<textarea class="form-control" id="voters-list-input" placeholder="alice@example.com, bob@example.com..." name="voters-list-input" rows="4"></textarea>
<textarea class="form-control input-control" id="voters-list-input" placeholder="alice@example.com, bob@example.com..." name="voters-list-input" rows="4"></textarea>
<span id="voters-list-input-help-block" class="help-block">
<!-- Error handling / input validation has been removed temporarily and would be placed here -->
Manually enter email addresses separated with commas. Alternatively, you can also upload a CSV file:
@ -422,7 +422,7 @@
</div>
<hr>
<input class="btn btn-success" type="submit" value="Create Event" id="submit-event-create" disabled/>
<input class="btn btn-danger" type="button" value="Cancel" onclick="location.href='{% url 'polls:index' %}'" />
<input class="btn btn-danger" type="button" value="Cancel" id="cancel-event-create" onclick="location.href='{% url 'polls:index' %}'" />
</form>
</div>
</div>

View file

@ -1,3 +1,111 @@
// Form submission and validation
var submitBtn = $("#submit-event-create");
var dateRegex = /^[0-9]{4}-(((0[13578]|(10|12))-(0[1-9]|[1-2][0-9]|3[0-1]))|(02-(0[1-9]|[1-2][0-9]))|((0[469]|11)-(0[1-9]|[1-2][0-9]|30)))\s[0-9]{2}:[0-9]{2}\s\+[0-9]{2}:[0-9]{2}$/;
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var reCaptchaValid = false;
$("#election-form").submit(function(e) {
// Intercept submission of form and temporarily suspend it
e.preventDefault();
var form = this;
// Get a reference to the submit button
submitBtn.prop('disabled', true);
submitBtn.val('Please wait...');
// Disable the cancel button during validation
var cancelBtn = $("#cancel-event-create");
cancelBtn.prop('disabled', true);
// Perform input validation
var formDataValid = isFormValid();
if( formDataValid === true ) {
form.submit();
} else {
submitBtn.val('Errors Found');
cancelBtn.removeAttr('disabled');
}
});
function isFormValid() {
var nameValid = isNameValid();
var slugValid = isSlugValid();
var voteStartValid = isVoteStartValid();
var voteEndValid = isVoteEndValid();
var pollOptsValid = arePollsAndOptsValid();
var minSelectionValid = isMinSelectionValid();
var maxSelectionValid = isMaxSelectionValid();
var organisersEmailsValid = areOrganisersEmailsValid();
var trusteesEmailsValid = areTrusteesEmailsValid();
var votersListValid = isVotersListValid();
var reCaptchaValid = isReCaptchaStillValid();
return nameValid && slugValid && voteStartValid && voteEndValid
&& pollOptsValid && minSelectionValid && maxSelectionValid
&& organisersEmailsValid && trusteesEmailsValid && votersListValid
&& reCaptchaValid;
}
function isNameValid() {
// Based on a list of names supplied
return true;
}
function isSlugValid() {
return true;
}
function isVoteStartValid() {
var start_date_time = $('#vote-start-input').val();
return isDateValid(start_date_time);
}
function isVoteEndValid() {
var end_date_time = $('#vote-end-input').val();
return isDateValid(end_date_time);
}
function isDateValid(date_time) {
return dateRegex.test(date_time);
}
function arePollsAndOptsValid() {
// Future validation could be added here
return true;
}
function isMinSelectionValid() {
return true;
}
function isMaxSelectionValid() {
return true;
}
function areOrganisersEmailsValid() {
return true;
}
function areTrusteesEmailsValid() {
return true;
}
function isVotersListValid() {
return true;
}
function isReCaptchaStillValid() {
return true;
}
$('.input-control').on('input', function(e) {
if(reCaptchaValid === true) {
submitBtn.val('Create Event');
submitBtn.removeAttr('disabled');
}
});
// File handling
function processFileChange(event) {
@ -55,11 +163,14 @@ document.getElementById('files').addEventListener('change', processFileChange, f
// reCAPTCHA
function reCVerificationCallback() {
$('#submit-event-create').removeAttr('disabled');
// TODO: call isFormValid before doing this and highlighting errors if any found
reCaptchaValid = true;
submitBtn.removeAttr('disabled');
}
function reCExpiredCallback() {
$('#submit-event-create').prop('disabled', true);
reCaptchaValid = false;
submitBtn.prop('disabled', true);
}
// Slug field.
@ -128,6 +239,7 @@ $('#vote-start-input, #vote-end-input').parent('.date').datetimepicker({
},
minDate: moment().startOf('day'),
useCurrent: false,
locale: moment.utc()
});
// Form management and Sortable rows
@ -138,18 +250,6 @@ function update(event, ui) {
updateFormset(formset);
}
/*$('#options-input-table').rowSorter({
"handler" : null, // drag handler selector (default: null)
"tbody" : true, // True if you want to sort only tbody > tr. (default: true)
"tableClass" : "sorting-table", // This is added to the table during sorting
"dragClass": "sorting-row", // dragging row's class name (default: "sorting-row").
"stickTopRows": 0, // count of top sticky rows (default: 0)
"stickBottomRows": 0, // count of bottom sticky rows (default: 0)
"onDragStart": dragStart, // (default: null)
"onDragEnd": dragEnd, // (default: null)
"onDrop": drop // (default: null)
});*/
$("#options-input-table, #organisers-input-table, #trustees-input-table").sortable({
items: "tr",
update: update