Updated create_event view backend to perform full form validation as a backup in the event the front end validation is incorrect and or is disabled. In the event that the back-end validation detects an error, the form is returned with the original data and errors informing the user on what pieces of information need corrected

This commit is contained in:
vince0656 2018-06-26 17:02:36 +01:00
parent e7440e6d6e
commit 5f32806506
7 changed files with 1255 additions and 532 deletions

View file

@ -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

View file

@ -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

View 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

View file

@ -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()
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:")
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
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
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
# 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":
'''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)
'''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)
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)
elif request.method == "GET":
# Render the template
return render(request,
"polls/create_event.html",

View file

@ -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,6 +128,136 @@
</tr>
</thead>
<tbody class="formset questions-formset" data-formset-prefix="polls" data-formset-type="modal" data-formset-modal-title="Add a New Poll">
{% 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>
</td>
</tr>
{% endfor %}
{% else %}
<!-- Poll -->
<tr class="formset-form formset-form-empty hidden" data-formset-form-prefix="poll">
<!-- # -->
@ -206,7 +341,7 @@
</button>
</td>
</tr>
<!-- Option -->
<!-- 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>
@ -264,6 +399,7 @@
</div>
</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,6 +434,49 @@
</tr>
</thead>
<tbody class="formset organiser-formset" data-formset-prefix="organisers" data-formset-type="inline">
{% 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>
@ -336,7 +516,7 @@
</button>
</td>
</tr>
<!-- Organiser -->
<!-- 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>
<!-- # -->
@ -355,6 +535,7 @@
</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,6 +568,49 @@
</tr>
</thead>
<tbody class="formset trustee-formset" data-formset-prefix="trustees" data-formset-type="inline">
{% 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>
@ -425,7 +650,7 @@
</button>
</td>
</tr>
<!-- Trustee -->
<!-- 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>
<!-- # -->
@ -444,6 +669,7 @@
</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>

View file

@ -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>

View file

@ -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;
}