RadarParlamentar-MES2017-1/radar

View on GitHub
radar_parlamentar/modelagem/models.py

Summary

Maintainability
D
1 day
Test Coverage

# coding=utf8

# Copyright (C) 2012, Leonardo Leite, Eduardo Hideo, Saulo Trento,
#                     Diego Rabatone
#
# This file is part of Radar Parlamentar.
#
# Radar Parlamentar is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Radar Parlamentar is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radar Parlamentar.  If not, see <http://www.gnu.org/licenses/>.


from django.db import models
from django.utils.dateparse import parse_datetime
import re
import logging
import os

logger = logging.getLogger("radar")
MODULE_DIR = os.path.abspath(os.path.dirname(__file__))

SIM = 'SIM'
NAO = 'NAO'
ABSTENCAO = 'ABSTENCAO'
OBSTRUCAO = 'OBSTRUCAO'
AUSENTE = 'AUSENTE'

OPCOES = (
    (SIM, "Sim"),
    (NAO, "Não"),
    (ABSTENCAO, "Abstenção"),
    (OBSTRUCAO, "Obstrução"),
    (AUSENTE, "Ausente"),
)

M = "M"
F = "F"

GENEROS = (
    (M, "Masculino"),
    (F, "Feminino"),
)

MUNICIPAL = 'MUNICIPAL'
ESTADUAL = 'ESTADUAL'
FEDERAL = 'FEDERAL'

ESFERAS = (
    (MUNICIPAL, 'Municipal'),
    (ESTADUAL, 'Estadual'),
    (FEDERAL, 'Federal'),
)

QUADRIENIO = "QUADRIENIO"
BIENIO = 'BIENIO'
ANO = 'ANO'
SEMESTRE = 'SEMESTRE'
MES = 'MES'

PERIODOS = (
    (QUADRIENIO, 'QUADRIENIO'),
    (BIENIO, 'BIENIO'),
    (ANO, 'ano'),
    (SEMESTRE, 'semestre'),
    (MES, 'mes')
)

SEM_PARTIDO = 'Sem partido'
COR_PRETA = '#000000'


class Indexadores(models.Model):
    """Termos utilizados na indexação de proposições

    Atributos:
        termo -- string; ex: "mulher" ou "partido político"+
        principal -- bool; identifica se o termo é o principal
                    de uma linha de sinônimos, o termo a ser usado.
    """
    termo = models.CharField(max_length=120)
    principal = models.BooleanField()

    def __unicode__(self):
        return '%s-%s-%s' % (self.nome, self.numero, self.cor)


