okfn-brasil/perfil-politico

View on GitHub
perfil/core/models.py

Summary

Maintainability
A
0 mins
Test Coverage
from datetime import date
from decimal import Decimal

from django.db import models
from django.contrib.postgres.fields import ArrayField, JSONField

from perfil.core.managers import CampaignManager


STATES = (
    ("AC", "Acre"),
    ("AL", "Alagoas"),
    ("AP", "Amapá"),
    ("AM", "Amazonas"),
    ("BA", "Bahia"),
    ("CE", "Ceará"),
    ("DF", "Distrito Federal"),
    ("ES", "Espírito Santo"),
    ("GO", "Goiás"),
    ("MA", "Maranhão"),
    ("MT", "Mato Grosso"),
    ("MS", "Mato Grosso do Sul"),
    ("MG", "Minas Gerais"),
    ("PA", "Pará"),
    ("PB", "Paraíba"),
    ("PR", "Paraná"),
    ("PE", "Pernambuco"),
    ("PI", "Piauí"),
    ("RJ", "Rio de Janeiro"),
    ("RN", "Rio Grande do Norte"),
    ("RS", "Rio Grande do Sul"),
    ("RO", "Rondônia"),
    ("RR", "Roraima"),
    ("SC", "Santa Catarina"),
    ("SP", "São Paulo"),
    ("SE", "Sergipe"),
    ("TO", "Tocantins"),
)


def age(date_of_birth, election_year):
    """Calculates the age of the politician when they started in office"""
    reference = date(election_year + 1, 1, 1)
    correction_reference = correction = (reference.month, reference.day)
    correction_date_of_birth = (date_of_birth.month, date_of_birth.day)
    correction = correction_reference < correction_date_of_birth
    return reference.year - date_of_birth.year - correction


class City(models.Model):
    code = models.IntegerField()
    name = models.CharField(max_length=63, default="", blank=True)
    state = models.CharField(max_length=2, choices=STATES)

    def __repr__(self):
        return f"{self.name} - {self.state}"

    class Meta:
        verbose_name = "city"
        verbose_name_plural = "cities"
        ordering = ("name", "state")
        indexes = (models.Index(fields=("name", "state")),)


class Party(models.Model):
    name = models.CharField(max_length=63, default="", blank=True)
    abbreviation = models.CharField(max_length=15, default="", blank=True)

    class Meta:
        verbose_name = "party"
        verbose_name_plural = "parties"
        ordering = ("name",)
        indexes = (models.Index(fields=("abbreviation",)),)

    def __repr__(self):
        return self.abbreviation


class Affiliation(models.Model):
    """Store raw data about political party affiliations"""

    REGULAR = "R"
    CANCELED = "C"
    EXCLUDED = "D"
    SUB_JUDICE = "S"
    STATUSES = (
        (REGULAR, "Regular"),
        (CANCELED, "Cancelado"),
        (EXCLUDED, "Desfiliado"),
        (SUB_JUDICE, "Sub judice"),
    )

    name = models.CharField(max_length=127, default="", blank=True)
    voter_id = models.CharField(max_length=12, default="", blank=True)
    started_in = models.DateField()
    electoral_section = models.IntegerField()
    electoral_zone = models.IntegerField()
    party = models.ForeignKey(
        Party, on_delete=models.CASCADE, related_name="affiliated"
    )
    city = models.ForeignKey(City, on_delete=models.CASCADE, related_name="affiliated")
    status = models.CharField(max_length=1, choices=STATUSES)
    ended_in = models.DateField(null=True)
    canceled_in = models.DateField(null=True)
    regularized_in = models.DateField(null=True)
    processed_in = models.DateField(null=True)
    extracted_in = models.DateTimeField(null=True)
    cancel_reason = models.CharField(max_length=31, default="", blank=True)

    def __repr__(self):
        return f"{self.name} ({self.party.abbreviation}/{self.city.state})"

    class Meta:
        verbose_name = "historical political affiliation"
        verbose_name_plural = "historical political affiliations"
        indexes = (
            models.Index(fields=("party",)),
            models.Index(fields=("name",)),
            models.Index(fields=("voter_id",)),
            models.Index(fields=("status",)),
            models.Index(fields=("started_in",)),
        )


class Politician(models.Model):
    """Subset of Affiliation keeping only the most recent and regular political
    party affiliation"""

    current_affiliation = models.OneToOneField(
        Affiliation, on_delete=models.CASCADE, related_name="politician"
    )
    affiliation_history = JSONField(default=list)
    asset_history = JSONField(default=list)
    election_history = JSONField(default=list)
    bill_keywords = JSONField(default=list)
    rosies_suspicions = JSONField(default=list)

    def __repr__(self):
        return (
            f"{self.current_affiliation.name} "
            f"({self.current_affiliation.party.abbreviation}"
            f"/{self.current_affiliation.city.state})"
        )

    class Meta:
        verbose_name = "politician"
        verbose_name_plural = "politicians"
        ordering = ("current_affiliation__name",)
        indexes = (models.Index(fields=("current_affiliation",)),)


