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:
parent
a0863e4ade
commit
6dcafb2e9a
4 changed files with 157 additions and 37 deletions
|
@ -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,10 +56,28 @@ 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]
|
||||
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]
|
||||
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
|
||||
|
|
|
@ -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 }])
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in a new issue