Merge pull request #2 from vincentmdealmeida/ValidationV1

Laid the ground work for client-side input validation by setting up a…
This commit is contained in:
vincentmdealmeida 2018-06-13 13:05:10 +01:00 committed by GitHub
commit 4ca3398683
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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