-
Notifications
You must be signed in to change notification settings - Fork 5
Descripción del sistema Votaciones
En esta sección se dará una explicación sobre el subsistema de votaciones, todo lo relacionado con su función dentro de la aplicación, su desarrollo para llevar a cabo los distintos cambios propuestos y la implementación de una serie de pruebas para comprobar el correcto funcionamiento del sistema en conjunto.
El subsistema de votaciones es un conjunto de operaciones que se llevan a cabo para poder realizar una votación dentro de la aplicación, la cuál conlleva la creación de diferentes tipos de preguntas con opciones disponibles para poder votar en cualquiera de ellas. Es un subsistema bastante completo, ya que la aplicación depende en gran parte de éste, porque es lógico que sin votaciones no se podría realizar nada interesante.
El funcionamiento de este subsistema se corresponde de la siguiente forma: el usuario crea una votación, donde debe rellenar los campos de nombre y descripción inicialmente, y necesitará crear una pregunta con distintas opciones para asignársela a la votación, además de introducir un campo auth para asignar esa votación al usuario que está autenticado creando la votación. Una vez realizada la votación, se envía a Cabina para poder realizar el proceso del voto por parte del usuario, y acto seguido, el módulo Mixnet se encarga de encriptar mediante el cifrado ElGamal, el cuál envía sus votos encriptados a Cabina para terminar con el proceso de votación. Cuando ya hemos votado, estos votos pasan al módulo Store, donde se realizarán una serie de procesos para guardar las votaciones y dar paso al subsistema de Votaciones, para acceder a estas y obtener un recuento de los votos, donde se hará uso de nuevo del módulo Mixnet para poder desencriptar los votos para poder hacer el recuento sin problemas. Para finalizar, se envía este recuento al subsistema Postprocesado para que puedan realizar las estadísticas de los votos.
Antes de comenzar con la explicación del desarrollo por parte del subsistema de Votaciones, vamos a introducir los cambios que se han realizado en la aplicación como modificación del proyecto base de Decide.
- Cambios realizados en el proyecto
Los cambios realizados en el proyecto son los siguientes:
- Impedir creación de dos votaciones con el mismo nombre
- Implementar múltiples tipos de votación
- Opción de incluirse en el Censo
- Nombre del usuario que ha iniciado la votación
- Impedir creación de preguntas con la misma descripción
- Cambiar filtro de votaciones por fecha
- Implementar mensaje emergente al finalizar votación con los datos de la misma
- Añadir pregunta con respuestas por orden de preferencia
- Soporte a recuento por paridad (postprocesado -> votaciones)
- Soporte a múltiples preguntas (postprocesado -> votaciones)
- Soporte a preguntas por orden de preferencia y soporte a recuentos electorales proporcionales (postprocesado -> votaciones)
- Soporte a incluir distintos tipos de recuentos para las votaciones (postprocesado -> votaciones)
- Soporte al nuevo guardado de votos (cabina -> votaciones)
- Soporte a corrección de error en el modelo (Visualización -> Votaciones)
- Soporte a parámetro que identifique el tipo de recuento de votos (postprocesado -> votaciones)
Una vez detallados los cambios que se han realizado al proyecto base y explicado todo el proceso que sigue el subsistema de Votaciones, se procede a explicar toda la implementación nueva.
Para crear las votaciones, se necesitan permisos de administrador, por lo que se debe tener un usuario administrador registrado y autenticado en la aplicación. Para crear la votación, se deben tener en cuenta los siguientes atributos, especificados en el modelo Voting
, con el siguiente formato:
class Voting(models.Model):
name = models.CharField(max_length=200, unique = True)
desc = models.TextField(blank=True, null=True)
question = models.ManyToManyField(Question, related_name='voting')
points = models.PositiveIntegerField(default="1")
start_date = models.DateTimeField(blank=True, null=True)
end_date = models.DateTimeField(blank=True, null=True)
started_by = models.CharField(max_length=200, blank=True, null = True)
pub_key = models.OneToOneField(Key, related_name='voting', blank=True, null=True, on_delete=models.SET_NULL)
auths = models.ManyToManyField(Auth, related_name='votings')
tally = JSONField(blank=True, null=True)
tallyM = JSONField(blank=True, null=True)
tallyF = JSONField(blank=True, null=True)
postproc = JSONField(blank=True, null=True)
Hay que tener en cuenta varios detalles de estos atributos:
- El atributo
name
debe ser único, para evitar distintas votaciones con el mismo nombre - El atributo
question
es una relación con el modeloQuestion
, el cuál explicaremos sus atributos más adelante - El atributo
points
se utiliza para hacer recuentos proporcionales, necesarios para el subsistema de Postprocesado - Los atributos
tally
,tallyM
,tallyF
se usan para hacer el recuento de los votos totales, y también para hacer el recuento de votos masculinos y femeninos. Las votaciones requieren de preguntas con opciones para formarlas, por lo que se necesitarán dos objetos: el modeloQuestion
yQuestionOption
.
class Question(models.Model):
ANSWER_TYPES = ((1, "Unique option"), (2,"Multiple option"), (3,"Rank order scale"))
option_types = models.PositiveIntegerField(choices=ANSWER_TYPES, default="1")
desc = models.TextField(unique=True)
ANSWER_TYPES_VOTING = ((0, "IDENTITY"), (1, "BORDA"), (2, "HONDT"),
(3, "EQUALITY"), (4, "SAINTE_LAGUE"), (5, "DROOP"),
(6, "IMPERIALI"), (7, "HARE"))
type = models.PositiveIntegerField(choices=ANSWER_TYPES_VOTING, default="0")
def clean(self):
if self.option_types == 3 and not self.type == 1:
raise ValidationError(('Rank order scale option type must be selected with Borda type.'))
def __str__(self):
return self.desc
class QuestionOption(models.Model):
question = models.ForeignKey(Question, related_name='options', on_delete=models.CASCADE)
number = models.PositiveIntegerField(blank=True, null=True)
option = models.CharField(max_length=200)
def save(self):
if not self.number:
self.number = self.question.options.count() + 2
return super().save()
def __str__(self):
return '{} ({})'.format(self.option, self.number)
En el modelo Question
, existen 2 atributos importantes: el answer_types
, donde se indica de qué tipo es la pregunta, y el answer_types_voting
, para indicar el tipo de recuento de los votos. Indicar también que el atributo desc
debe ser único, para que no existan distintas preguntas con misma descripción.
Para el atributo answer_types
, se utilizan 3 tipos de pregunta:
- Opción única, donde el votante solo podrá escoger una opción de las disponibles en la pregunta
- Opción múltiple, donde el votante podrá escoger tantas opciones como haya disponibles en la pregunta
- Escala de orden de clasificación, donde el votante debe elegir un orden de las opciones que hay disponibles en la pregunta
Para las preguntas por orden de clasificación, el votante deberá tener en cuenta que el tipo de recuento de votos será de tipo Borda
, y de no ser así, no podrá crear la pregunta.
Para el modelo QuestionOption
, indicar que en el controlador admin.py
se incluye un atributo llamado inlines
, necesario para que, en la creación de la pregunta, también aparezca la creación de las opciones para la misma. También en este controlador se añade una propiedad al formulario de votación llamado autocenso
, el cuál es un checkbox que, si está activo, permitirá al usuario autenticado incluirse automáticamente en el censo, para que pueda realizar la votación en la cabina, ya que, si no está en el censo, no tendrá permisos para votar.
Una vez completado el proceso de creación de la votación, la iniciamos y se indicará la fecha exacta en la que se ha iniciado la votación. Con esto, y sabiendo que el usuario que va a votar está incluido en el censo, ya podemos pasar al módulo de Cabina para poder realizar la votación.
Cuando el usuario ha entrado en la cabina para realizar sus votos a las respectivas preguntas incluidas en la votación, estos votos pasan por el módulo Mixnet, y mediante la encriptación de ElGamal, ciframos los votos y lo envíamos al módulo de Store, donde vamos a mostrar la creación del modelo Vote
.
class Vote(models.Model):
voting_id = models.PositiveIntegerField()
voter_id = models.PositiveIntegerField()
question_id = models.PositiveIntegerField()
sex=models.CharField(max_length=200,blank=True)
a = models.TextField()
b = models.TextField()
voted = models.DateTimeField(auto_now=True)
Para el voto necesitamos saber los identificadores del votante, de la votación y de la pregunta, además del sexo del votante, pero lo importante son los atributos a
y b
, que son las opciones de la pregunta que el votante ha escogido en la votación cifradas.
Desde el módulo Store se envía el voto encriptado al subsistema de Votaciones y se procede a parar la votación y comenzar el recuento de votos, con el siguiente método:
def tally_votes(self, token=''):
'''
The tally is a shuffle and then a decrypt
'''
votos = self.get_votes(token)
votes = []
for i in votos:
aa = i['a'].split(',')
bb = i['b'].split(',')
for j in range(len(aa)):
votes.append([int(aa[j]), int(bb[j]), j, i['question_id']])
auth = self.auths.first()
shuffle_url = "/shuffle/{}/".format(self.id)
decrypt_url = "/decrypt/{}/".format(self.id)
auths = [{"name": a.name, "url": a.url} for a in self.auths.all()]
# first, we do the shuffle
data = { "msgs": votes }
response = mods.post('mixnet', entry_point=shuffle_url, baseurl=auth.url, json=data,
response=True)
if response.status_code != 200:
# TODO: manage error
pass
# then, we can decrypt that
data = {"msgs": response.json()}
response = mods.post('mixnet', entry_point=decrypt_url, baseurl=auth.url, json=data,
response=True)
if response.status_code != 200:
# TODO: manage error
pass
self.tally = response.json()
self.save()
tally=self.tally
self.tally_votes_masc(token)
return tally
def tally_votes_masc(self, token=''):
'''
The tally is a shuffle and then a decrypt
'''
votos = self.get_votes_masc(token)
votes = []
for i in votos:
if i['sex'] == 'M':
aa = i['a'].split(',')
bb = i['b'].split(',')
for j in range(len(aa)):
#[[int(i['a']), int(i['b'])] for i in votes if i['sex']=='F']
votes.append([int(aa[j]), int(bb[j]), j, i['question_id']])
auth = self.auths.first()
shuffle_url = "/shuffle/{}/".format(self.id)
decrypt_url = "/decrypt/{}/".format(self.id)
auths = [{"name": a.name, "url": a.url} for a in self.auths.all()]
# first, we do the shuffle
data = { "msgs": votes }
response = mods.post('mixnet', entry_point=shuffle_url, baseurl=auth.url, json=data,
response=True)
if response.status_code != 200:
# TODO: manage error
pass
# then, we can decrypt that
data = {"msgs": response.json()}
response = mods.post('mixnet', entry_point=decrypt_url, baseurl=auth.url, json=data,
response=True)
if response.status_code != 200:
# TODO: manage error
pass
self.tallyM = response.json()
self.save()
tallyM=self.tallyM
self.tally_votes_fem(token)
return tallyM
def tally_votes_fem(self, token=''):
'''
The tally is a shuffle and then a decrypt
'''
votos = self.get_votes_fem(token)
votes = []
for i in votos:
if i['sex'] == 'F':
aa = i['a'].split(',')
bb = i['b'].split(',')
for j in range(len(aa)):
votes.append([int(aa[j]), int(bb[j]), j,i['question_id']])
auth = self.auths.first()
shuffle_url = "/shuffle/{}/".format(self.id)
decrypt_url = "/decrypt/{}/".format(self.id)
auths = [{"name": a.name, "url": a.url} for a in self.auths.all()]
# first, we do the shuffle
data = { "msgs": votes }
response = mods.post('mixnet', entry_point=shuffle_url, baseurl=auth.url, json=data,
response=True)
if response.status_code != 200:
# TODO: manage error
pass
# then, we can decrypt that
data = {"msgs": response.json()}
response = mods.post('mixnet', entry_point=decrypt_url, baseurl=auth.url, json=data,
response=True)
if response.status_code != 200:
# TODO: manage error
pass
self.tallyF = response.json()
self.save()
tallyF=self.tallyF
self.do_postproc()
return tallyF
Hay 3 tipos de recuento, para diferenciar entre recuentos de votantes masculinos, de votantes femeninos, y del total de votantes, pero la única diferencia entre estos métodos es el campo sex
del modelo Vote. El método recibe los votos encriptados del módulo Store y mediante un iterador, necesario para establecer el orden de las opciones votadas de una pregunta, se guarda en una lista de votos para enviar al subsistema de Mixnet y que puedan utilizar los métodos shuffle
y decrypt
para realizar el descifrado. Después de que se haya hecho este proceso, guardamos el JSON y generamos un mensaje de confirmación con el siguiente método:
def votes_info_votos(self,tally):
data=[]
for i, q in enumerate(self.question.all()):
opciones = q.options.all()
opt_count=len(opciones)
opts = []
for opt in opciones:
if q.option_types == 3:
votes = []
for i in range (opt_count):
votes.append(0)
for dicc in tally:
indice = opt.number
pos = dicc.get(str(indice))
if pos!=None and pos[1]==q.id:
votes[pos[0]] = votes[pos[0]] + 1
else:
votes = 0
for dicc in tally:
indice = opt.number
pos = dicc.get(str(indice))
if pos!=None and pos[1]==q.id:
votes = votes + 1
opts.append({
'option': opt.option,
'number': opt.number,
'votes': votes,
'question_id': opt.question_id,
})
data.append({'pregunta': q.desc, 'opciones':opts})
return data
En este método se obtienen todas las opciones de las preguntas de la votación, y se obtiene un mensaje donde se indica la opción escogida, su identificador, el número de votos realizados a esa opción y el identificador de la pregunta que contiene a la opción.
Una vez hecho el recuento, se realiza el último método por parte del subsistema de Votaciones, el do_postproc
:
def do_postproc(self):
tally = self.tally
tallyM = self.tallyM
tallyF = self.tallyF
points = self.points
tallies = ['IDENTITY', 'BORDA', 'HONDT', 'EQUALITY', 'SAINTE_LAGUE', 'DROOP', 'IMPERIALI', 'HARE']
data = []
for i, q in enumerate(self.question.all()):
opciones = q.options.all()
opt_count=len(opciones)
opts = []
for opt in opciones:
if q.option_types == 3:
votes = []
votesM = []
votesF = []
for i in range (opt_count):
votes.append(0)
votesM.append(0)
votesF.append(0)
for dicc in tally:
indice = opt.number
pos = dicc.get(str(indice))
if pos!=None and pos[1]==q.id:
votes[pos[0]] = votes[pos[0]] + 1
for dicc in tallyM:
indice = opt.number
pos = dicc.get(str(indice))
if pos!=None and pos[1]==q.id:
votesM[pos[0]] = votesM[pos[0]] + 1
for dicc in tallyF:
indice = opt.number
pos = dicc.get(str(indice))
if pos!=None and pos[1]==q.id:
votesF[pos[0]] = votesF[pos[0]] + 1
else:
votes = 0
votesM = 0
votesF = 0
for dicc in tally:
indice = opt.number
pos = dicc.get(str(indice))
if pos!=None and pos[1]==q.id:
votes = votes + 1
for dicc in tallyM:
indice = opt.number
pos = dicc.get(str(indice))
if pos!=None and pos[1]==q.id:
votesM = votesM + 1
for dicc in tallyF:
indice = opt.number
pos = dicc.get(str(indice))
if pos!=None and pos[1]==q.id:
votesF = votesF + 1
opts.append({
'question': opt.question.desc,
'question_id':opt.question.id,
'option': opt.option,
'number': opt.number,
'votes': votes,
'votes_masc': votesM,
'votes_fem': votesF,
'points': points
})
data.append( { 'type': tallies[q.type],'options': opts})
postp = mods.post('postproc', json=data)
self.postproc = postp
self.save()
Aquí se prepara el JSON que se envía al subsistema de Postprocesado, donde se recogen los recuentos obtenidos con el método anterior, añadiendo también el tipo de recuento de cada opción votada. Se crea una lista donde se guarda el tipo de recuento y una lista de opciones, que tendrá la pregunta de la votación con su identificador, la opción escogida de la pregunta con su identificador, el número de votos obtenidos en esa opción, el número de votos tanto masculino como femenino y los points para el recuento proporcional de los votos. Con todo esto preparado, se envía el JSON al módulo de Postprocesado y termina el proceso completo del subsistema de Votaciones.
Por último, indicar dos cambios realizados que no han sido necesarios para el proceso de votación, pero necesarios para aportar mejor información a la creación de la votación. En el controlador admin.py
se ha importado una librería llamada range filter
, añadiéndolo al archivo de configuración de Django settings.py
y al requirements.txt
. Esta librería nos permite filtrar las votaciones por fecha de inicio y fecha de finalización, incluyendo este filtro en el método VotingAdmin
. Además, se ha añadido al método start
un atributo que indica el usuario autenticado que ha realizado la votación, añadiendo así al método VotingAdmin
el campo started_by
:
def start(modeladmin, request, queryset):
for v in queryset.all():
v.create_pubkey()
v.start_date = timezone.now()
v.started_by=str(request.user)
v.save()
class VotingAdmin(admin.ModelAdmin):
list_display = ('name', 'start_date', 'end_date', 'started_by')
readonly_fields = ('start_date', 'end_date', 'pub_key',
'tally', 'tallyM', 'tallyF', 'postproc', 'started_by')
list_filter = (StartedFilter, ('start_date', DateRangeFilter), ('end_date', DateRangeFilter),)
search_fields = ('name', )
actions = [ start, stop, tally ]
form = VotingModel
def save_model(self, request, obj, form, change):
super(VotingAdmin, self).save_model(request, obj, form, change)
if form.cleaned_data.get('autocenso'):
user = request.user
Census.objects.get_or_create(voter_id=user.id, voting_id=obj.id)