class Candidate(models.Model):
    politician = models.ForeignKey(Politician, null=True, on_delete=models.CASCADE)
    voter_id = models.CharField(max_length=12, default="", blank=True)
    taxpayer_id = models.CharField(max_length=11, blank=True, default="")
    date_of_birth = models.DateField(null=True)
    place_of_birth = models.ForeignKey(City, null=True, on_delete=models.CASCADE)
    gender = models.CharField(max_length=16, blank=True, default="")
    email = models.CharField(max_length=128, blank=True, default="")
    age = models.CharField(max_length=16, blank=True, default="")

    ethnicity = models.CharField(max_length=16, blank=True, default="")
    ethnicity_code = models.CharField(max_length=2, blank=True, default="")
    marital_status = models.CharField(max_length=32, blank=True, default="")
    marital_status_code = models.CharField(max_length=32, blank=True, default="")
    education = models.CharField(max_length=32, blank=True, default="")
    education_code = models.CharField(max_length=16, blank=True, default="")
    nationality = models.CharField(max_length=64, blank=True, default="")
    nationality_code = models.CharField(max_length=32, blank=True, default="")
    occupation = models.CharField(max_length=128, blank=True, default="")
    occupation_code = models.CharField(max_length=64, blank=True, default="")

    election = models.CharField(max_length=64, blank=True, default="")
    year = models.IntegerField()
    state = models.CharField(max_length=2, choices=STATES)
    round = models.IntegerField()
    post = models.CharField(max_length=128, blank=True, default="")
    post_code = models.IntegerField()
    status = models.CharField(max_length=64, blank=True, default="")

    party = models.ForeignKey(Party, on_delete=models.CASCADE)
    name = models.CharField(max_length=128, blank=True, default="")
    ballot_name = models.CharField(max_length=32, blank=True, default="")
    number = models.IntegerField(null=True)
    sequential = models.CharField(max_length=16, blank=True, default="")
    coalition_name = models.CharField(max_length=128, blank=True, default="")
    coalition_description = models.CharField(max_length=256, blank=True, default="")
    coalition_short_name = models.CharField(max_length=16, blank=True, default="")
    max_budget = models.CharField(max_length=16, blank=True, default="")

    round_result = models.CharField(max_length=64, blank=True, default="")
    round_result_code = models.IntegerField(null=True)

    objects = CampaignManager()

    def _history(self, prefix, sort_by="year"):
        if not self.politician:
            return []

        data = getattr(self.politician, f"{prefix}_history", [])
        return sorted(data, key=lambda obj: obj[sort_by])

    def affiliation_history(self):
        return self._history("affiliation", "started_in")

    def asset_history(self):
        return self._history("asset")

    def election_history(self):
        return self._history("election")

    def elections(self):
        return len(self.election_history())

    def elections_won(self):
        return sum(1 for election in self.election_history() if election["elected"])

    def bills(self):
        if not self.politician:
            return []

        return self.politician.bills.all()

    def bill_keywords(self):
        if not self.politician:
            return []

        data = self.politician.bill_keywords
        return sorted(data, key=lambda obj: obj["total"], reverse=True)

    def rosies_suspicions(self):
        return self.politician.rosies_suspicions if self.politician else []

    def image(self):
        if self.year != 2018:
            return None

        # TODO bucket configuration as a setting
        bucket = "https://serenata-de-amor-data.nyc3.digitaloceanspaces.com/"
        return f"{bucket}perfil-politico/{self.state}/{self.sequential}.jpg"

    def get_age(self):
        """The age column is blank too many times, so let's calculate it"""
        if self.age:
            return self.age

        if not self.date_of_birth:
            return None

        return age(self.date_of_birth, self.year)

    def __repr__(self):
        return f"{self.ballot_name} ({self.party.abbreviation}/{self.state})"

    class Meta:
        verbose_name = "candidate"
        verbose_name_plural = "candidates"
        ordering = ("-year", "ballot_name")
        indexes = (
            models.Index(fields=("politician",)),
            models.Index(fields=("voter_id",)),
            models.Index(fields=("year",)),
            models.Index(fields=("year", "state", "post", "ballot_name")),
            models.Index(fields=("round_result",)),
            models.Index(fields=("sequential",)),
        )


class Asset(models.Model):
    candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE)
    value = models.DecimalField(max_digits=16, decimal_places=2, null=True)
    category = models.CharField(max_length=128, blank=True, default="")
    category_code = models.IntegerField(null=True)
    detail = models.CharField(max_length=255, blank=True, default="")
    order = models.IntegerField(null=True)
    last_update = models.DateTimeField(null=True)

    def __repr__(self):
        return f"{self.category} (R$ {Decimal(self.value):,})"

    class Meta:
        verbose_name = "asset"
        verbose_name_plural = "assets"
        ordering = ("candidate__ballot_name", "-value")
        indexes = (models.Index(fields=("candidate",)), models.Index(fields=("value",)))


class Bill(models.Model):
    authors = models.ManyToManyField(Politician, related_name="bills")
    summary = models.TextField(blank=True, default="")
    name = models.CharField(max_length=16, blank=True, default="")
    keywords = ArrayField(models.CharField(max_length=255))
    source_id = models.IntegerField()
    url = models.URLField(unique=True)

    def __repr__(self):
        return self.name

    class Meta:
        verbose_name = "bill"
        verbose_name_plural = "bills"
        ordering = ("name",)
        indexes = (models.Index(fields=("keywords",)), models.Index(fields=("url",)))