As for the problem and the suggestion presented in the my previous gist Voting System Schema Model Analysis, this is the first version for the new Voting System.
This is a self contained Django application with all the logic and behavior encapsulated in the models layer.
For simple election based on the rules
- Voter type (staff or expert)
- Difference of votes for a regula user. +2 or -2 to approve or reject.
This is the main polymorphic class to hold any kind of election.
This model provides the pick_vote method, to register annotator's votes.
import uuid
from django.db import models
from django.db.models import Q
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from model_utils.models import TimeStampedModel
from simple_history.models import HistoricalRecords
from images.models import BoundingBox, Category, Species, Activity, Annotator
class VoteResults(TimeStampedModel):
CLOSE_CHOICES= (('SE', 'Staff Expert Rule'), ('DV', 'Difference of Votes'))
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
content_object = GenericForeignKey()
vote_state = models.BooleanField(null=True)
close_rule = models.CharField(_("Close Rule"), max_length=2, choices=CLOSE_CHOICES, null=True)
history = HistoricalRecords()
def get_vote_state(self, new_vote):
votes = self.get_votes()
total_votes = 0
if votes:
total_votes = votes.filter(vote=1).count() - votes.filter(vote=0).count()
total_votes += 1 if new_vote else -1
if total_votes >= 2:
self.vote_state = True
elif total_votes <= -2:
self.vote_state = False
else:
return None # keep voting open
return True # close voting
def has_staff_expert_vote(self):
votes = self.get_votes()
if votes.filter(annotator__human__is_staff=True).exists() or \
votes.filter(annotator__human__is_expert=True).exists():
return True
return False
def is_open(self):
if self.vote_state is not None or self.close_rule is not None:
raise Exception("Election is closed, no more votes are accepted:\n{}".format(self))
return True
def get_votes(self):
return self.registervote_set.all()
def __str__(self):
return "{} id: {} \nVote State: {}\nClose Rule: {}".format(self.content_type.model.upper(),
self.object_id,
self.vote_state,
self.close_rule
)
class Meta:
constraints = [
models.CheckConstraint(
check=Q(vote_state__isnull=True) | Q(close_rule__isnull=False),
name='close_rule_null_when_vote_state_null'
)
]
class RegisterVote(TimeStampedModel):
vote = models.BooleanField()
annotator = models.ForeignKey(Annotator, on_delete=models.PROTECT, blank=True, null=True)
comment = models.TextField()
vote_results = models.ForeignKey(VoteResults, on_delete=models.CASCADE)
def pick_vote(self, annotator, diputed_object, vote, comment=''):
try:
self._get_object_class(diputed_object)
except:
raise
self.vote = vote
self.annotator = annotator
self.comment = comment
self.vote_results, created = VoteResults.objects.get_or_create(content_type=self.content_type, object_id=self.object_id)
if self._is_staff_expert(annotator):
if not self.vote_results.is_open():
raise
self.vote_results.vote_state = True if self.vote else False
self.vote_results.close_rule = 'SE'
elif not created:
if not self.vote_results.is_open():
raise
elif self.vote_results.has_staff_expert_vote():
raise Exception("This election should be closed, since a staff or expert vote exists: {}".format(diputed_object))
if self.vote_results.get_vote_state(self.vote):
self.vote_results.close_rule = 'DV'
self.vote_results.save()
self.save()
def _is_staff_expert(self, annotator):
return annotator.human.is_staff or annotator.human.is_expert
def _get_object_class(self, diputed_object):
classes = [BoundingBox, Category, Species, Activity]
for klass in classes:
if isinstance(diputed_object, klass):
try:
self.content_type = content_type = ContentType.objects.get_for_model(klass)
self.object_id = diputed_object.id
except klass.DoesNotExist:
raise Exception("Disputed Object not found")
def __str__(self):
return "Annotator: {}\nVote: {}\nDate: {}".format(self.annotator, self.vote, self.created)
class Meta:
constraints = [
models.UniqueConstraint(fields=['annotator', 'vote_results'], name='unique_annotator_vote_results_constraint')
]
The following code simulates a voting to test rules and the results.
Cleanup your tables when needed:
- vote_results_historicalvoteresults
- vote_results_registervote
- vote_results_voteresults
Run simulation: ./manage.py test vote_results --settings=<your settings configuration>
import random
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from images.models import BoundingBox, Category, Species, Activity, Annotator
from .models import VoteResults, RegisterVote
# Get random 10 objects for each model
categories = Category.objects.all().order_by('?')[:10]
species = Species.objects.all().order_by('?')[:10]
bouding_boxes = BoundingBox.objects.all().order_by('?')[:10]
activities = Activity.objects.all().order_by('?')[:10]
objects = [categories, species, bouding_boxes, activities]
annotators = \<LIST OF IDs to TEST\>
def get_random_element(lst):
return random.sample(lst, 1)
def check_vote_result(content_type, obj, before=False):
try:
vr = VoteResults.objects.get(content_type=content_type, object_id=obj.id)
if before: print('Election exists:')
print(vr)
return vr
except VoteResults.DoesNotExist:
print('Election does not exist. It will be created for:')
print('{}: {}'.format(content_type.model.upper(), obj.id))
def print_votes(votes):
for vote in votes:
print(vote)
def vote():
# Vote setup
human_id = get_random_element(annotators)[0]
annotator = Annotator.objects.get(human_id=human_id)
obj = get_random_element(objects)[0][0]
vote = get_random_element([True, False])[0]
content_type = ContentType.objects.get_for_model(type(obj))
print('\n\n\n\n==================================================')
print('ELECTION STATUS BEFORE NEW VOTE')
print('-------------------------------')
vr = check_vote_result(content_type, obj, True)
try:
rv = RegisterVote()
rv.pick_vote(annotator=annotator, diputed_object=obj, vote=vote, comment='')
print('\n\nNEW VOTE')
print('--------')
print(rv)
except Exception as e:
print('\n\nEXCEPTION ON VOTE')
print('-----------------')
print(e)
print('\nVOTE(S)')
print('-------')
votes = vr.get_votes()
print_votes(votes)
print('==================================================')
return
# Print Vote Result
print('\n\nELECTION STATUS AFTER NEW VOTE')
print('------------------------------')
vr = check_vote_result(content_type, obj)
votes = vr.get_votes()
print('\nVOTE(S)')
print('-------')
print_votes(votes)
print
print('==================================================')
idx = 0
while idx < 100:
vote()
idx += 1
People name were hided.
==================================================
ELECTION STATUS BEFORE NEW VOTE
-------------------------------
Election exists:
ACTIVITY id: bce2e7bb-5426-43f6-8a83-ba70ed1521ef
Vote State: False
Close Rule: SE
EXCEPTION ON VOTE
-----------------
Election is closed, no more votes are accepted:
ACTIVITY id: bce2e7bb-5426-43f6-8a83-ba70ed1521ef
Vote State: False
Close Rule: SE
VOTE(S)
-------
Annotator: -----------------
Vote: False
Date: 2023-06-13 20:37:25.806976+00:00
==================================================
==================================================
ELECTION STATUS BEFORE NEW VOTE
-------------------------------
Election does not exist. It will be created for:
SPECIES: d2af6d9d-eda0-4355-9073-0ebba506dc8c
NEW VOTE
--------
Annotator: -----------------
Vote: True
Date: 2023-06-13 22:09:40.289427+00:00
ELECTION STATUS AFTER NEW VOTE
------------------------------
SPECIES id: d2af6d9d-eda0-4355-9073-0ebba506dc8c
Vote State: True
Close Rule: SE
VOTE(S)
-------
Annotator: -----------------
Vote: True
Date: 2023-06-13 22:09:40.289427+00:00
==================================================
==================================================
ELECTION STATUS BEFORE NEW VOTE
-------------------------------
Election exists:
ACTIVITY id: cd1e25b4-81b6-4aee-b7bc-2eb9954c0d76
Vote State: None
Close Rule: None
NEW VOTE
--------
Annotator: -----------------
Vote: True
Date: 2023-06-13 22:09:40.308486+00:00
ELECTION STATUS AFTER NEW VOTE
------------------------------
ACTIVITY id: cd1e25b4-81b6-4aee-b7bc-2eb9954c0d76
Vote State: None
Close Rule: None
VOTE(S)
-------
Annotator: -----------------
Vote: True
Date: 2023-06-13 22:03:37.499496+00:00
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:30.912736+00:00
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:40.237888+00:00
Annotator: -----------------
Vote: True
Date: 2023-06-13 22:09:40.308486+00:00
==================================================
==================================================
ELECTION STATUS BEFORE NEW VOTE
-------------------------------
Election does not exist. It will be created for:
CATEGORY: 6cfc887d-8240-49f5-828a-51969733d115
NEW VOTE
--------
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:40.742575+00:00
ELECTION STATUS AFTER NEW VOTE
------------------------------
CATEGORY id: 6cfc887d-8240-49f5-828a-51969733d115
Vote State: None
Close Rule: None
VOTE(S)
-------
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:40.742575+00:00
==================================================
==================================================
ELECTION STATUS BEFORE NEW VOTE
-------------------------------
Election does not exist. It will be created for:
SPECIES: c6049fae-f84d-44c1-98f7-b14308716f94
NEW VOTE
--------
Annotator: -----------------
Vote: True
Date: 2023-06-13 22:09:40.766005+00:00
ELECTION STATUS AFTER NEW VOTE
------------------------------
SPECIES id: c6049fae-f84d-44c1-98f7-b14308716f94
Vote State: None
Close Rule: None
VOTE(S)
-------
Annotator: -----------------
Vote: True
Date: 2023-06-13 22:09:40.766005+00:00
==================================================
==================================================
ELECTION STATUS BEFORE NEW VOTE
-------------------------------
Election does not exist. It will be created for:
BOUNDINGBOX: f29b2106-2066-427a-9578-7f8f6874be11
NEW VOTE
--------
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:41.167891+00:00
ELECTION STATUS AFTER NEW VOTE
------------------------------
BOUNDINGBOX id: f29b2106-2066-427a-9578-7f8f6874be11
Vote State: None
Close Rule: None
VOTE(S)
-------
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:41.167891+00:00
==================================================
==================================================
ELECTION STATUS BEFORE NEW VOTE
-------------------------------
Election does not exist. It will be created for:
SPECIES: 955a6085-0aab-49fb-8b28-a5a809f96c74
NEW VOTE
--------
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:41.187829+00:00
ELECTION STATUS AFTER NEW VOTE
------------------------------
SPECIES id: 955a6085-0aab-49fb-8b28-a5a809f96c74
Vote State: None
Close Rule: None
VOTE(S)
-------
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:41.187829+00:00
==================================================
==================================================
ELECTION STATUS BEFORE NEW VOTE
-------------------------------
Election does not exist. It will be created for:
SPECIES: 878aabbe-4cc3-4712-b8e1-b8afc0e2f70d
NEW VOTE
--------
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:41.212384+00:00
ELECTION STATUS AFTER NEW VOTE
------------------------------
SPECIES id: 878aabbe-4cc3-4712-b8e1-b8afc0e2f70d
Vote State: None
Close Rule: None
VOTE(S)
-------
Annotator: -----------------
Vote: False
Date: 2023-06-13 22:09:41.212384+00:00
==================================================