Merge pull request #8 from vincentmdealmeida/BackendValidation
Updated create_event view backend to perform full form validation as …
This commit is contained in:
commit
de9eaa7881
7 changed files with 1255 additions and 532 deletions
|
@ -1,13 +1,8 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import models
|
||||
|
||||
from django import forms
|
||||
|
||||
# Create your models here.
|
||||
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from allauthdemo.auth.models import DemoUser
|
||||
|
@ -31,6 +26,27 @@ class Event(models.Model):
|
|||
c_email = models.CharField(max_length=512, blank=True)
|
||||
trustees = models.CharField(max_length=4096)
|
||||
|
||||
def duration(self):
|
||||
duration_str = self.start_time.strftime("%d-%m-%y %H:%M")
|
||||
duration_str = duration_str + " - " + self.end_time.strftime("%d-%m-%y %H:%M %Z")
|
||||
return duration_str
|
||||
|
||||
def status(self):
|
||||
status_str = ""
|
||||
|
||||
# Get the current date and time to compare against to establish if this is a past, current or
|
||||
# future event
|
||||
present = timezone.now()
|
||||
|
||||
if present >= self.start_time and present <= self.end_time:
|
||||
status_str = "Active"
|
||||
elif present > self.end_time:
|
||||
status_str = "Expired"
|
||||
elif present < self.start_time:
|
||||
status_str = "Future"
|
||||
|
||||
return status_str
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
|
|
@ -1,221 +0,0 @@
|
|||
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
|
||||
from allauthdemo.polls.models import EmailUser
|
||||
from allauthdemo.auth.models import DemoUser
|
||||
|
||||
'''
|
||||
Goal: Convert the new form data (from the updated DEMOS2 UI) returned to '/event/create' into
|
||||
an Event object that can be persisted via a Model to the DB
|
||||
|
||||
Author: Vincent de Almeida
|
||||
|
||||
Created: 11/06/2018
|
||||
'''
|
||||
|
||||
# TODO: Define a validation function that can do back-end verification on top of the front end validation
|
||||
# TODO: Validation can make use of __contains__ from QueryDict:
|
||||
# TODO: https://docs.djangoproject.com/en/2.0/ref/request-response/#django.http.QueryDict
|
||||
|
||||
class CreateNewEventModelAdaptor:
|
||||
# Raw data from form and django
|
||||
form_data = None
|
||||
user = None
|
||||
|
||||
# Extracted form data
|
||||
event_name = None
|
||||
identifier = None
|
||||
starts_at = None
|
||||
ends_at = None
|
||||
organisers = []
|
||||
trustees = []
|
||||
voters = []
|
||||
|
||||
# Each element of the map has a sub array with 2 elements - poll and associated options
|
||||
polls_options_map = []
|
||||
|
||||
# Event Model Object containing all the extracted data
|
||||
event = None
|
||||
|
||||
def __init__(self, form_data, user):
|
||||
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()
|
||||
|
||||
|
||||
def __extractData(self):
|
||||
# Extract name and identifier first
|
||||
self.event_name = self.form_data.pop('name-input')[0]
|
||||
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
|
||||
organisers_list = self.form_data.pop('organiser-email-input')
|
||||
|
||||
for organiser in organisers_list:
|
||||
if organiser != '' and DemoUser.objects.filter(email=organiser).count() == 1:
|
||||
self.organisers.append(DemoUser.objects.filter(email=organiser).get())
|
||||
|
||||
# Extract the list of trustees
|
||||
trustees_list = self.form_data.pop('trustee-email-input')
|
||||
|
||||
for trustee in trustees_list:
|
||||
if trustee != '':
|
||||
if EmailUser.objects.filter(email=trustee).count() == 1:
|
||||
self.trustees.append(EmailUser.objects.filter(email=trustee).get())
|
||||
else:
|
||||
self.trustees.append(EmailUser(email=trustee))
|
||||
|
||||
# Extract the email list of voters
|
||||
voters_csv_string = self.form_data.pop('voters-list-input')[0].replace(' ', '')
|
||||
voters_email_list = voters_csv_string.split(',')
|
||||
|
||||
for voter_email in voters_email_list:
|
||||
if voter_email != '':
|
||||
if EmailUser.objects.filter(email=voter_email).count() == 1:
|
||||
self.voters.append(EmailUser.objects.filter(email=voter_email).get())
|
||||
else:
|
||||
self.voters.append(EmailUser(email=voter_email))
|
||||
|
||||
# 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,
|
||||
title=self.event_name,
|
||||
EID=self.identifier,
|
||||
creator=self.user.first_name + ' ' + self.user.last_name,
|
||||
c_email=self.user.email,
|
||||
trustees=voters_csv_string)
|
||||
|
||||
|
||||
def __gen_polls_options_map(self):
|
||||
# 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])
|
||||
|
||||
for i in range(poll_count):
|
||||
# String version of i
|
||||
i_str = str(i)
|
||||
|
||||
# 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
|
||||
|
||||
for option in options:
|
||||
if option != '':
|
||||
poll_options_list.append(PollOption(choice_text=option, votes=votes))
|
||||
|
||||
# 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])
|
||||
|
||||
|
||||
# Instantiate all the polls and their associated poll options
|
||||
def __get_instantiated_polls(self):
|
||||
polls = []
|
||||
for poll_option_map in self.polls_options_map:
|
||||
poll = poll_option_map[0]
|
||||
poll_options = poll_option_map[1]
|
||||
|
||||
# Save the poll to the db
|
||||
poll.save()
|
||||
|
||||
# Instantiate poll options
|
||||
for option in poll_options:
|
||||
option.question = poll
|
||||
option.save()
|
||||
|
||||
poll.options = poll_options
|
||||
poll.save()
|
||||
|
||||
polls.append(poll)
|
||||
|
||||
return polls
|
||||
|
||||
def updateModel(self):
|
||||
# First thing to do is persist the event object to the db
|
||||
# with basic data before adding things like poll data
|
||||
self.event.save()
|
||||
|
||||
# List of organisers should already be instantiated and present in the db
|
||||
# so it can just be added
|
||||
self.event.users_organisers = self.organisers
|
||||
|
||||
# Add the list of trustees to the event, making sure they're instantiated
|
||||
for trustee in self.trustees:
|
||||
if EmailUser.objects.filter(email=trustee.email).count() == 0:
|
||||
trustee.save()
|
||||
|
||||
self.event.users_trustees = self.trustees
|
||||
|
||||
# Add the list of voters to the event, making sure they're instantiated
|
||||
for voter in self.voters:
|
||||
if EmailUser.objects.filter(email=voter.email).count() == 0:
|
||||
voter.save()
|
||||
|
||||
self.event.voters = self.voters
|
||||
|
||||
# Extract all the poll data for the event and associated poll option data
|
||||
# This can only be done at this point as the event has been persisted
|
||||
self.__gen_polls_options_map()
|
||||
|
||||
# Get the instantiated list of polls which have already instantiated options
|
||||
self.event.polls = self.__get_instantiated_polls()
|
||||
|
||||
self.event.save()
|
||||
|
||||
# Finally perform a data clean up
|
||||
self.__clear_data()
|
||||
|
||||
def __clear_data(self):
|
||||
self.form_data = None
|
||||
self.user = None
|
||||
self.event_name = None
|
||||
self.identifier = None
|
||||
self.starts_at = None
|
||||
self.ends_at = None
|
||||
self.organisers[:] = []
|
||||
self.trustees[:] = []
|
||||
self.voters[:] = []
|
||||
self.polls_options_map[:] = []
|
||||
self.event = None
|
||||
|
677
allauthdemo/polls/utils/EventModelAdaptor.py
Normal file
677
allauthdemo/polls/utils/EventModelAdaptor.py
Normal file
|
@ -0,0 +1,677 @@
|
|||
import re
|
||||
|
||||
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
|
||||
from allauthdemo.polls.models import EmailUser
|
||||
from allauthdemo.auth.models import DemoUser
|
||||
|
||||
'''
|
||||
Goal: Convert the new form data (from the updated DEMOS2 UI) returned to '/event/create' into
|
||||
an Event object that can be persisted via a Model to the DB
|
||||
|
||||
Author: Vincent de Almeida
|
||||
|
||||
Created: 11/06/2018
|
||||
'''
|
||||
|
||||
# TODO: Define a validation function that can do back-end verification on top of the front end validation
|
||||
# TODO: Validation can make use of __contains__ from QueryDict:
|
||||
# TODO: https://docs.djangoproject.com/en/2.0/ref/request-response/#django.http.QueryDict
|
||||
|
||||
class EventModelAdaptor:
|
||||
# Raw data from form and django
|
||||
form_data = None
|
||||
user = None
|
||||
|
||||
# Used for validating the form data
|
||||
form_data_validation = None
|
||||
invalid_form_fields = {}
|
||||
validation_starts_at = None
|
||||
validation_ends_at = None
|
||||
|
||||
# Extracted form data
|
||||
event_name = None
|
||||
identifier = None
|
||||
starts_at = None
|
||||
ends_at = None
|
||||
organisers = []
|
||||
trustees = []
|
||||
voters = []
|
||||
|
||||
# Each element of the map has a sub array with 2 elements - poll and associated options
|
||||
polls_options_map = []
|
||||
|
||||
# Event Model Object containing all the extracted data
|
||||
event = None
|
||||
|
||||
def __init__(self, form_data, user):
|
||||
self.form_data = form_data.copy()
|
||||
self.form_data_validation = form_data.copy()
|
||||
self.user = user
|
||||
|
||||
def isFormDataValid(self, events, demo_users):
|
||||
nameValid = self.__isNameValid(events)
|
||||
identifierValid = self.__isIdentifierValid(events)
|
||||
eventTimingsValid = self.__isEventTimingsValid()
|
||||
pollsValid = self.__arePollsValid()
|
||||
organisersEmailsValid = self.__areOrganisersEmailsValid(demo_users)
|
||||
trusteesEmailsValid = self.__areTrusteeEmailsValid()
|
||||
votersListValid = self.__isVotersListValid()
|
||||
|
||||
return nameValid \
|
||||
and identifierValid \
|
||||
and eventTimingsValid \
|
||||
and pollsValid \
|
||||
and organisersEmailsValid \
|
||||
and trusteesEmailsValid \
|
||||
and votersListValid
|
||||
|
||||
def __isNameValid(self, events):
|
||||
valid = True
|
||||
|
||||
event_name = self.form_data_validation.pop('name-input')[0]
|
||||
|
||||
if event_name == '':
|
||||
self.invalid_form_fields['event_name'] = {'error': 'The event name field is blank.'}
|
||||
valid = False
|
||||
else:
|
||||
for event in events:
|
||||
if event.title == event_name:
|
||||
self.invalid_form_fields['event_name'] = {'error': "The event name '" + event_name + "' is already in use."}
|
||||
valid = False
|
||||
break
|
||||
|
||||
self.invalid_form_fields['event_name_data'] = {'val': event_name}
|
||||
|
||||
return valid
|
||||
|
||||
def __isIdentifierValid(self, events):
|
||||
valid = True
|
||||
|
||||
identifier = self.form_data_validation.pop('identifier-input')[0]
|
||||
|
||||
if identifier == '':
|
||||
self.invalid_form_fields['identifier'] = {'error': 'The event slug field is blank.'}
|
||||
valid = False
|
||||
else:
|
||||
for event in events:
|
||||
if event.EID == identifier:
|
||||
self.invalid_form_fields['identifier'] = {'error': "The event slug '" + identifier + "' is already in use."}
|
||||
valid = False
|
||||
break
|
||||
|
||||
self.invalid_form_fields['identifier_data'] = {'val': identifier}
|
||||
|
||||
return valid
|
||||
|
||||
def __isVoteStartValid(self):
|
||||
valid = True
|
||||
|
||||
# Extract start and end times as string and convert to datetime to perform validation
|
||||
# The UTC offset comes with a colon i.e. '+01:00' which needs to be removed
|
||||
validation_error = "The voting start date and time format is invalid."
|
||||
starts_at_input = self.form_data_validation.pop('vote-start-input')[0]
|
||||
|
||||
if starts_at_input == '':
|
||||
self.invalid_form_fields['starts_at'] = {'error': 'The voting start time is blank.'}
|
||||
return False
|
||||
|
||||
starts_at = starts_at_input
|
||||
starts_at_offset_index = starts_at.find('+')
|
||||
|
||||
if starts_at_offset_index != -1:
|
||||
# timezone data has been supplied so use parse_datetime from django
|
||||
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
|
||||
|
||||
try:
|
||||
starts_at = parse_datetime(starts_at)
|
||||
|
||||
if starts_at is None:
|
||||
self.invalid_form_fields['starts_at'] = {'error': validation_error}
|
||||
valid = False
|
||||
except ValueError:
|
||||
self.invalid_form_fields['starts_at'] = {'error': validation_error}
|
||||
valid = False
|
||||
else:
|
||||
# No Timezone data has been supplied so use strptime instead
|
||||
try:
|
||||
starts_at = datetime.strptime(starts_at, '%Y-%m-%d %H:%M')
|
||||
|
||||
if starts_at is None:
|
||||
self.invalid_form_fields['starts_at'] = {'error': validation_error}
|
||||
valid = False
|
||||
except ValueError:
|
||||
self.invalid_form_fields['starts_at'] = {'error': validation_error}
|
||||
valid = False
|
||||
|
||||
self.validation_starts_at = starts_at
|
||||
self.invalid_form_fields['starts_at_data'] = {'val': starts_at_input}
|
||||
|
||||
return valid
|
||||
|
||||
def __isVoteEndValid(self):
|
||||
valid = True
|
||||
|
||||
validation_error = "The voting end date and time format is invalid."
|
||||
ends_at_input = self.form_data_validation.pop('vote-end-input')[0]
|
||||
|
||||
if ends_at_input == '':
|
||||
self.invalid_form_fields['ends_at'] = {'error': 'The voting end time is blank.'}
|
||||
return False
|
||||
|
||||
ends_at = ends_at_input
|
||||
ends_at_offset_index = ends_at.find('+')
|
||||
|
||||
if ends_at_offset_index != -1:
|
||||
# timezone data has been supplied so use parse_datetime from django
|
||||
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
|
||||
|
||||
try:
|
||||
ends_at = parse_datetime(ends_at)
|
||||
|
||||
if ends_at is None:
|
||||
self.invalid_form_fields['ends_at'] = {'error': validation_error}
|
||||
valid = False
|
||||
except ValueError:
|
||||
self.invalid_form_fields['ends_at'] = {'error': validation_error}
|
||||
valid = False
|
||||
else:
|
||||
# No Timezone data has been supplied so use strptime instead
|
||||
try:
|
||||
ends_at = datetime.strptime(ends_at, '%Y-%m-%d %H:%M')
|
||||
|
||||
if ends_at is None:
|
||||
self.invalid_form_fields['ends_at'] = {'error': validation_error}
|
||||
valid = False
|
||||
except ValueError:
|
||||
self.invalid_form_fields['ends_at'] = {'error': validation_error}
|
||||
valid = False
|
||||
|
||||
# Store the ends_at for further validation as well as the original data val
|
||||
self.validation_ends_at = ends_at
|
||||
self.invalid_form_fields['ends_at_data'] = {'val': ends_at_input}
|
||||
|
||||
return valid
|
||||
|
||||
def __isEventTimingsValid(self):
|
||||
# Ensure that the start and end times are independently valid and then ensure they don't overlap
|
||||
# in an invalid manner
|
||||
voteStartValid = self.__isVoteStartValid()
|
||||
voteEndValid = self.__isVoteEndValid()
|
||||
eventTimingsValid = True
|
||||
|
||||
# Ensure that the start date is before the end date and that the end is after the start
|
||||
if voteStartValid and voteEndValid:
|
||||
if not self.validation_starts_at < self.validation_ends_at and self.validation_ends_at > self.validation_starts_at:
|
||||
self.invalid_form_fields['event_timings'] = {'error': 'The start date must be before the end date and the end after the start date.'}
|
||||
eventTimingsValid = False
|
||||
|
||||
return voteStartValid and voteEndValid and eventTimingsValid
|
||||
|
||||
def __arePollsValid(self):
|
||||
valid = True
|
||||
|
||||
# Get the poll count
|
||||
poll_count = int(self.form_data_validation.pop('poll-count-input')[0])
|
||||
polls_json = []
|
||||
errors_summary = "The following poll # have errors: "
|
||||
|
||||
for i in range(poll_count):
|
||||
# Whether there are errors for this specific poll
|
||||
poll_valid = True
|
||||
|
||||
# JSON representation of the poll
|
||||
poll_json = {}
|
||||
|
||||
# JSON struct for defining errors in the poll
|
||||
poll_errors_json = {}
|
||||
|
||||
# String version of i
|
||||
i_str = str(i)
|
||||
poll_json['no'] = {'val': i_str}
|
||||
|
||||
# Inspect all of the options for this poll
|
||||
options = self.form_data_validation.pop('option-name-input-' + i_str)
|
||||
options_list = []
|
||||
blank_count = 0
|
||||
|
||||
for option in options:
|
||||
if option == '':
|
||||
blank_count += 1
|
||||
else:
|
||||
options_list.append(option)
|
||||
|
||||
# Add back the blank options to the option list not including the hidden one
|
||||
for i in range(blank_count-1):
|
||||
options_list.append("")
|
||||
|
||||
# blank count is expected to be 1 due to the hidden option row that's cloned in the
|
||||
# front end every time a new option is added
|
||||
if blank_count > 1:
|
||||
poll_errors_json['options'] = {'val': "There are " + str(blank_count-1) + " blank poll options"}
|
||||
valid = False
|
||||
poll_valid = False
|
||||
|
||||
poll_json['options'] = {'val': options_list}
|
||||
|
||||
# Ensure that the poll question / statement isn't blank
|
||||
name = self.form_data_validation.pop('question-name-input-' + i_str)[0]
|
||||
|
||||
if name == '':
|
||||
poll_errors_json['name'] = {'val': "The poll name is blank."}
|
||||
valid = False
|
||||
poll_valid = False
|
||||
|
||||
# Record the poll name in the JSON representation of the poll
|
||||
poll_json['name'] = {'val': name}
|
||||
|
||||
# Validate the min max poll option selections
|
||||
min_num_selections_str = self.form_data_validation.pop('minimum-input-' + i_str)[0]
|
||||
max_num_selections_str = self.form_data_validation.pop('maximum-input-' + i_str)[0]
|
||||
errors = ""
|
||||
|
||||
if min_num_selections_str == '':
|
||||
errors = "The minimum selection cannot be blank. "
|
||||
valid = False
|
||||
poll_valid = False
|
||||
else:
|
||||
min_num_selections = None
|
||||
|
||||
try:
|
||||
min_num_selections = int(min_num_selections_str)
|
||||
|
||||
if min_num_selections < 0:
|
||||
errors = "The minimum selection cannot be less than zero. "
|
||||
valid = False
|
||||
poll_valid = False
|
||||
|
||||
if min_num_selections > len(options) - blank_count:
|
||||
if len(errors) > 0:
|
||||
errors = errors + "and it cannot be more than the number of options. "
|
||||
else:
|
||||
errors = "The minimum selection cannot be greater than the number of options. "
|
||||
|
||||
valid = False
|
||||
poll_valid = False
|
||||
|
||||
if max_num_selections_str == '':
|
||||
max_sel_blank_err = "The maximum selection cannot be blank. "
|
||||
|
||||
if len(errors) > 0:
|
||||
errors = errors + max_sel_blank_err
|
||||
else:
|
||||
errors = max_sel_blank_err
|
||||
|
||||
valid = False
|
||||
poll_valid = False
|
||||
else:
|
||||
max_num_selections = None
|
||||
|
||||
try:
|
||||
max_num_selections = int(max_num_selections_str)
|
||||
|
||||
if min_num_selections > max_num_selections:
|
||||
min_gt_max_err = "The minimum selection cannot be greater than the maximum. "
|
||||
if len(errors) > 0:
|
||||
errors = errors + min_gt_max_err
|
||||
else:
|
||||
errors = min_gt_max_err
|
||||
|
||||
valid = False
|
||||
poll_valid = False
|
||||
|
||||
if max_num_selections < 0:
|
||||
max_less_zero_err = "The maximum cannot be less than 0. "
|
||||
|
||||
if len(errors) > 0:
|
||||
errors = errors + max_less_zero_err
|
||||
else:
|
||||
errors = max_less_zero_err
|
||||
|
||||
valid = False
|
||||
poll_valid = False
|
||||
|
||||
if max_num_selections > len(options) - blank_count:
|
||||
max_options_err = "The max number of option selections cannot be more than the number of options."
|
||||
|
||||
if len(errors) > 0:
|
||||
errors = errors + max_options_err
|
||||
else:
|
||||
errors = max_options_err
|
||||
|
||||
valid = False
|
||||
poll_valid = False
|
||||
|
||||
# Record the min max poll option selection values in the JSON rep of the poll
|
||||
poll_json['min_selection'] = {'val': min_num_selections}
|
||||
poll_json['max_selection'] = {'val': max_num_selections}
|
||||
except ValueError:
|
||||
max_opts_input_err = "The maximum option selection input is not valid. "
|
||||
|
||||
if len(errors) > 0:
|
||||
errors = errors + max_opts_input_err
|
||||
else:
|
||||
errors = max_opts_input_err
|
||||
|
||||
valid = False
|
||||
poll_valid = False
|
||||
except ValueError:
|
||||
errors = "The minimum option selection input is not valid."
|
||||
valid = False
|
||||
poll_valid = False
|
||||
|
||||
poll_errors_json['min_max'] = {'val': errors}
|
||||
|
||||
# Store the errors as part of the JSON rep of the poll
|
||||
poll_json['errors'] = {'val': poll_errors_json}
|
||||
|
||||
# Add the poll rep to the list of polls
|
||||
polls_json.append(poll_json)
|
||||
|
||||
# If the validation for the poll has failed, add it to the error summary
|
||||
if not poll_valid:
|
||||
errors_summary = errors_summary + str(i + 1) + " "
|
||||
|
||||
self.invalid_form_fields['polls_data'] = {'val': polls_json}
|
||||
|
||||
if not valid and len(errors_summary) > 34:
|
||||
errors_summary = errors_summary + "and can be corrected by editing them."
|
||||
self.invalid_form_fields['polls_errors'] = {'error': errors_summary}
|
||||
|
||||
return valid
|
||||
|
||||
def __areOrganisersEmailsValid(self, demo_users):
|
||||
valid = True
|
||||
|
||||
# Create a list of emails from the demo users
|
||||
emails = []
|
||||
for user in demo_users:
|
||||
emails.append(user.email)
|
||||
|
||||
# Check that the list of organiser emails are actually valid
|
||||
organisers_list_input = self.form_data_validation.pop('organiser-email-input')
|
||||
organisers_list = []
|
||||
blank_count = 0
|
||||
error = "The following email(s) supplied are not organisers: "
|
||||
|
||||
for organiser in organisers_list_input:
|
||||
if organiser != '':
|
||||
organisers_list.append(organiser)
|
||||
|
||||
if organiser not in emails:
|
||||
error = error + organiser + " "
|
||||
valid = False
|
||||
else:
|
||||
blank_count += 1
|
||||
|
||||
if blank_count > 1:
|
||||
if not valid:
|
||||
error = error + " and there are " + str(blank_count - 1) + " blank organiser inputs."
|
||||
else:
|
||||
error = "There are " + str(blank_count - 1) + " blank organiser inputs."
|
||||
valid = False
|
||||
|
||||
# This adds in blank organisers so that the template can render them for the user to fix
|
||||
for i in range(blank_count - 1):
|
||||
organisers_list.append("")
|
||||
|
||||
if not valid:
|
||||
self.invalid_form_fields['organiser_emails'] = {'error': error}
|
||||
|
||||
self.invalid_form_fields['organiser_emails_data'] = {'val': organisers_list}
|
||||
|
||||
return valid
|
||||
|
||||
def __areTrusteeEmailsValid(self):
|
||||
valid = True
|
||||
|
||||
# Check that the list of trustees is valid
|
||||
trustees_list_input = self.form_data_validation.pop('trustee-email-input')
|
||||
trustees_list = []
|
||||
error = "The following email(s) supplied are not valid: "
|
||||
blank_count = 0
|
||||
|
||||
for trustee in trustees_list_input:
|
||||
if trustee != '':
|
||||
trustees_list.append(trustee)
|
||||
match = re.match(r'[^\s@]+@[^\s@]+\.[^\s@]+', trustee)
|
||||
|
||||
if match is None:
|
||||
error = error + trustee + " "
|
||||
valid = False
|
||||
else:
|
||||
blank_count += 1
|
||||
|
||||
if blank_count > 1:
|
||||
if not valid:
|
||||
error = error + " and there are " + str(blank_count - 1) + " blank trustee inputs."
|
||||
else:
|
||||
error = "There are " + str(blank_count - 1) + " blank trustee inputs."
|
||||
valid = False
|
||||
|
||||
# This adds in blank trustees so that the template can render them for the user to fix
|
||||
for i in range(blank_count - 1):
|
||||
trustees_list.append("")
|
||||
|
||||
if not valid:
|
||||
self.invalid_form_fields['trustee_emails'] = {'error': error}
|
||||
|
||||
self.invalid_form_fields['trustee_emails_data'] = {'val': trustees_list}
|
||||
|
||||
return valid
|
||||
|
||||
def __isVotersListValid(self):
|
||||
valid = True
|
||||
|
||||
# Check that the list of voters is valid
|
||||
voters_csv_string = self.form_data_validation.pop('voters-list-input')[0].replace(' ', '')
|
||||
|
||||
if voters_csv_string == '':
|
||||
self.invalid_form_fields['voters_emails'] = {'error': 'The voters list is blank.'}
|
||||
self.invalid_form_fields['voters_emails_data'] = {'val': voters_csv_string}
|
||||
return False
|
||||
|
||||
voters_email_list = voters_csv_string.split(',')
|
||||
error = "The following email(s) supplied are not valid: "
|
||||
|
||||
for voter_email in voters_email_list:
|
||||
if voter_email != '' and re.match(r'[^\s@]+@[^\s@]+\.[^\s@]+', voter_email) is None:
|
||||
error = error + voter_email + " "
|
||||
valid = False
|
||||
|
||||
if not valid:
|
||||
self.invalid_form_fields['voters_emails'] = {'error': error}
|
||||
|
||||
self.invalid_form_fields['voters_emails_data'] = {'val': voters_csv_string}
|
||||
|
||||
return valid
|
||||
|
||||
def getInvalidFormFields(self):
|
||||
return self.invalid_form_fields
|
||||
|
||||
def extractData(self):
|
||||
# Extract name and identifier first
|
||||
self.event_name = self.form_data.pop('name-input')[0]
|
||||
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:
|
||||
# timezone data has been supplied so use parse_datetime from django
|
||||
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:
|
||||
# No Timezone data has been supplied so use strptime instead
|
||||
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:
|
||||
# timezone data has been supplied so use parse_datetime from django
|
||||
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:
|
||||
# No Timezone data has been supplied so use strptime instead
|
||||
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')
|
||||
|
||||
for organiser in organisers_list:
|
||||
if organiser != '' and DemoUser.objects.filter(email=organiser).count() == 1:
|
||||
self.organisers.append(DemoUser.objects.filter(email=organiser).get())
|
||||
|
||||
# Extract the list of trustees
|
||||
trustees_list = self.form_data.pop('trustee-email-input')
|
||||
|
||||
for trustee in trustees_list:
|
||||
if trustee != '':
|
||||
if EmailUser.objects.filter(email=trustee).count() == 1:
|
||||
self.trustees.append(EmailUser.objects.filter(email=trustee).get())
|
||||
else:
|
||||
self.trustees.append(EmailUser(email=trustee))
|
||||
|
||||
# Extract the email list of voters
|
||||
voters_csv_string = self.form_data.pop('voters-list-input')[0].replace(' ', '')
|
||||
voters_email_list = voters_csv_string.split(',')
|
||||
|
||||
for voter_email in voters_email_list:
|
||||
if voter_email != '':
|
||||
if EmailUser.objects.filter(email=voter_email).count() == 1:
|
||||
self.voters.append(EmailUser.objects.filter(email=voter_email).get())
|
||||
else:
|
||||
self.voters.append(EmailUser(email=voter_email))
|
||||
|
||||
# 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,
|
||||
title=self.event_name,
|
||||
EID=self.identifier,
|
||||
creator=self.user.first_name + ' ' + self.user.last_name,
|
||||
c_email=self.user.email,
|
||||
trustees=voters_csv_string)
|
||||
|
||||
|
||||
def __gen_polls_options_map(self):
|
||||
# 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])
|
||||
|
||||
for i in range(poll_count):
|
||||
# String version of i
|
||||
i_str = str(i)
|
||||
|
||||
# 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
|
||||
|
||||
for option in options:
|
||||
if option != '':
|
||||
poll_options_list.append(PollOption(choice_text=option, votes=votes))
|
||||
|
||||
# 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])
|
||||
|
||||
|
||||
# Instantiate all the polls and their associated poll options
|
||||
def __get_instantiated_polls(self):
|
||||
polls = []
|
||||
for poll_option_map in self.polls_options_map:
|
||||
poll = poll_option_map[0]
|
||||
poll_options = poll_option_map[1]
|
||||
|
||||
# Save the poll to the db
|
||||
poll.save()
|
||||
|
||||
# Instantiate poll options
|
||||
for option in poll_options:
|
||||
option.question = poll
|
||||
option.save()
|
||||
|
||||
poll.options = poll_options
|
||||
poll.save()
|
||||
|
||||
polls.append(poll)
|
||||
|
||||
return polls
|
||||
|
||||
def updateModel(self):
|
||||
# First thing to do is persist the event object to the db
|
||||
# with basic data before adding things like poll data
|
||||
self.event.save()
|
||||
|
||||
# List of organisers should already be instantiated and present in the db
|
||||
# so it can just be added
|
||||
self.event.users_organisers = self.organisers
|
||||
|
||||
# Add the list of trustees to the event, making sure they're instantiated
|
||||
for trustee in self.trustees:
|
||||
if EmailUser.objects.filter(email=trustee.email).count() == 0:
|
||||
trustee.save()
|
||||
|
||||
self.event.users_trustees = self.trustees
|
||||
|
||||
# Add the list of voters to the event, making sure they're instantiated
|
||||
for voter in self.voters:
|
||||
if EmailUser.objects.filter(email=voter.email).count() == 0:
|
||||
voter.save()
|
||||
|
||||
self.event.voters = self.voters
|
||||
|
||||
# Extract all the poll data for the event and associated poll option data
|
||||
# This can only be done at this point as the event has been persisted
|
||||
self.__gen_polls_options_map()
|
||||
|
||||
# Get the instantiated list of polls which have already instantiated options
|
||||
self.event.polls = self.__get_instantiated_polls()
|
||||
|
||||
self.event.save()
|
||||
|
||||
# Finally perform a data clean up
|
||||
self.__clear_data()
|
||||
|
||||
def __clear_data(self):
|
||||
self.form_data = None
|
||||
self.form_data_validation = None
|
||||
self.invalid_form_fields = {}
|
||||
self.validation_starts_at = None
|
||||
self.validation_ends_at = None
|
||||
self.user = None
|
||||
self.event_name = None
|
||||
self.identifier = None
|
||||
self.starts_at = None
|
||||
self.ends_at = None
|
||||
self.organisers[:] = []
|
||||
self.trustees[:] = []
|
||||
self.voters[:] = []
|
||||
self.polls_options_map[:] = []
|
||||
self.event = None
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import urllib
|
||||
import urllib2
|
||||
import json
|
||||
|
||||
from io import StringIO
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect, HttpResponse, Http404
|
||||
from django.http.response import HttpResponseNotAllowed
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.shortcuts import get_object_or_404, render, render_to_response
|
||||
from django.utils import timezone
|
||||
|
@ -15,7 +20,7 @@ from allauthdemo.auth.models import DemoUser
|
|||
from .tasks import create_voters, create_ballots, generate_event_param, generate_combpk, generate_enc, tally_results
|
||||
from .cpp_calls import param, addec, combpk, tally
|
||||
|
||||
from .utils.CreateNewEventModelAdaptor import CreateNewEventModelAdaptor
|
||||
from .utils.EventModelAdaptor import EventModelAdaptor
|
||||
|
||||
class EventListView(generic.ListView):
|
||||
|
||||
|
@ -255,54 +260,56 @@ def manage_questions(request, event_id):
|
|||
else:
|
||||
return HttpResponseNotAllowed()
|
||||
|
||||
def render_invalid(request, events, demo_users, invalid_fields):
|
||||
return render(request,
|
||||
"polls/create_event.html",
|
||||
{
|
||||
"G_R_SITE_KEY": settings.RECAPTCHA_PUBLIC_KEY,
|
||||
"user_email": request.user.email,
|
||||
"events": events,
|
||||
"demo_users": demo_users,
|
||||
"invalid_fields": invalid_fields
|
||||
})
|
||||
|
||||
def create_event(request):
|
||||
#return HttpResponse(param(str(len("lol_age"))))
|
||||
event = Event()
|
||||
# Obtain context data for the rendering of the html template and validation
|
||||
events = Event.objects.all()
|
||||
demo_users = DemoUser.objects.all()
|
||||
|
||||
if request.method == "POST":
|
||||
'''if request.FILES: # if there is a file we should ignore voters...?
|
||||
csvfile = StringIO(request.FILES['votersTextFile'].read().decode('utf-8'))
|
||||
print("got file from request:")
|
||||
'''Perform Google reCAPTCHA validation'''
|
||||
recaptcha_response = request.POST.get('g-recaptcha-response')
|
||||
url = 'https://www.google.com/recaptcha/api/siteverify'
|
||||
values = {
|
||||
'secret': settings.RECAPTCHA_PRIVATE_KEY,
|
||||
'response': recaptcha_response
|
||||
}
|
||||
data = urllib.urlencode(values)
|
||||
req = urllib2.Request(url, data)
|
||||
response = urllib2.urlopen(req)
|
||||
result = json.load(response)
|
||||
|
||||
form = EventForm(request.POST)
|
||||
organiser_formset = OrganiserFormSet(request.POST, prefix="formset_organiser") # incase form fails, we still want to retain formset data
|
||||
trustee_formset = TrusteeFormSet(request.POST, prefix="formset_trustee")
|
||||
if form.is_valid():
|
||||
event = form.save()
|
||||
generate_event_param.delay(event)
|
||||
if request.FILES:
|
||||
print("creating voters")
|
||||
create_voters.delay(csvfile, event) # this will be done on event launch ultimately
|
||||
'''Perform form data validation'''
|
||||
adaptor = EventModelAdaptor(request.POST, request.user)
|
||||
form_data_valid = adaptor.isFormDataValid(events, demo_users)
|
||||
|
||||
'''Process form data based on above results'''
|
||||
if result['success']:
|
||||
if form_data_valid:
|
||||
adaptor.extractData()
|
||||
adaptor.updateModel()
|
||||
|
||||
return HttpResponseRedirect(reverse('polls:index'))
|
||||
else:
|
||||
invalid_fields = adaptor.getInvalidFormFields()
|
||||
return render_invalid(request, events, demo_users, invalid_fields)
|
||||
|
||||
if organiser_formset.is_valid():
|
||||
#event.users_organisers.clear()
|
||||
for oform in organiser_formset:
|
||||
if (oform.cleaned_data.get('email')):
|
||||
event.users_organisers.add(DemoUser.objects.get(email=oform.cleaned_data['email']))
|
||||
event.users_organisers.add(request.user) # always add editor/creator
|
||||
if trustee_formset.is_valid():
|
||||
#event.users_trustees.clear()
|
||||
for tform in trustee_formset:
|
||||
if (tform.cleaned_data.get('email')):
|
||||
event.users_trustees.add(EmailUser.objects.get_or_create(email=tform.cleaned_data['email'])[0])
|
||||
return HttpResponseRedirect('/event/' + str(event.id) + '/create/poll') # change to reverse format
|
||||
else:
|
||||
invalid_fields = adaptor.getInvalidFormFields()
|
||||
invalid_fields['recaptcha'] = {'error': 'The reCAPTCHA server validation failed, please try again.'}
|
||||
return render_invalid(request, events, demo_users, invalid_fields)
|
||||
|
||||
|
||||
return render(request, "polls/create_event.html", {"event": event, "form": form, "organiser_formset": organiser_formset, "trustee_formset": trustee_formset})'''
|
||||
|
||||
adaptor = CreateNewEventModelAdaptor(request.POST, request.user)
|
||||
adaptor.updateModel()
|
||||
|
||||
# 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(reverse('polls:index'))
|
||||
elif request.method == "GET":
|
||||
# Obtain context data for the rendering of the html template
|
||||
events = Event.objects.all()
|
||||
demo_users = DemoUser.objects.all()
|
||||
|
||||
# Render the template
|
||||
return render(request,
|
||||
"polls/create_event.html",
|
||||
|
|
|
@ -44,12 +44,13 @@
|
|||
<div class="form-group">
|
||||
<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 input-control" id="name-input" placeholder="Example: EU Election" name="name-input" maxlength="255">
|
||||
<input type="text" class="form-control input-control" id="name-input" placeholder="Example: EU Election" name="name-input" maxlength="255" {% if invalid_fields %}value="{{ invalid_fields.event_name_data.val }}"{% endif %}>
|
||||
<span id="name-input-hint-block" class="help-block">
|
||||
A short and clear name.
|
||||
</span>
|
||||
<span id="name-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.event_name %}{{ invalid_fields.event_name.error }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,12 +58,13 @@
|
|||
<div class="form-group">
|
||||
<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 input-control" id="identifier-input" placeholder="Example: eu-election" name="identifier-input" maxlength="255">
|
||||
<input type="text" class="form-control input-control" id="identifier-input" placeholder="Example: eu-election" name="identifier-input" maxlength="255" {% if invalid_fields %}value="{{ invalid_fields.identifier_data.val }}"{% endif %}>
|
||||
<span id="identifier-input-help-block" class="help-block">
|
||||
Used in the election URL, it must only consist of letters, numbers, underscores or hyphens; no whitespace is permitted.
|
||||
</span>
|
||||
<span id="identifier-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.identifier %}{{ invalid_fields.identifier.error }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -71,7 +73,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 input-control" data-date-format="YYYY-MM-DD H:mm Z" 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" {% if invalid_fields %}value="{{ invalid_fields.starts_at_data.val }}"{% endif %}>
|
||||
<span class="input-group-addon btn">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
/
|
||||
|
@ -83,6 +85,7 @@
|
|||
</span>
|
||||
<span id="vote-start-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.starts_at %}{{ invalid_fields.starts_at.error }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -91,7 +94,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 input-control" data-date-format="YYYY-MM-DD H:mm Z" 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" {% if invalid_fields %}value="{{ invalid_fields.ends_at_data.val }}"{% endif %}>
|
||||
<span class="input-group-addon btn">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
/
|
||||
|
@ -103,9 +106,11 @@
|
|||
</span>
|
||||
<span id="vote-end-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.ends_at %}{{ invalid_fields.ends_at.error }}{% endif %}
|
||||
</span>
|
||||
<span id="event-timings-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.event_timings %}{{ invalid_fields.event_timings.error }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -123,147 +128,278 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="formset questions-formset" data-formset-prefix="polls" data-formset-type="modal" data-formset-modal-title="Add a New Poll">
|
||||
<!-- Poll -->
|
||||
<tr class="formset-form formset-form-empty hidden" data-formset-form-prefix="poll">
|
||||
<!-- # -->
|
||||
<td class="formset-form-index text-center" scope=row>
|
||||
1
|
||||
</td>
|
||||
<!-- Question / Statement Label -->
|
||||
<th class="formset-form-name">
|
||||
<!-- Q Label Goes Here -->
|
||||
</th>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-edit" aria-label="Edit">
|
||||
<i class="fa fa-pencil-square-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="hidden">
|
||||
<div class="formset-form-fields">
|
||||
<!-- Name -->
|
||||
<div class="form-group dialogFormField">
|
||||
<label for="question-name-input">Question / Statement</label>
|
||||
<input type="text" class="form-control dialogQ" id="question-name-input" name="question-name-input" placeholder="Example: Elections for the European Parliament" maxlength="200">
|
||||
<span id="question-name-input-help-block" class="help-block">
|
||||
Question / Statement that will be put forward to voters along with the below options.
|
||||
</span>
|
||||
<span id="question-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
</span>
|
||||
</div>
|
||||
<!-- Options -->
|
||||
<div class="form-group dialogFormField">
|
||||
<label for="options-table">Options</label>
|
||||
<table id="options-table" class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">#</th>
|
||||
<th>Option</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sort" class="formset option-formset" data-formset-prefix="options" data-formset-type="inline">
|
||||
<!-- Option -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="option">
|
||||
<!-- # -->
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
1
|
||||
</th>
|
||||
<!-- Option Label -->
|
||||
<td>
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control dialogO" placeholder="Example: Candidate 1" id="option-name-input" name="option-name-input" maxlength="200">
|
||||
</div>
|
||||
</td>
|
||||
<!-- Delete Action -->
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Option -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="option">
|
||||
<!-- # -->
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
2
|
||||
</th>
|
||||
<!-- Option Label -->
|
||||
<td>
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control dialogO" placeholder="Example: Candidate 2" id="option-name-input" name="option-name-input" maxlength="200">
|
||||
</div>
|
||||
</td>
|
||||
<!-- Delete Action -->
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Option -->
|
||||
<tr class="formset-form sorting-row formset-form-empty hidden" data-formset-form-prefix="option">
|
||||
<!-- # -->
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
X
|
||||
</th>
|
||||
<!-- Option Label -->
|
||||
<td>
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control dialogO" placeholder="Example: Candidate X" id="option-name-input" name="option-name-input" maxlength="200">
|
||||
</div>
|
||||
</td>
|
||||
<!-- Delete Action -->
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="clearfix">
|
||||
<button type="button" class="btn btn-primary formset-add" data-formset-prefix="options">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add Poll Option
|
||||
</button>
|
||||
{% if invalid_fields %}
|
||||
{% for poll in invalid_fields.polls_data.val %}
|
||||
<!-- Poll -->
|
||||
<tr class="formset-form" data-formset-form-prefix="poll">
|
||||
<!-- # -->
|
||||
<td class="formset-form-index text-center" scope=row>
|
||||
{{ forloop.counter }}
|
||||
</td>
|
||||
<!-- Question / Statement Label -->
|
||||
<th class="formset-form-name">
|
||||
{{ poll.name.val }}
|
||||
</th>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-edit" aria-label="Edit">
|
||||
<i class="fa fa-pencil-square-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="hidden">
|
||||
<div class="formset-form-fields">
|
||||
<!-- Name -->
|
||||
<div class="form-group dialogFormField">
|
||||
<label for="question-name-input">Question / Statement</label>
|
||||
<input type="text" class="form-control dialogQ" id="question-name-input-{{ poll.no.val }}" name="question-name-input-{{ poll.no.val }}" placeholder="Example: Elections for the European Parliament" maxlength="200" value="{{ poll.name.val }}">
|
||||
<span id="question-name-input-help-block" class="help-block">
|
||||
Question / Statement that will be put forward to voters along with the below options.
|
||||
</span>
|
||||
<span id="question-input-error-block-{{ poll.no.val }}" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if poll.errors.val.name %}{{ poll.errors.val.name.val }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Options -->
|
||||
<div class="form-group dialogFormField">
|
||||
<label for="options-table">Options</label>
|
||||
<table id="options-table" class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">#</th>
|
||||
<th>Option</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sort" class="formset option-formset" data-formset-prefix="options" data-formset-type="inline">
|
||||
{% for option in poll.options.val %}
|
||||
<!-- Option -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="option">
|
||||
<!-- # -->
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
{{ forloop.counter }}
|
||||
</th>
|
||||
<!-- Option Label -->
|
||||
<td>
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control dialogO" placeholder="Example: Candidate 1" id="option-name-input-{{ poll.no.val }}" name="option-name-input-{{ poll.no.val }}" maxlength="200" value="{{ option }}">
|
||||
</div>
|
||||
</td>
|
||||
<!-- Delete Action -->
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<!-- Option: Hidden -->
|
||||
<tr class="formset-form sorting-row formset-form-empty hidden" data-formset-form-prefix="option">
|
||||
<!-- # -->
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
X
|
||||
</th>
|
||||
<!-- Option Label -->
|
||||
<td>
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control dialogO" placeholder="Example: Candidate X" id="option-name-input-{{ poll.no.val }}" name="option-name-input-{{ poll.no.val }}" maxlength="200">
|
||||
</div>
|
||||
</td>
|
||||
<!-- Delete Action -->
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="clearfix">
|
||||
<button type="button" class="btn btn-primary formset-add" data-formset-prefix="options">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add Poll Option
|
||||
</button>
|
||||
</div>
|
||||
<span id="question-input-help-block" class="help-block">
|
||||
Drag and drop to re-order options.
|
||||
</span>
|
||||
<span id="options-input-error-block-{{ poll.no.val }}" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if poll.errors.val.options %}{{ poll.errors.val.options.val }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Number of option selections -->
|
||||
<div class="form-group dialogFormField">
|
||||
<label for="selections-input" class="control-label">Number of Selections:</label> <!-- This text can be a template variable -->
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<label class="sr-only" for="minimum-input">Minimum</label>
|
||||
<input type="number" class="form-control min-input" id="minimum-input-{{ poll.no.val }}" placeholder="Minimum" value="{{ poll.min_selection.val }}" name="minimum-input-{{ poll.no.val }}" min="0" max="{{ poll.options.val.length }}"> <!-- Max is the default number of options initially displayed (2) -->
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<label class="sr-only" for="maximum-input">Maximum</label>
|
||||
<input type="number" class="form-control max-input" id="maximum-input-{{ poll.no.val }}" placeholder="Maximum" value="{{ poll.max_selection.val }}" name="maximum-input-{{ poll.no.val }}" min="1" max="{{ poll.options.val.length }}"> <!-- Max is the default number of options initially displayed (2) -->
|
||||
</div>
|
||||
</div>
|
||||
<span id="question-input-help-block" class="help-block">
|
||||
Minimum and maximum number of option selections that a voter can make for the specified question / statement.
|
||||
</span>
|
||||
<span id="selections-input-error-block-{{ poll.no.val }}" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if poll.errors.val.min_max %}{{ poll.errors.val.min_max.val }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span id="question-input-help-block" class="help-block">
|
||||
Drag and drop to re-order options.
|
||||
</span>
|
||||
<span id="options-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
</span>
|
||||
</div>
|
||||
<!-- Number of option selections -->
|
||||
<div class="form-group dialogFormField">
|
||||
<label for="selections-input" class="control-label">Number of Selections:</label> <!-- This text can be a template variable -->
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<label class="sr-only" for="minimum-input">Minimum</label>
|
||||
<input type="number" class="form-control min-input" id="minimum-input" placeholder="Minimum" value="" name="minimum-input" min="0" max="2"> <!-- Max is the default number of options initially displayed (2) -->
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<label class="sr-only" for="maximum-input">Maximum</label>
|
||||
<input type="number" class="form-control max-input" id="maximum-input" placeholder="Maximum" value="" name="maximum-input" min="1" max="2"> <!-- Max is the default number of options initially displayed (2) -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<!-- Poll -->
|
||||
<tr class="formset-form formset-form-empty hidden" data-formset-form-prefix="poll">
|
||||
<!-- # -->
|
||||
<td class="formset-form-index text-center" scope=row>
|
||||
1
|
||||
</td>
|
||||
<!-- Question / Statement Label -->
|
||||
<th class="formset-form-name">
|
||||
<!-- Q Label Goes Here -->
|
||||
</th>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-edit" aria-label="Edit">
|
||||
<i class="fa fa-pencil-square-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="hidden">
|
||||
<div class="formset-form-fields">
|
||||
<!-- Name -->
|
||||
<div class="form-group dialogFormField">
|
||||
<label for="question-name-input">Question / Statement</label>
|
||||
<input type="text" class="form-control dialogQ" id="question-name-input" name="question-name-input" placeholder="Example: Elections for the European Parliament" maxlength="200">
|
||||
<span id="question-name-input-help-block" class="help-block">
|
||||
Question / Statement that will be put forward to voters along with the below options.
|
||||
</span>
|
||||
<span id="question-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
</span>
|
||||
</div>
|
||||
<!-- Options -->
|
||||
<div class="form-group dialogFormField">
|
||||
<label for="options-table">Options</label>
|
||||
<table id="options-table" class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">#</th>
|
||||
<th>Option</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sort" class="formset option-formset" data-formset-prefix="options" data-formset-type="inline">
|
||||
<!-- Option -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="option">
|
||||
<!-- # -->
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
1
|
||||
</th>
|
||||
<!-- Option Label -->
|
||||
<td>
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control dialogO" placeholder="Example: Candidate 1" id="option-name-input" name="option-name-input" maxlength="200">
|
||||
</div>
|
||||
</td>
|
||||
<!-- Delete Action -->
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Option -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="option">
|
||||
<!-- # -->
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
2
|
||||
</th>
|
||||
<!-- Option Label -->
|
||||
<td>
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control dialogO" placeholder="Example: Candidate 2" id="option-name-input" name="option-name-input" maxlength="200">
|
||||
</div>
|
||||
</td>
|
||||
<!-- Delete Action -->
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Option: Hidden -->
|
||||
<tr class="formset-form sorting-row formset-form-empty hidden" data-formset-form-prefix="option">
|
||||
<!-- # -->
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
X
|
||||
</th>
|
||||
<!-- Option Label -->
|
||||
<td>
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control dialogO" placeholder="Example: Candidate X" id="option-name-input" name="option-name-input" maxlength="200">
|
||||
</div>
|
||||
</td>
|
||||
<!-- Delete Action -->
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="clearfix">
|
||||
<button type="button" class="btn btn-primary formset-add" data-formset-prefix="options">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add Poll Option
|
||||
</button>
|
||||
</div>
|
||||
<span id="question-input-help-block" class="help-block">
|
||||
Drag and drop to re-order options.
|
||||
</span>
|
||||
<span id="options-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
</span>
|
||||
</div>
|
||||
<!-- Number of option selections -->
|
||||
<div class="form-group dialogFormField">
|
||||
<label for="selections-input" class="control-label">Number of Selections:</label> <!-- This text can be a template variable -->
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<label class="sr-only" for="minimum-input">Minimum</label>
|
||||
<input type="number" class="form-control min-input" id="minimum-input" placeholder="Minimum" value="" name="minimum-input" min="0" max="2"> <!-- Max is the default number of options initially displayed (2) -->
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<label class="sr-only" for="maximum-input">Maximum</label>
|
||||
<input type="number" class="form-control max-input" id="maximum-input" placeholder="Maximum" value="" name="maximum-input" min="1" max="2"> <!-- Max is the default number of options initially displayed (2) -->
|
||||
</div>
|
||||
</div>
|
||||
<span id="question-input-help-block" class="help-block">
|
||||
Minimum and maximum number of option selections that a voter can make for the specified question / statement.
|
||||
</span>
|
||||
<span id="selections-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
</span>
|
||||
</div>
|
||||
<span id="question-input-help-block" class="help-block">
|
||||
Minimum and maximum number of option selections that a voter can make for the specified question / statement.
|
||||
</span>
|
||||
<span id="selections-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="clearfix">
|
||||
|
@ -277,6 +413,7 @@
|
|||
</span>
|
||||
<span id="polls-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.polls_errors %}{{ invalid_fields.polls_errors.error }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -297,64 +434,108 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="formset organiser-formset" data-formset-prefix="organisers" data-formset-type="inline">
|
||||
<!-- Organiser -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="organiser">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
1
|
||||
</th>
|
||||
<td>
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<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">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<!-- Action -->
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Organiser -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="organiser">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
2
|
||||
</th>
|
||||
<td>
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<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">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Organiser -->
|
||||
<tr class="formset-form sorting-row formset-form-empty hidden" data-formset-form-prefix="organiser">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
X
|
||||
</th>
|
||||
<td>
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control" placeholder="Example: organiserX@example.com" id="organiser-email-input" name="organiser-email-input" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% if invalid_fields %}
|
||||
{% for organiser in invalid_fields.organiser_emails_data.val %}
|
||||
<!-- Organiser -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="organiser">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
{{ forloop.counter }}
|
||||
</th>
|
||||
<td>
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control" placeholder="Example: organiser@example.com" id="organiser-email-input" name="organiser-email-input" value="{{ organiser }}" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<!-- Action -->
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<!-- Organiser: Hidden -->
|
||||
<tr class="formset-form sorting-row formset-form-empty hidden" data-formset-form-prefix="organiser">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
X
|
||||
</th>
|
||||
<td>
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control" placeholder="Example: organiserX@example.com" id="organiser-email-input" name="organiser-email-input" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<!-- Organiser -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="organiser">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
1
|
||||
</th>
|
||||
<td>
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<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">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<!-- Action -->
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Organiser -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="organiser">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
2
|
||||
</th>
|
||||
<td>
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<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">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Organiser: Hidden -->
|
||||
<tr class="formset-form sorting-row formset-form-empty hidden" data-formset-form-prefix="organiser">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
X
|
||||
</th>
|
||||
<td>
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<!-- TODO: Add an invisible screen reader label to associate with this and other inputs -->
|
||||
<input type="text" class="form-control input-sm input-control" placeholder="Example: organiserX@example.com" id="organiser-email-input" name="organiser-email-input" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="clearfix">
|
||||
|
@ -368,6 +549,7 @@
|
|||
</span>
|
||||
<span id="organisers-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.organiser_emails %}{{ invalid_fields.organiser_emails.error }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -386,64 +568,108 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="formset trustee-formset" data-formset-prefix="trustees" data-formset-type="inline">
|
||||
<!-- Trustee -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="trustee">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
1
|
||||
</th>
|
||||
<td>
|
||||
<!-- 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 input-control" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" value="{{ user_email }}" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<!-- Action -->
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Trustee -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="trustee">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
2
|
||||
</th>
|
||||
<td>
|
||||
<!-- 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 input-control" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Trustee -->
|
||||
<tr class="formset-form sorting-row formset-form-empty hidden" data-formset-form-prefix="trustee">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
X
|
||||
</th>
|
||||
<td>
|
||||
<!-- 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 input-control" placeholder="Example: trusteeX@example.com" id="trustee-email-input" name="trustee-email-input" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% if invalid_fields %}
|
||||
{% for trustee in invalid_fields.trustee_emails_data.val %}
|
||||
<!-- Trustee -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="trustee">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
{{ forloop.counter }}
|
||||
</th>
|
||||
<td>
|
||||
<!-- 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 input-control" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" value="{{ trustee }}" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<!-- Action -->
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<!-- Trustee: Hidden -->
|
||||
<tr class="formset-form sorting-row formset-form-empty hidden" data-formset-form-prefix="trustee">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
X
|
||||
</th>
|
||||
<td>
|
||||
<!-- 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 input-control" placeholder="Example: trusteeX@example.com" id="trustee-email-input" name="trustee-email-input" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<!-- Trustee -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="trustee">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
1
|
||||
</th>
|
||||
<td>
|
||||
<!-- 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 input-control" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" value="{{ user_email }}" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<!-- Action -->
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Trustee -->
|
||||
<tr class="formset-form sorting-row" data-formset-form-prefix="trustee">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
2
|
||||
</th>
|
||||
<td>
|
||||
<!-- 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 input-control" placeholder="Example: trustee@example.com" id="trustee-email-input" name="trustee-email-input" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Trustee: Hidden -->
|
||||
<tr class="formset-form sorting-row formset-form-empty hidden" data-formset-form-prefix="trustee">
|
||||
<th class="formset-form-index text-center" scope=row>
|
||||
<!-- # -->
|
||||
X
|
||||
</th>
|
||||
<td>
|
||||
<!-- 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 input-control" placeholder="Example: trusteeX@example.com" id="trustee-email-input" name="trustee-email-input" maxlength="255">
|
||||
</div>
|
||||
</td>
|
||||
<td class="formset-form-actions text-center">
|
||||
<button type="button" class="btn btn-sm btn-default formset-form-remove" aria-label="Remove">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="clearfix">
|
||||
|
@ -457,6 +683,7 @@
|
|||
</span>
|
||||
<span id="trustees-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.trustee_emails %}{{ invalid_fields.trustee_emails.error }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -465,7 +692,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 input-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">{% if invalid_fields %}{{ invalid_fields.voters_emails_data.val }}{% endif %}</textarea>
|
||||
<span id="voters-list-input-help-block" class="help-block">
|
||||
Manually enter email addresses separated with commas. Alternatively, you can also upload a CSV file:
|
||||
</span>
|
||||
|
@ -477,6 +704,7 @@
|
|||
<h4 id="result" class="hidden successText"></h4>
|
||||
<span id="voters-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.voters_emails %}{{ invalid_fields.voters_emails.error }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -491,6 +719,10 @@
|
|||
<span id="recaptcha-input-help-block" class="help-block">
|
||||
Tick the box to prove that you're not a robot.
|
||||
</span>
|
||||
<span id="recaptcha-input-error-block" class="help-block errorText">
|
||||
<!-- Errors flagged here -->
|
||||
{% if invalid_fields.recaptcha %}{{ invalid_fields.recaptcha.error }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
|
|
@ -23,10 +23,10 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">Event</th>
|
||||
<th class="text-center">Start Time</th>
|
||||
<th class="text-center">End Time</th>
|
||||
<th class="text-center">Duration</th>
|
||||
<th class="text-center">No. Polls</th>
|
||||
<th class="text-center">Actions</th>
|
||||
<th class="text-center">Status</th>
|
||||
<!-- Could also add a delete column to easily remove an event -->
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -34,8 +34,7 @@
|
|||
{% for event in object_list %}
|
||||
<tr>
|
||||
<td class="text-center"><a href="{% url 'polls:view-event' event.id %}">{{ event.title }}</a></td>
|
||||
<td class="text-center">{{ event.start_time }}</td>
|
||||
<td class="text-center">{{ event.end_time }}</td>
|
||||
<td class="text-center">{{ event.duration }}</td>
|
||||
<td class="text-center">{{ event.polls.count }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'polls:edit-event' event.id %}">
|
||||
|
@ -45,6 +44,14 @@
|
|||
<span class="btn btn-default glyphicon glyphicon-trash"></span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="btn statusBtn
|
||||
{% if event.status == 'Expired' %}btn-danger{% endif %}
|
||||
{% if event.status == 'Active' %}btn-success{% endif %}
|
||||
{% if event.status == 'Future' %}btn-info{% endif %}">
|
||||
{{ event.status }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -98,6 +98,7 @@
|
|||
/* Formsets */
|
||||
|
||||
.form-group table {
|
||||
border-top: 2px solid #ddd;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
|
@ -155,6 +156,10 @@ input[type="file"] {
|
|||
|
||||
/* Events List page */
|
||||
|
||||
.statusBtn {
|
||||
width: 74px;
|
||||
}
|
||||
|
||||
.marginTopEventList {
|
||||
margin-top: 5.75em;
|
||||
}
|
||||
|
|
Reference in a new issue