Skip to content

Instantly share code, notes, and snippets.

@robson-koji
Last active June 14, 2023 12:57
Show Gist options
  • Save robson-koji/0ab263685411ff8a24b9d5139e20eeee to your computer and use it in GitHub Desktop.
Save robson-koji/0ab263685411ff8a24b9d5139e20eeee to your computer and use it in GitHub Desktop.
Modeling, development, test and simulation

A Brand New Voting System

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.

Modeling

This is a self contained Django application with all the logic and behavior encapsulated in the models layer.

VoteResults

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.

RegisterVote

This model provides the pick_vote method, to register annotator's votes.

votingsystemnew

Development

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')
        ]                    

Simulation

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    

Sample Output

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
==================================================
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment