Пример #1
0
class Ballot(models.Model):
    """For example, a ballot for ASHMC President would have
        candidates (actually PersonCandidates).

        Multiple ballots can appear in a measure; that is,
        you can have a ballot for ASHMC President election and
        one for VP election in the same measure.
    """
    VOTE_TYPES = Utility.enum('POPULARITY',
                              'PREFERENCE',
                              'SELECT_X',
                              'INOROUT',
                              type_name='BallotVoteType')

    TYPES = (
        (VOTE_TYPES.POPULARITY, "Popularity"),
        (VOTE_TYPES.PREFERENCE, 'Preference'),
        (VOTE_TYPES.SELECT_X, 'Select Top X'),
        (VOTE_TYPES.INOROUT, 'Yes or No'),
    )

    vote_type = models.SmallIntegerField(default=0, choices=TYPES)
    number_to_select = models.PositiveIntegerField(blank=True, null=True)

    measure = models.ForeignKey('Measure', null=True)

    display_position = models.IntegerField(default=1)

    title = models.CharField(max_length=50)
    blurb = models.TextField()

    can_write_in = models.BooleanField(default=False)
    can_abstain = models.BooleanField(default=True)
    is_secret = models.BooleanField(default=False)
    is_irv = models.BooleanField(
        default=False,
        help_text=
        'Only applies to Preferential type; changes the way winners are calculated.',
    )

    def get_winners(self):
        """does not break ties."""
        if self.candidate_set.count() == 0:
            return []

        if self.vote_type == self.VOTE_TYPES.POPULARITY or self.vote_type == self.VOTE_TYPES.INOROUT:
            max_choices = max(
                self.candidate_set.annotate(
                    pv_max=models.Count('popularityvote')).values_list(
                        'pv_max', flat=True))
            return self.candidate_set.annotate(
                models.Count('popularityvote')).filter(
                    popularityvote__count=max_choices)
        elif self.vote_type == self.VOTE_TYPES.PREFERENCE:
            if not self.is_irv:
                # The lower the sum of the ranks of a candidate, the better they're doing overall.
                min_choices = min(
                    self.candidate_set.annotate(pf_sum=models.Sum(
                        'preferentialvote__amount')).values_list('pf_sum',
                                                                 flat=True))
                return self.candidate_set.annotate(
                    models.Sum('preferentialvote__amount')).filter(
                        preferentialvote__amount=min_choices)

            # IRV voting is a little more intense.
            # we do rounds - until one candidate has a majority, we eliminate the
            # least popular candidate and apply the voters' other votes to their
            # next favorite choice.
            # http://en.wikipedia.org/wiki/Instant-runoff_voting

            # Since each candidate gets a vote with a different ballot,
            # we normalize the number of votes by the number of voters --
            # each voter should have contributed the same number of votes per
            # ballot (e.g., the number of candidates on the ballot)

            # If no more votes can come in, then don't calculate again.

            # Don't do anything while votes can still come in.
            if not self.measure.voting_closed:
                return Candidate.objects.none()

            # Don't re-calculate if it's been calculated.
            if InstantRerunVotingRound.objects.filter(
                    ballot=self).count() != 0:

                irvr = InstantRerunVotingRound.objects.filter(
                    ballot=self).order_by('-number')[0]
                try:
                    max_votes = max(
                        IRVCandidate.objects.filter(irv_round=irvr, ).annotate(
                            models.Count('votes')).values_list(
                                'votes__count',
                                flat=True,
                            ))
                    return irvr.irvcandidate_set.annotate(
                        models.Count('votes')).filter(votes__count=max_votes)

                except ValueError:
                    return IRVCandidate.objects.none()

            logger.debug("Calcluating IRV for BALLOT %s", self)

            total_votes = self.measure.vote_set.count()
            candidates = list(self.candidate_set.all())
            logger.debug("total votes: %s", total_votes)
            the_round = InstantRerunVotingRound.objects.create(
                number=1,
                ballot=self,
            )
            for candidate in candidates:
                logger.debug("Creating initial IRVCandidate for %s", candidate)
                irvc = IRVCandidate.objects.create(
                    candidate=candidate,
                    irv_round=the_round,
                    # Get the FIRST-CHOICE votes for every candidate.
                )
                irvc.votes.add(*list(
                    self.preferentialvote_set.filter(
                        candidate=candidate,
                        amount=1,
                    )))

            # Do rounds until *someone* has a majority.
            logger.debug("calculating starting max votes")
            try:
                max_votes = max(
                    IRVCandidate.objects.filter(
                        irv_round=the_round, ).annotate(
                            models.Count('votes')).values_list(
                                'votes__count',
                                flat=True,
                            ))
                logger.debug("max_votes: %s", max_votes)
            except ValueError:
                Candidate.objects.none()

            while max_votes <= total_votes / 2:

                logger.debug("%s out of %s", max_votes, total_votes)

                old_candidates = IRVCandidate.objects.filter(
                    irv_round=the_round).annotate(
                        pv_count=models.Count('votes')).order_by('pv_count')
                logger.debug("old candidates: %s", old_candidates)

                the_round = InstantRerunVotingRound.objects.create(
                    ballot=self,
                    number=the_round.number + 1,
                )
                logger.debug("creating new round: %s", the_round.number)

                # TODO: break ties LOLOLOL
                loser = old_candidates[0]
                logger.debug("found loser: %s", loser)

                # Move all the surviving votes over, and delete the
                # loser from the running.
                for irvcandidate in old_candidates[1:]:
                    irvc = IRVCandidate.objects.create(
                        irv_round=the_round,
                        candidate=irvcandidate.candidate,
                    )
                    irvc.votes.add(*list(irvcandidate.votes.all()))

                # Attach losers votes to their next preference
                for pvote in loser.votes.all():
                    logger.debug("attaching loser's votes to other candidates")
                    v = pvote.vote
                    try:
                        next_pvote = v.preferentialvote_set.get(
                            amount=pvote.amount + 1, )
                    except PreferentialVote.ObjectDoesNotExist:
                        logger.info(
                            "losing a preference due to lack of ranking")
                        # If there are no more preferences, that's fine.
                        # their vote no longer contributes to the total,
                        # and they don't get a candidate assigned to them.
                        total_votes -= 1
                        continue

                    next_irc = IRVCandidate.objects.get(
                        irv_round=the_round,
                        candidate=next_pvote.candidate,
                    )
                    next_irc.votes.add(next_pvote)

                max_votes = max(
                    IRVCandidate.objects.filter(
                        irv_round=the_round, ).annotate(
                            models.Count('votes')).values_list('votes__count',
                                                               flat=True))
            # The rounds are over! Time to find the max votes and return the list
            # of winners.
            return IRVCandidate.objects.annotate(models.Count('votes')).filter(
                votes__count=max_votes, irv_round=the_round)

        elif self.vote_type == self.VOTE_TYPES.SELECT_X:
            max_choices = self.candidate_set.annotate(pv_max=models.Count(
                'popularityvote')).order_by('-pv_max').values_list(
                    'pv_max', flat=True)[0]
            return self.candidate_set.annotate(
                models.Count('popularityvote')).filter(
                    popularityvote__count=max_choices)

    def ordered_candidates(self):
        if self.vote_type == self.VOTE_TYPES.POPULARITY or self.vote_type == self.VOTE_TYPES.INOROUT:
            return self.candidate_set.annotate(
                votes=models.Count('popularityvote')).order_by('-votes')
        elif self.vote_type == self.VOTE_TYPES.PREFERENCE:
            return self.candidate_set.annotate(
                votes=models.Sum('preferentialvote__amount')).order_by('votes')
        elif self.vote_type == self.VOTE_TYPES.SELECT_X:
            return self.candidate_set.annotate(
                votes=models.Count('popularityvote')).order_by('-votes')

    def __unicode__(self):
        return u"Ballot #{}: {}".format(self.id, self.title)

    class Meta:
        unique_together = (('measure', 'title'), )

    def save(self, *args, **kwargs):
        if self.vote_type == self.VOTE_TYPES.SELECT_X and self.number_to_select is None:
            raise IntegrityError(
                "Can't have a SELECT_X vote type and no number_to_select.")

        super(Ballot, self).save(*args, **kwargs)
        if self.vote_type == self.VOTE_TYPES.INOROUT:
            # create the two candidates now.
            yes, _ = Candidate.objects.get_or_create(
                title="Yes",
                ballot=self,
            )

            no, _ = Candidate.objects.get_or_create(
                title="No",
                ballot=self,
            )
Пример #2
0
class Ballot(models.Model):
    """For example, a ballot for ASHMC President would have
        candidates (actually PersonCandidates).

        Multiple ballots can appear in a measure; that is,
        you can have a ballot for ASHMC President election and
        one for VP election in the same measure.
    """
    VOTE_TYPES = Utility.enum('POPULARITY',
                              'PREFERENCE',
                              'SELECT_X',
                              'INOROUT',
                              type_name='BallotVoteType')

    TYPES = (
        (VOTE_TYPES.POPULARITY, "Popularity"),
        (VOTE_TYPES.PREFERENCE, 'Preference'),
        (VOTE_TYPES.SELECT_X, 'Select Top X'),
        (VOTE_TYPES.INOROUT, 'Yes or No'),
    )

    vote_type = models.SmallIntegerField(default=0, choices=TYPES)
    number_to_select = models.PositiveIntegerField(blank=True, null=True)

    measure = models.ForeignKey('Measure', null=True)

    display_position = models.IntegerField(default=1)

    title = models.CharField(max_length=50)
    blurb = models.TextField()

    can_write_in = models.BooleanField(default=False)
    can_abstain = models.BooleanField(default=True)
    is_secret = models.BooleanField(default=False)

    def get_winners(self):
        """does not break ties."""
        if self.vote_type == self.VOTE_TYPES.POPULARITY or self.vote_type == self.VOTE_TYPES.INOROUT:
            max_choices = max(
                self.candidate_set.annotate(
                    pv_max=models.Count('popularityvote')).values_list(
                        'pv_max', flat=True))
            return self.candidate_set.annotate(
                models.Count('popularityvote')).filter(
                    popularityvote__count=max_choices)
        elif self.vote_type == self.VOTE_TYPES.PREFERENCE:
            # The lower the sum of the ranks of a candidate, the better they're doing overall.
            min_choices = min(
                self.candidate_set.annotate(
                    pf_sum=models.Sum('preferentialvote__amount')).values_list(
                        'pf_sum', flat=True))
            return self.candidate_set.annotate(
                models.Sum('preferentialvote__amount')).filter(
                    preferentialvote__amount=min_choices)
        elif self.vote_type == self.VOTE_TYPES.SELECT_X:
            max_choices = self.candidate_set.annotate(pv_max=models.Count(
                'popularityvote')).order_by('-pv_max').values_list(
                    'pv_max', flat=True)[0]
            return self.candidate_set.annotate(
                models.Count('popularityvote')).filter(
                    popularityvote__count=max_choices)

    def ordered_candidates(self):
        if self.vote_type == self.VOTE_TYPES.POPULARITY or self.vote_type == self.VOTE_TYPES.INOROUT:
            return self.candidate_set.annotate(
                votes=models.Count('popularityvote')).order_by('-votes')
        elif self.vote_type == self.VOTE_TYPES.PREFERENCE:
            return self.candidate_set.annotate(
                votes=models.Sum('preferentialvote__amount')).order_by('votes')
        elif self.vote_type == self.VOTE_TYPES.SELECT_X:
            return self.candidate_set.annotate(
                votes=models.Count('popularityvote')).order_by('-votes')

    def __unicode__(self):
        return u"Ballot #{}: {}".format(self.id, self.title)

    class Meta:
        unique_together = (('measure', 'title'), )

    def save(self, *args, **kwargs):
        if self.vote_type == self.VOTE_TYPES.SELECT_X and self.number_to_select is None:
            raise IntegrityError(
                "Can't have a SELECT_X vote type and no number_to_select.")

        super(Ballot, self).save(*args, **kwargs)
        if self.vote_type == self.VOTE_TYPES.INOROUT:
            # create the two candidates now.
            yes, _ = Candidate.objects.get_or_create(
                title="Yes",
                ballot=self,
            )

            no, _ = Candidate.objects.get_or_create(
                title="No",
                ballot=self,
            )