class Partido(models.Model):
    """Partido político.

    Atributos:
        nome -- string; ex: 'PT'
        numero -- int; ex: '13'
        cor -- string; ex: #FFFFFF

    Métodos da classe:
        from_nome(nome): retorna objeto do tipo Partido
        from_numero(numero): retorna objeto do tipo Partido
        get_sem_partido(): retorna um partido chamado 'SEM PARTIDO'
    """

    LISTA_PARTIDOS = os.path.join(MODULE_DIR, 'recursos/partidos.txt')

    nome = models.CharField(max_length=12)
    numero = models.IntegerField()
    cor = models.CharField(max_length=7)

    @classmethod
    def from_nome(cls, nome):
        """Recebe um nome e retornar um objeto do tipo Partido,
            ou None se nome for inválido
        """
        if nome is None:
            return None

        # procura primeiro no banco de dados
        nome = cls._normaliza_nome_partido(nome)
        p = Partido.objects.filter(nome__iexact=nome)
        if p:
            return p[0]
        else:
            # se não estiver no BD, procura hardcoded
            return cls._from_regex(1, nome.strip())

    @classmethod
    def _normaliza_nome_partido(cls, nome_partido):
        trocar = {'DEMOCRATAS': 'DEM', 'SOLIDARIED': 'SD', 'SDD': 'SD'}
        nome_partido = nome_partido.upper().replace(' ', '')
        nome_partido = trocar.get(nome_partido, nome_partido)
        return nome_partido

    @classmethod
    def from_numero(cls, numero):
        """Recebe um número (int) e retornar um objeto do tipo Partido,
            ou None se nome for inválido
        """

        if numero is None:
            return None

        # procura primeiro no banco de dados
        p = Partido.objects.filter(numero=numero)
        if p:
            return p[0]
        else:
            # se não estiver no BD, procura no arquivo hardcoded
            return cls._from_regex(2, str(numero))

    @classmethod
    def get_sem_partido(cls):
        """Retorna um partido chamado 'SEM PARTIDO'"""
        lista = Partido.objects.filter(nome=SEM_PARTIDO)
        if not lista:
            partido = Partido()
            partido.nome = SEM_PARTIDO
            partido.numero = 0
            partido.cor = COR_PRETA
            partido.save()
        else:
            partido = lista[0]
        return partido

    @classmethod
    def _from_regex(cls, idx, key):
        PARTIDO_REGEX = '([a-zA-Z]*); *([0-9]{2}); *(#+[0-f]{6})'
        f = open(cls.LISTA_PARTIDOS)
        for line in f:
            res = re.search(PARTIDO_REGEX, line)
            if res and res.group(idx).upper() == key:
                partido = Partido()
                partido.nome = res.group(1)
                partido.numero = int(res.group(2))
                partido.cor = res.group(3)
                partido.save()
                return partido
        return None

    def __unicode__(self):
        return '%s-%s' % (self.nome, self.numero)


class CasaLegislativa(models.Model):
    """Instituição tipo Senado, Câmara etc

    Atributos:
        nome -- string; ex 'Câmara Municipal de São Paulo'
        nome_curto -- string; será usado pra gerar links.
                        ex 'cmsp' para 'Câmara Municipal de São Paulo'
        esfera -- string (municipal, estadual, federal)
        local -- string; ex 'São Paulo' para a CMSP
    """

    nome = models.CharField(max_length=100)
    nome_curto = models.CharField(max_length=50, unique=True)
    esfera = models.CharField(max_length=10, choices=ESFERAS)
    local = models.CharField(max_length=100)

    def __unicode__(self):
        return self.nome

    def partidos(self):
        """Retorna os partidos existentes nesta casa legislativa"""
        return Partido.objects.filter(
            parlamentar__casa_legislativa=self).distinct()

    def parlamentares(self):
        """Retorna os parlamentares existentes nesta casa legislativa"""
        return Parlamentar.objects.filter(casa_legislativa=self).distinct()

    def num_votacao(self, data_inicial=None, data_final=None):
        """retorna a quantidade de votacao numa casa legislativa"""
        return Votacao.por_casa_legislativa(
            self, data_inicial, data_final).count()

    def num_votos(self, data_inicio=None, data_fim=None):
        """retorna a quantidade de votos numa casa legislativa"""
        votacoes = Votacao.por_casa_legislativa(self, data_inicio, data_fim)
        votos = []
        for votacao in votacoes:
            votos += votacao.votos()
        return len(votos)

    @staticmethod
    def deleta_casa(nome_casa_curto):
        """Método que deleta determinado registro de casa legislativa
            em cascata
            Argumentos:
                nome_casa - Nome da casa a ser deletada"""
        try:
            try:
                CasaLegislativa.objects.get(
                    nome_curto=nome_casa_curto).delete()

            except CasaLegislativa.DoesNotExist:
                logger.info(
                    'Casa legislativa ' + nome_casa_curto + ' não existe')
        except:
            logger.info('Possivelmente a operacao extrapolou o limite de '
                        'operacoes do SQLite, tente utilizar o MySQL')


