diff --git a/allauthdemo/polls/utils/CreateNewEventModelAdaptor.py b/allauthdemo/polls/utils/CreateNewEventModelAdaptor.py index 1444ce6..a0c7f93 100644 --- a/allauthdemo/polls/utils/CreateNewEventModelAdaptor.py +++ b/allauthdemo/polls/utils/CreateNewEventModelAdaptor.py @@ -14,7 +14,7 @@ from allauthdemo.auth.models import DemoUser Author: Vincent de Almeida - Created: 11/07/2018 + Created: 11/06/2018 ''' # TODO: Define a validation function that can do back-end verification on top of the front end validation @@ -31,8 +31,6 @@ class CreateNewEventModelAdaptor: identifier = None starts_at = None ends_at = None - min_num_selections = 0 - max_num_selections = 0 organisers = [] trustees = [] voters = [] @@ -47,6 +45,8 @@ class CreateNewEventModelAdaptor: self.form_data = form_data.copy() self.user = user # TODO: Call validation func here (incl functionality for verifying CSRF + reCAPTCHA) + print("Form Data:") + print(self.form_data) self.__extractData() @@ -108,11 +108,6 @@ class CreateNewEventModelAdaptor: else: self.voters.append(EmailUser(email=voter_email)) - - # Extract the min and max number of selections - self.min_num_selections = int(self.form_data.pop('minimum-input')[0]) - self.max_num_selections = int(self.form_data.pop('maximum-input')[0]) - # Create the Event model object - this does not persist it to the DB self.event = Event(start_time=self.starts_at, end_time=self.ends_at, @@ -124,27 +119,35 @@ class CreateNewEventModelAdaptor: def __gen_polls_options_map(self): - # At the time of writing, you can only define one poll at event-creation time + # Get the poll count (the number of poll and options that have been defined) + poll_count = int(self.form_data.pop('poll-count-input')[0]) - # Generate PollOption objects from the option data defined in form_data - options = self.form_data.pop('option-name-input') - poll_options_list = [] + for i in range(poll_count): + # String version of i + i_str = str(i) - for option in options: - if option != '': - poll_options_list.append(PollOption(choice_text=option, votes=0)) + # Generate PollOption objects from the option data defined in form_data + options = self.form_data.pop('option-name-input-' + i_str) + poll_options_list = [] + votes = 0 - # Extract required Poll object data and create a poll with its PollOption objects - text = self.form_data.pop('question-input')[0] - votes = 0 + for option in options: + if option != '': + poll_options_list.append(PollOption(choice_text=option, votes=votes)) - poll = Poll(question_text=text, - total_votes=votes, - min_num_selections=self.min_num_selections, - max_num_selections=self.max_num_selections, - event=self.event) + # Extract required Poll object data and create a poll with its PollOption objects + text = self.form_data.pop('question-name-input-' + i_str)[0] + min_num_selections = int(self.form_data.pop('minimum-input-' + i_str)[0]) + max_num_selections = int(self.form_data.pop('maximum-input-' + i_str)[0]) + + poll = Poll(question_text=text, + total_votes=votes, + min_num_selections=min_num_selections, + max_num_selections=max_num_selections, + event=self.event) + + self.polls_options_map.append([poll, poll_options_list]) - self.polls_options_map.append([poll, poll_options_list]) # Instantiate all the polls and their associated poll options def __get_instantiated_polls(self): @@ -210,8 +213,6 @@ class CreateNewEventModelAdaptor: self.identifier = None self.starts_at = None self.ends_at = None - self.min_num_selections = 0 - self.max_num_selections = 0 self.organisers[:] = [] self.trustees[:] = [] self.voters[:] = [] diff --git a/allauthdemo/templates/polls/create_event.html b/allauthdemo/templates/polls/create_event.html index 479d5eb..e6737ef 100755 --- a/allauthdemo/templates/polls/create_event.html +++ b/allauthdemo/templates/polls/create_event.html @@ -107,132 +107,180 @@ - +
- -
- - - Question / Statement that will be put forward to voters along with the below options. - - - - -
-
- -
- +
- +
- + - - - + + + - - + + - - - - - - - - - - - - - - - - - -
#OptionQuestion / Statement Actions
- 2 - -
- - -
-
- -
-
- Drag and drop to re-order options. + Drag and drop to re-order polls. - +
- -
- -
-
-
- - -
-
- - -
-
- - Minimum and maximum number of option selections that a voter can make for the specified question / statement. - - - - -
-
+ +
@@ -453,5 +501,25 @@
+ + {% endblock %} \ No newline at end of file diff --git a/allauthdemo/templates/polls/event_list.html b/allauthdemo/templates/polls/event_list.html index 14435a4..7963037 100755 --- a/allauthdemo/templates/polls/event_list.html +++ b/allauthdemo/templates/polls/event_list.html @@ -26,7 +26,7 @@ Start Time End Time No. Polls - Edit + Actions @@ -40,7 +40,10 @@ - + + + + {% endfor %} diff --git a/static/css/main.css b/static/css/main.css index 686f783..b184b26 100755 --- a/static/css/main.css +++ b/static/css/main.css @@ -111,6 +111,10 @@ tbody.trustee-formset > tr { margin-bottom: 10px; } +.dialogFormField { + margin-top: 0.65em; +} + div.formset_object { animation: 0.3s drop-intro; } diff --git a/static/js/create-event-poll.js b/static/js/create-event-poll.js index 142f7de..4353980 100755 --- a/static/js/create-event-poll.js +++ b/static/js/create-event-poll.js @@ -7,8 +7,19 @@ var dateRegex = /^[0-9]{4}-(((0[13578]|(10|12))-(0[1-9]|[1-2][0-9]|3[0-1]))|(02- var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; var reCaptchaValid = false; var generalErrorBlock = document.getElementById('all-errors-help-block'); - var errors = []; +var create = true; +var pollCount = 0; + +function finalisePolls() { + // Update the value of the poll count input + $('#poll-count-input').val(pollCount); + + // Remove the empty and hidden poll row from the poll table + var formset = $(".formset[data-formset-prefix='questions']"); + var emptyForm = formset.children('.formset-form-empty'); + emptyForm.remove(); +} $("#election-form").submit(function(e) { // Intercept submission of form and temporarily suspend it @@ -28,6 +39,7 @@ $("#election-form").submit(function(e) { if( formDataValid === true ) { clearErrors(); + finalisePolls(); form.submit(); } else { submitBtn.val(submitBtnErrLabel); @@ -54,12 +66,11 @@ function isFormValid() { var slugValid = isSlugValid(); var voteStartValid = isVoteStartValid(); var voteEndValid = isVoteEndValid(); - var pollOptsValid = isPollAndOptsValid(); var organisersEmailsValid = areOrganisersEmailsValid(); var trusteesEmailsValid = areTrusteesEmailsValid(); var votersListValid = isVotersListValid(); - return nameValid && slugValid && voteStartValid && voteEndValid && pollOptsValid + return nameValid && slugValid && voteStartValid && voteEndValid && organisersEmailsValid && trusteesEmailsValid && votersListValid; } @@ -86,7 +97,7 @@ function isNameValid() { // Based on a list of names supplied from the create_event html template if(events_list !== undefined) { var valid = true; - var event_name = $('#name-input').val(); + var event_name = $('#name-input').val().trim(); if(event_name === '') { checkAndAddError({ @@ -215,33 +226,16 @@ function isDateValid(date_time) { return dateRegex.test(date_time); } -function isPollAndOptsValid() { - var pollValid = true; - var optsValid = true; - var minMaxSelValid = true; - - // Check question is valid - pollValid = isPollValid(); - - // Check opts are valid - optsValid = isPollOptionsValid(); - - // Check min and max selections are valid - minMaxSelValid = isMinMaxSelectionValid(); - - return pollValid && optsValid && minMaxSelValid; -} - -function isPollValid() { +function isPollQValid() { var valid = true; // Check question is valid - var question = $('#question-input').val(); + var question = $('#question-name-input-' + pollCount).val(); if(question === '') { checkAndAddError({ error: "Question / Statement for the poll is blank.", - helpBlockId: "question-input-error-block" + helpBlockId: "question-input-error-block-" + pollCount }); valid = false; @@ -252,8 +246,8 @@ function isPollValid() { function isPollOptionsValid() { var valid = true; - var optsInputs = $('.option-formset #option-name-input'); - var helpBlockId = "options-input-error-block"; + var optsInputs = $('.option-formset #option-name-input-' + pollCount); + var helpBlockId = "options-input-error-block-" + pollCount; var index = 0; var errorStr = "Option "; @@ -284,36 +278,28 @@ function isPollOptionsValid() { return valid; } -$('#question-input').on('input', function (e) { - validateFormField(isPollValid, "question-input-error-block"); -}); - -$('.option-formset #option-name-input').on('input', function(e) { - validateFormField(isPollOptionsValid, "options-input-error-block"); -}); - function isMinMaxSelectionValid() { var valid = true; - var minInput = $('#minimum-input'); + var minInput = $('#minimum-input-' + pollCount); var minInputMinAttr = parseInt(minInput[0].min); var minInputVal = minInput.val(); - var helpBlockId = "selections-input-error-block"; + var helpBlockId = "selections-input-error-block-" + pollCount; var errorStr = ""; - if(minInputVal < minInputMinAttr) { - errorStr = "The minimum option selection cannot be less than " + minInputMinAttr; + if(minInputVal === "" || minInputVal < minInputMinAttr) { + errorStr = "The minimum option selection cannot be less than " + minInputMinAttr + " or blank"; valid = false; } - var maxInput = $('#maximum-input'); + var maxInput = $('#maximum-input-' + pollCount); var maxInputMinAttr = parseInt(maxInput[0].min); var maxInputVal = maxInput.val(); - if(maxInputVal < maxInputMinAttr) { + if(maxInputVal === "" || maxInputVal < maxInputMinAttr) { if(errorStr !== '') { - errorStr = errorStr + " and the maximum cannot be less than " + maxInputMinAttr; + errorStr = errorStr + " and the maximum cannot be less than " + maxInputMinAttr + " or blank"; } else { - errorStr = "The maximum option selection cannot be less than " + maxInputMinAttr; + errorStr = "The maximum option selection cannot be less than " + maxInputMinAttr + " or blank"; } valid = false; @@ -331,10 +317,6 @@ function isMinMaxSelectionValid() { return valid; } -$('#minimum-input, #maximum-input').on('input', function(e) { - validateFormField(isMinMaxSelectionValid, "selections-input-error-block"); -}); - function areOrganisersEmailsValid() { var valid = true; var organiserInputs = $('.organiser-formset #organiser-email-input'); @@ -739,7 +721,7 @@ function update(event, ui) { updateFormset(formset); } -$("#options-input-table, #organisers-input-table, #trustees-input-table").sortable({ +$("#questions-input-table, #organisers-input-table, #trustees-input-table").sortable({ items: "tr", update: update }); @@ -759,7 +741,7 @@ function updateFormset(formset) { // Ported from DEMOS 1. Updates the row number function updateForm(form, formIndex) { // Ported from DEMOS 1. // Specific update for option forms var mayBeTextInput = form.find('input:text')[0]; - if(mayBeTextInput.placeholder !== undefined) { + if(mayBeTextInput !== undefined && mayBeTextInput.placeholder !== undefined) { if( mayBeTextInput.placeholder.indexOf("Candidate") > -1) { mayBeTextInput.placeholder = "Example: Candidate " + (formIndex + 1); } else if (mayBeTextInput.placeholder.indexOf("trusteeX") > -1) { @@ -794,12 +776,107 @@ function manageTotalForms(formset, value) { // Ported from DEMOS1. addButton.prop('disabled', parseInt(totalForms.val()) - removedForms.length >= parseInt(maxNumForms.val())); } +function updateQuestionFormInputs(form) { + // Update the name and IDs of all the dialog input fields + var clonedFields = form.find('.formset-form-fields:first >'); + + // Update the table ID + var table = form.find('.table:first'); + table.attr("id", "options-table-" + pollCount); + + // Update the poll question / statement ID + clonedFields.find(".dialogQ:first") + .attr("id", "question-name-input-" + pollCount) + .attr("name", "question-name-input-" + pollCount); + + // Update one of the help block IDs for various sections of the dialog + var pollQuestionErrorHelpBlock = clonedFields.find("#question-input-error-block"); + pollQuestionErrorHelpBlock.attr("id", "question-input-error-block-" + pollCount); + + var pollOptionsErrorHelpBlock = clonedFields.find("#options-input-error-block"); + pollOptionsErrorHelpBlock.attr("id", "options-input-error-block-" + pollCount); + + var pollSelectionsErrorHelpBlock = clonedFields.find("#selections-input-error-block"); + pollSelectionsErrorHelpBlock.attr("id", "selections-input-error-block-" + pollCount); + + // Update the poll option input IDs + var optsInputs = clonedFields.find(".dialogO"); + + for(var i = 0; i < optsInputs.length; i++) { + var input = optsInputs[i]; + input.id = "option-name-input-" + pollCount; + input.name = "option-name-input-" + pollCount; + } + + // Update the data-formset-prefix for correct referencing + var dataFormsetPrefix = "options-" + pollCount; + var optionFormSet = clonedFields.find(".option-formset"); + optionFormSet.attr("data-formset-prefix", dataFormsetPrefix); + + var addPollOptBtn = clonedFields.find('.formset-add'); + addPollOptBtn.attr("data-formset-prefix", dataFormsetPrefix); + + // Update the poll min and max selection + clonedFields.find(".min-input:first") + .attr("id", "minimum-input-" + pollCount) + .attr("name", "minimum-input-" + pollCount); + + clonedFields.find(".max-input:first") + .attr("id", "maximum-input-" + pollCount) + .attr("name", "maximum-input-" + pollCount); +} + +function isDialogFormValid() { + var pollQValid = true; + var optsValid = true; + var minMaxSelValid = true; + + // Check question is valid + var pollQErrorHelpBlockId = "question-input-error-block-" + pollCount; + pollQValid = isPollQValid(); + + if(pollQValid === true) { + clearError(pollQErrorHelpBlockId); + } else { + highlightError(pollQErrorHelpBlockId); + } + + // Check opts are valid + var pollOptsErrorHelpBlockId = "options-input-error-block-" + pollCount; + optsValid = isPollOptionsValid(); + + if(optsValid === true) { + clearError(pollOptsErrorHelpBlockId); + } else { + highlightError(pollOptsErrorHelpBlockId); + } + + // Check min and max selections are valid + var pollSelErrorHelpBlockId = "selections-input-error-block-" + pollCount; + minMaxSelValid = isMinMaxSelectionValid(); + + if(minMaxSelValid === true) { + clearError(pollSelErrorHelpBlockId); + } else { + highlightError(pollSelErrorHelpBlockId); + } + + return pollQValid && optsValid && minMaxSelValid; +} + $('.formset-add').click(function (e) { // Ported from DEMOS1 var formsetPrefix = $(this).attr('data-formset-prefix'); var formset = $('.formset[data-formset-prefix="' + formsetPrefix + '"]'); var emptyForm = formset.children('.formset-form-empty'); var emptyFormCheckedInputs = emptyForm.find('input:checkbox:checked, input:radio:checked'); var form = emptyForm.clone(true).removeClass('formset-form-empty'); + + // Update the IDs of the inputs of the dialogs based on the number of questions + if(formsetPrefix === "questions") { + // Update the IDs and names of all of the cloned input form fields + updateQuestionFormInputs(form); + } + var formIndex = formset.children('.formset-form:not(.formset-form-empty)').length; formset.append(form); @@ -836,7 +913,7 @@ $('.formset-form-remove').click(function (e) { // Ported from DEMOS1 // Perform validation now that a row has been removed switch (formPrefix) { case 'option': - validateFormField(isPollOptionsValid, "options-input-error-block"); + validateFormField(isPollOptionsValid, "options-input-error-block-" + pollCount); break; case 'organiser': validateFormField(areOrganisersEmailsValid, "organisers-input-error-block"); @@ -845,4 +922,90 @@ $('.formset-form-remove').click(function (e) { // Ported from DEMOS1 validateFormField(areTrusteesEmailsValid, "trustees-input-error-block"); break; } +}); + +$('.formset-form-save').click(function (e) { + var dialogValid = isDialogFormValid(); + + if(dialogValid === true) { + // TODO: Clear errors + var modal = $(this).closest('.modal'); + var form = modal.data('form'); + var name = $('#question-name-input-' + pollCount).val(); + form.find('.formset-form-name:first').text(name); + modal.data('formSave', true); + modal.modal('hide'); + pollCount++; + } else { + //highlightErrors(); + } +}); + +$('.formset-form-edit').click(function (e) { + var form = $(this).closest('.formset-form'); + $('#formset-modal').data('form', form).modal('show'); +}); + +$('#formset-modal').on('show.bs.modal', function (e) { // Ported from DEMOS1 + var modal = $(this); + var modalBody = modal.find('.modal-body > .row > [class^="col-"]'); + var modalTitle = modal.find('.modal-title'); + var form = modal.data('form'); + var formset = form.parent('.formset'); + var formFields = form.find('.formset-form-fields:first >').detach(); + modal.data('formFields', formFields); + + var clonedFields = formFields.clone(true); + modalBody.append(clonedFields); + + modalTitle.text(formset.attr('data-formset-modal-title')); + formset.trigger('formsetModalShow', [modalBody]); + + // Attach an event handler for poll option row sorting + $("#options-table-" + pollCount).sortable({ + items: "tr", + update: update + }); +}); + +$('#formset-modal').on('hide.bs.modal', function (e) { + var modal = $(this); + var modalBody = modal.find('.modal-body > .row > [class^="col-"]'); + var form = modal.data('form'); + var formset = form.parent('.formset'); + if (modal.data('formSave')) { + var formset = form.parent('.formset'); + if (modal.data('formAdd')) { + manageTotalForms(formset, +1); + form.removeClass('hidden'); + } + } else { + if (modal.data('formAdd')) { + form.remove(); + } + } + formset.trigger('formsetModalHide', [modalBody]); +}); + +$('#formset-modal').on('hidden.bs.modal', function (e) { + var modal = $(this); + var modalBody = modal.find('.modal-body > .row > [class^="col-"]'); + var form = modal.data('form'); + var formset = form.parent('.formset'); + var formFields = form.find('.formset-form-fields:first'); + if (modal.data('formSave')) { + formFields.append(modalBody.children().detach()); + if (modal.data('formAdd')) { + formset.trigger('formsetFormAdded', [form]); + } else { + formset.trigger('formsetFormEdited', [form]); + } + } else { + modalBody.empty(); + if (!modal.data('formAdd')) { + formFields.append(modal.data('formFields')); + } + } + modal.find('.modal-title').text(''); + modal.removeData(); }); \ No newline at end of file