class ChefeExecutivo(models.Model):
    """Atributos:
        nome -- string; ex Lula
        genero -- string: ex Masculino
        partido -- tipo Partido
        mandato_ano_inicio -- objetos datetime
        mandato_ano_fim -- objetos datetime
    """

    nome = models.CharField(max_length=100)
    genero = models.CharField(max_length=10, choices=GENEROS, blank=True)
    partido = models.ForeignKey(Partido)
    mandato_ano_inicio = models.IntegerField()
    mandato_ano_fim = models.IntegerField()
    casas_legislativas = models.ManyToManyField(CasaLegislativa)
    titulo = None

    def __unicode__(self):
        self.titulo = self.get_titulo_chefe()
        return self.titulo + ": " + self.nome + " - " + self.partido.nome

    @staticmethod
    def por_casa_legislativa_e_periodo(
            casa_legislativa, data_inicial=None, data_final=None):

        chefes_executivo = ChefeExecutivo.objects.filter(
            casas_legislativas__nome_curto=casa_legislativa.nome_curto)

        chefes = []
        if data_inicial is not None and data_final is not None:
            ano_inicio = int(data_inicial.year)
            ano_fim = int(data_final.year)
            if(ano_inicio == ano_fim):
                chefes = ChefeExecutivo.get_chefe_anual(
                    ano_inicio, chefes_executivo)
            else:
                chefes = ChefeExecutivo.get_chefe_periodo(
                    ano_inicio, ano_fim, chefes_executivo)
        else:
            chefes = chefes_executivo

        return chefes

    @staticmethod
    def get_chefe_anual(ano, chefes_executivo):
        chefes = []
        for chefe in chefes_executivo:
            ano_valido = (chefe.mandato_ano_inicio <= ano) and (
                chefe.mandato_ano_fim >= ano)
            if(ano_valido):
                chefes.append(chefe)

        return chefes

    @staticmethod
    def get_chefe_periodo(ano_inicio, ano_fim, chefes_executivo):
        chefes = []
        for chefe in chefes_executivo:
            ano_inicio_valido = (ano_inicio >= chefe.mandato_ano_inicio) and (
                ano_inicio <= chefe.mandato_ano_fim)
            ano_fim_valido = (ano_fim >= chefe.mandato_ano_inicio) and (
                ano_fim <= chefe.mandato_ano_fim)
            mandato_ano_inicio_valido = (
                chefe.mandato_ano_inicio >= ano_inicio) and (
                chefe.mandato_ano_inicio <= ano_fim)
            mandato_ano_fim_valido = (
                chefe.mandato_ano_fim >= ano_inicio) and (
                chefe.mandato_ano_fim <= ano_fim)

            if(ano_inicio_valido or ano_fim_valido or
                    mandato_ano_inicio_valido or mandato_ano_fim_valido):
                chefes.append(chefe)

        return chefes

    def get_titulo_chefe(self):
        titulo = ""
        casas_legislativas = self.casas_legislativas.all()
        for casa in casas_legislativas:
            esfera = casa.esfera
            genero_masculino = self.genero == M
            if(esfera == FEDERAL):
                if(genero_masculino):
                    titulo = "Presidente"
                else:
                    titulo = "Presidenta"
            elif(esfera == MUNICIPAL):
                if(genero_masculino):
                    titulo = "Prefeito"
                else:
                    titulo = "Prefeita"
        return titulo


class PeriodoCasaLegislativa(object):
    """Atributos:
        ini, fim -- objetos datetime
        string -- Descrição do período
        quantidade_votacoes -- inteiro
    """

    def __init__(self, data_inicio, data_fim, quantidade_votacoes=0):
        # TODO self.casa_legislativa = ...
        self.ini = data_inicio
        self.fim = data_fim
        self.quantidade_votacoes = quantidade_votacoes

    def __str__(self):
        data_string = ''
#       data_string = str(self.ini.year) # sempre começa com o ano
        delta = self.fim - self.ini
        if delta.days < 35:  # período é de um mês
            meses = ['',
                     'Jan',
                     'Fev',
                     'Mar',
                     'Abr',
                     'Maio',
                     'Jun',
                     'Jul',
                     'Ago',
                     'Set',
                     'Out',
                     'Nov',
                     'Dez']
            data_string += str(self.ini.year)
            data_string += " " + str(meses[self.ini.month])
        elif delta.days < 200:  # periodo é de um semestre
            data_string += str(self.ini.year)
            if self.ini.month < 6:
                data_string += " 1o Semestre"
            else:
                data_string += " 2o Semestre"
        elif delta.days < 370:  # periodo é de um ano
            data_string += str(self.ini.year)
        elif delta.days < 750:  # periodo é um biênio
            data_string += str(self.ini.year) + " e "
            data_string += str(self.fim.year)
        elif delta.days < 1500:  # periodo é um quadriênio
            data_string += str(self.ini.year) + " a "
            data_string += str(self.fim.year)
        return data_string


class Parlamentar(models.Model):
    """Um parlamentar em uma determinada situação.
    Se o cidadão troca de partido, de casa legislativa ou de localidade,
    passa a ser um novo Parlamentar.

    Atributos:
        id_parlamentar --
                    string identificadora de acordo a fonte de dados
                    (i.e., pode se repetir entre diferentes casa legislativas)
        nome, genero -- strings
        casa_legislativa -- tipo CasaLegislativa
        partido -- tipo Partido
        localidade -- string, ex: RJ (usado apenas para parlamentares federais)
    """
    id_parlamentar = models.CharField(max_length=100, blank=True)
    nome = models.CharField(max_length=100)
    genero = models.CharField(max_length=10, choices=GENEROS, blank=True)
    casa_legislativa = models.ForeignKey(CasaLegislativa, null=True)
    partido = models.ForeignKey(Partido)
    localidade = models.CharField(max_length=100, blank=True)

    def __unicode__(self):
        return '%s - %s' % (self.nome, self.partido.nome)


class Proposicao(models.Model):
    """Proposição parlamentar (proposta de lei).

    Atributos:
        id_prop - string identificadora de acordo a fonte de dados
        sigla, numero, ano -- strings que formam o nome legal da proposição
        ementa-- descrição sucinta e oficial
        descricao -- descrição mais detalhada
        indexacao -- palavras chaves
        autores -- lista de objetos do tipo Parlamentar
        data_apresentacao -- quando foi proposta
        situacao -- como está agora
        casa_legislativa -- objeto do tipo CasaLegislativa

    Métodos:
        nome: retorna "sigla numero/ano"
    """
    # obs: id_prop não é chave primária!
    id_prop = models.CharField(max_length=100, blank=True)
    sigla = models.CharField(max_length=10)
    numero = models.CharField(max_length=10)
    ano = models.CharField(max_length=4)
    ementa = models.TextField(blank=True)
    descricao = models.TextField(blank=True)
    indexacao = models.TextField(blank=True)
    data_apresentacao = models.DateField(null=True)
    situacao = models.TextField(blank=True)
    casa_legislativa = models.ForeignKey(CasaLegislativa, null=True)
    autor_principal = models.TextField(blank=True)
    # TODO
    # autor_principal = models.ForeignKey(
    #    Parlamentar,
    #    null=True,
    #    related_name='Autor principal')
    autores = models.ManyToManyField(
        Parlamentar,
        null=True,
        related_name='demais_autores')

    def nome(self):
        return "%s %s/%s" % (self.sigla, self.numero, self.ano)

    def __unicode__(self):
        return "[%s] %s" % (self.nome(), self.ementa)


class Votacao(models.Model):
    """Votação em planário.

    Atributos:
        id_vot - string identificadora de acordo a fonte de dados
        descricao, resultado -- strings
        data -- data da votação (tipo date)
        resultado -- string
        proposicao -- objeto do tipo Proposicao

    Métodos:
        votos()
        por_partido()
    """
    # obs: id_vot não é chave primária!
    id_vot = models.CharField(max_length=100, blank=True)
    descricao = models.TextField(blank=True)
    data = models.DateField(blank=True, null=True)
    resultado = models.TextField(blank=True)
    proposicao = models.ForeignKey(Proposicao, null=True)

    def votos(self):
        """Retorna os votos da votação (depende do banco de dados)"""
        return self.voto_set.all()

    def por_partido(self):
        """Retorna votos agregados por partido.

        Retorno: um dicionário cuja chave é o nome do partido (string)
        e o valor é um VotoPartido
        """
        dic = {}
        for voto in self.votos():
            part = voto.parlamentar.partido.nome
            if part not in dic:
                dic[part] = VotoPartido(part)
            voto_partido = dic[part]
            voto_partido.add(voto.opcao)
        return dic

    @staticmethod
    def por_casa_legislativa(casa_legislativa,
                             data_inicial=None,
                             data_final=None):
        votacoes = Votacao.objects.filter(
            proposicao__casa_legislativa=casa_legislativa)
        from django.utils.dateparse import parse_datetime
        if data_inicial is not None:
            ini = parse_datetime('%s 0:0:0' % data_inicial)
            votacoes = votacoes.filter(data__gte=ini)
        if data_final is not None:
            fim = parse_datetime('%s 0:0:0' % data_final)
            votacoes = votacoes.filter(data__lte=fim)
        return votacoes

    # TODO def por_uf(self):

    def __unicode__(self):
        if self.data:
            return "[%s] %s" % (self.data, self.descricao)
        else:
            return self.descricao


class Voto(models.Model):
    """Um voto dado por um parlamentar em uma votação.

    Atributos:
        parlamentar -- objeto do tipo Parlamentar
        opcao -- qual foi o voto do parlamentar
                (sim, não, abstenção, obstrução, não votou)
    """

    votacao = models.ForeignKey(Votacao)
    parlamentar = models.ForeignKey(Parlamentar)
    opcao = models.CharField(max_length=10, choices=OPCOES)

    def __unicode__(self):
        return "%s votou %s" % (self.parlamentar, self.opcao)


class VotosAgregados:
    """Um conjunto de votos.

    Atributos:
        sim, nao, abstencao --
            inteiros que representam a quantidade de votos no conjunto

    Método:
        add
        total
        voto_medio
    """

    def __init__(self):
        self.sim = 0
        self.nao = 0
        self.abstencao = 0

    def add(self, voto):
        """Adiciona um voto ao conjunto de votos.

        Argumentos:
            voto -- string \in {SIM, NAO, ABSTENCAO, AUSENTE, OBSTRUCAO}
            OBSTRUCAO conta como um voto ABSTENCAO
            AUSENTE não conta como um voto
        """
        if voto == SIM:
            self.sim += 1
        if voto == NAO:
            self.nao += 1
        if voto == ABSTENCAO:
            self.abstencao += 1
        if voto == OBSTRUCAO:
            self.abstencao += 1
        # if (voto == AUSENTE):
        #    self.abstencao += 1

    def total(self):
        return self.sim + self.nao + self.abstencao

    def voto_medio(self):
        """Valor real que representa a 'opnião média' dos
            votos agregados; 1 representa sim e -1 representa não."""
        total = self.total()
        if total > 0:
            return 1.0 * (self.sim - self.nao) / self.total()
        else:
            return 0

    def __unicode__(self):
        return '(%s, %s, %s)' % (self.sim, self.nao, self.abstencao)

    def __str__(self):
        return str(self).encode('utf-8')


class VotoPartido(VotosAgregados):
    """Um conjunto de votos de um partido.

    Atributos:
        partido -- objeto do tipo Partido
        sim, nao, abstencao --
            inteiros que representam a quantidade de votos no conjunto
    """
    def __init__(self, partido):
        VotosAgregados.__init__(self)
        self.partido = partido