Skip to content

Commit

Permalink
Merge pull request #308 from nossas/feature/second-round-filters
Browse files Browse the repository at this point in the history
Add second round and elected filters at search view
  • Loading branch information
igr-santos authored Oct 11, 2024
2 parents e16b13f + 557f0ea commit 5a0b5d8
Show file tree
Hide file tree
Showing 15 changed files with 428,557 additions and 10 deletions.
2 changes: 1 addition & 1 deletion app/contrib/bonde/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ class PlacesIBGE(models.Model):
codigo_municipio_completo = models.CharField(max_length=10)
nome_municipio = models.CharField(max_length=100)
distrito = models.CharField(max_length=10)
codigo_distrito_completo = models.CharField(max_length=15)
codigo_distrito_completo = models.CharField(max_length=15, primary_key=True)
nome_distrito = models.CharField(max_length=100)
sigla_uf = models.CharField(max_length=2)

Expand Down
17 changes: 13 additions & 4 deletions app/org_eleicoes/votepeloclima/candidature/admin.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
from django.contrib import admin

from .forms.register import RegisterAdminForm
from .models import Candidature, CandidatureFlow
from .models import Candidature, CandidatureFlow, ElectionResult
from .choices import PoliticalParty


class ElectionResultsInline(admin.StackedInline):
model = ElectionResult
extra = 1

class CandidatureAdmin(admin.ModelAdmin):
search_fields = ("legal_name", "ballot_name", "email", "political_party")
list_display = ("legal_name", "email", "political_party", "status", "updated_at")
ordering = ("updated_at",)
inlines = (ElectionResultsInline,)

def get_readonly_fields(self, request, obj=None):
readonly_fields = []
for field in obj._meta.get_fields():
if not field.is_relation:
readonly_fields.append(field.name)
return readonly_fields

def has_add_permission(self, request):
return False

def has_change_permission(self, request, obj=None):
return False

def has_delete_permission(self, request, obj=None):
return False

Expand Down
8 changes: 7 additions & 1 deletion app/org_eleicoes/votepeloclima/candidature/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,10 @@ class Education(models.TextChoices):
mestrado_incompleto = "mestrado_incompleto", "Mestrado Incompleto"
mestrado_completo = "mestrado_completo", "Mestrado Completo"
doutorado_incompleto = "doutorado_incompleto", "Doutorado Incompleto"
doutorado_completo = "doutorado_completo", "Doutorado Completo"
doutorado_completo = "doutorado_completo", "Doutorado Completo"

class ElectionStatus(models.TextChoices):
empty = "", "Selecione uma opção"
eleita = "eleita", "Eleita/o"
segundo_turno = "segundo_turno", "Segundo Turno"
nao_eleita = "nao_eleito", "Não Eleita/o"

Large diffs are not rendered by default.

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions app/org_eleicoes/votepeloclima/candidature/forms/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# from django_select2.forms import Select2Widget

from ..layout import NoCrispyField
from ..choices import Gender, Color, Sexuality
from ..choices import Gender, Color, Sexuality, ElectionStatus
from ..fields import CepField, ButtonCheckboxSelectMultiple, ButtonRadioSelect
from ..locations_utils import get_states, get_choices
from ..models import Candidature
Expand Down Expand Up @@ -68,6 +68,16 @@ def __init__(self, *args, **kwargs):


class FilterFormSidebar(RemoveRequiredMixin, forms.ModelForm):
election_status = forms.ChoiceField(
label="Filtrar por",
choices=[
("second_round", "Candidaturas no 2º turno"),
("all", "Todas as candidaturas"),
("elected", "Candidaturas eleitas")
],
widget=ButtonRadioSelect,
initial="second_round" # Define o filtro de 2º turno como padrão
)
proposes = forms.MultipleChoiceField(
label="Propostas", widget=ButtonCheckboxSelectMultiple
)
Expand All @@ -88,14 +98,15 @@ class FilterFormSidebar(RemoveRequiredMixin, forms.ModelForm):

class Meta:
model = Candidature
fields = ["proposes", "mandate_type", "gender", "color", "sexuality"]
fields = ["election_status", "proposes", "mandate_type", "gender", "color", "sexuality"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.fields["proposes"].choices = self.get_proposes_choices()

self.helper.layout = Layout(
NoCrispyField("election_status"),
NoCrispyField("proposes"),
NoCrispyField("mandate_type"),
NoCrispyField("gender"),
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import csv

from django.core.management.base import BaseCommand
from contrib.bonde.models import PlacesIBGE

from ...models import Candidature, ElectionResult
from ...choices import ElectionStatus


class Command(BaseCommand):
help = 'Popula os resultados eleitorais a partir de arquivos CSV'

def add_arguments(self, parser):
parser.add_argument(
'eleicao',
type=str,
help="Tipo de eleição: 'vereador' ou 'prefeito'."
)

parser.add_argument(
'ano',
type=int,
help="Ano da eleição."
)

def map_situacao_to_status(self, situacao, eleicao):
"""
Mapeia as situações eleitorais do CSV para os valores do ElectionStatus.
"""
situacao = situacao.lower()

if eleicao == "prefeito":
if situacao == "eleito":
return ElectionStatus.eleita
elif situacao == "2º turno":
return ElectionStatus.segundo_turno
elif situacao == "não eleito":
return ElectionStatus.nao_eleita
elif eleicao == "vereador":
if situacao in ["eleito", "eleito por qp", "eleito por média"]:
return ElectionStatus.eleita
elif situacao == "não eleito" or situacao == "suplente":
return ElectionStatus.nao_eleita

return ElectionStatus.empty

def handle(self, *args, **kwargs):
eleicao = kwargs['eleicao']
ano = kwargs['ano']

# Define o caminho correto para o CSV com base no tipo de eleição
if eleicao == 'vereador':
csv_path = 'org_eleicoes/votepeloclima/candidature/csv/resultados_primeiro_turno_vereacao_2024.csv'
csv_key_num = 'numero_na_urna'
elif eleicao == 'prefeito':
csv_path = 'org_eleicoes/votepeloclima/candidature/csv/resultados_primeiro_turno_prefeituras_2024.csv'
csv_key_num = 'numero'
else:
self.stdout.write(self.style.ERROR("Tipo de eleição inválido. Use 'vereador' ou 'prefeito'."))
return

try:
# Carrega o CSV em um dicionário com uma chave composta
csv_data = {}
with open(csv_path, mode='r', encoding='utf-8-sig') as csv_file:
reader = csv.DictReader(csv_file)
for row in reader:
numero = row[csv_key_num].strip()
municipio = row['municipio'].strip().lower() # Normaliza para lowercase
estado = row['estado'].strip().upper()

# Cria a chave composta (numero, municipio, estado)
chave_composta = f"{numero}|{municipio}|{estado}"

csv_data[chave_composta] = row

# Carrega todas as candidaturas da base de dados
if eleicao == 'vereador':
candidatures = Candidature.objects.filter(election_year=ano, intended_position='vereacao')
elif eleicao == 'prefeito':
candidatures = Candidature.objects.filter(election_year=ano, intended_position='prefeitura')

# Itera pelas candidaturas e busca os resultados no CSV
for candidature in candidatures:
numero = str(candidature.number_id)

# Busca o município e estado no banco de dados
places = PlacesIBGE.objects.filter(
codigo_municipio_completo=candidature.city,
uf=candidature.state
)

if not places.exists():
self.stdout.write(self.style.WARNING(f"Nenhum lugar encontrado para a candidatura {numero}. Ignorando..."))
continue

place = places.first()

municipio = place.nome_municipio.lower() # Normaliza para lowercase
estado = place.sigla_uf.upper()

# Cria a chave composta para a candidatura atual
chave_composta = f"{numero}|{municipio}|{estado}"

# Verifica se a candidatura atual existe no CSV
if chave_composta in csv_data:
row = csv_data[chave_composta]

# Ajusta as colunas com base no tipo de eleição
situacao = row['situacao'].strip()

# Mapeia a situação para o ElectionStatus
election_status = self.map_situacao_to_status(situacao, eleicao)

if election_status == ElectionStatus.empty:
self.stdout.write(self.style.WARNING(f"Situação inválida encontrada no CSV: {situacao}. Ignorando..."))
continue

# Cria ou atualiza o resultado da eleição
ElectionResult.objects.update_or_create(
candidature=candidature,
defaults={'status': election_status}
)
self.stdout.write(self.style.SUCCESS(f"Resultado da eleição atualizado para candidatura {numero} em {municipio}, {estado}."))
else:
self.stdout.write(self.style.WARNING(f"Resultado não encontrado no CSV para candidatura {numero} em {municipio}, {estado}. Ignorando..."))

except FileNotFoundError:
self.stdout.write(self.style.ERROR(f"Arquivo CSV não encontrado no caminho: {csv_path}"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2 on 2024-10-08 14:40

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('candidature', '0022_alter_candidature_ballot_name_and_more'),
]

operations = [
migrations.CreateModel(
name='ElectionResult',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('eleita', 'Eleita/o'), ('segundo_turno', 'Segundo Turno'), ('nao_eleito', 'Não Eleita/o')], max_length=20, verbose_name='Status da eleição')),
],
),
migrations.AddField(
model_name='candidature',
name='election_year',
field=models.PositiveIntegerField(default=2024, verbose_name='Ano da eleição'),
),
migrations.AddConstraint(
model_name='candidature',
constraint=models.UniqueConstraint(fields=('cpf', 'election_year'), name='unique_cpf_per_year'),
),
migrations.AddField(
model_name='electionresult',
name='candidature',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='election_results', to='candidature.candidature'),
),
]
16 changes: 15 additions & 1 deletion app/org_eleicoes/votepeloclima/candidature/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.utils.text import slugify
from django.utils.html import mark_safe

from .choices import CandidatureFlowStatus, IntendedPosition, PoliticalParty, Gender, Color, Sexuality, Education
from .choices import CandidatureFlowStatus, IntendedPosition, PoliticalParty, Gender, Color, Sexuality, Education, ElectionStatus
from .locations_utils import get_choices, get_ufs


Expand Down Expand Up @@ -38,6 +38,8 @@ class Candidature(models.Model):
proposes = models.JSONField(blank=True, verbose_name="Propostas")
appointments = models.JSONField(blank=True, verbose_name="Compromissos")

election_year = models.PositiveIntegerField(default=2024, verbose_name="Ano da eleição")

# friendly url by ballot_name
slug = models.SlugField(max_length=100, unique=True, blank=True, null=True)

Expand All @@ -48,6 +50,9 @@ class Candidature(models.Model):
class Meta:
verbose_name = "Candidatura"
ordering = ["-updated_at"]
constraints = [
models.UniqueConstraint(fields=['cpf', 'election_year'], name='unique_cpf_per_year')
]

@property
def status(self):
Expand Down Expand Up @@ -104,13 +109,22 @@ def get_proposes_items(self):

return proposes_list

@property
def get_election_result(self):
return self.election_results.first().status


def save(self, *args, **kwargs):
if not self.slug:
self.slug = f"{slugify(self.ballot_name)}-{self.number_id}"
super().save(*args, **kwargs)


class ElectionResult(models.Model):
candidature = models.ForeignKey('Candidature', on_delete=models.CASCADE, related_name='election_results')
status = models.CharField(max_length=20, choices=ElectionStatus.choices, verbose_name="Status da eleição")


class CandidatureFlow(models.Model):
# Propriedades só podem ser editadas quando status for `draft`
photo = models.ImageField(upload_to="candidatures/photos/", null=True, verbose_name="Foto")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ legend.form-label {
object-fit: cover;
object-position: top;
}

.card-flag {
border-radius: 10px 0px 0px 10px;
}

.card-footer {
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ <h1 class="text-uppercase fw-bold">{{ candidature.ballot_name }} | {{ candidatur
{% if candidature.is_collective_mandate %}
<span class="badge bg-primary fs-6">Mandato Coletivo</span>
{% endif %}
{% if candidature.get_election_result == "segundo_turno" %}
<span class="badge bg-primary fs-6">2º Turno</span>
{% endif %}
{% if candidature.get_election_result == "eleita" %}
<span class="badge bg-primary fs-6">Eleito/a</span>
{% endif %}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ <h6 class="text-uppercase fw-bold"><i class="ds-sidebar-bars"></i> Filtrar resul
{% for candidature in candidatures %}
<div class="g-col-12 g-col-md-6 g-col-lg-4">
<div class="card">
<a href="{% url 'candidate_profile' candidature.slug %}">
<a class="position-relative" href="{% url 'candidate_profile' candidature.slug %}">
<img src="{{ candidature.photo|thumbnail_url:'profile-photo' }}" class="card-img-top" alt="Foto de {{ candidature.legal_name }}">
{% if candidature.get_election_result == "segundo_turno" %}
<span class="card-flag badge bg-primary position-absolute top-0 end-0 mt-3 p-2">2º Turno</span>
{% endif %}
{% if candidature.get_election_result == "eleita" %}
<span class="card-flag badge bg-secondary position-absolute top-0 end-0 mt-3 p-2">Eleito/a</span>
{% endif %}
</a>
<div class="card-body px-2 py-1">
<p class="state-city mb-1 text-black-50">{{ candidature.get_city_display }} - {{ candidature.get_state_display }}</p>
Expand Down
13 changes: 13 additions & 0 deletions app/org_eleicoes/votepeloclima/candidature/views/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from ..models import CandidatureFlowStatus, Candidature
from ..forms.filters import FilterFactoryForm
from ..choices import ElectionStatus


class CandidatureSearchView(ListView):
Expand Down Expand Up @@ -77,6 +78,18 @@ def get_queryset(self):
)
)

election_status = cleaned_data.get("election_status", "second_round")
else:
election_status = "second_round"

# Filtra com base no status da eleição
if election_status == "second_round":
# Filtra candidaturas que foram para o 2º turno
queryset = queryset.filter(election_results__status=ElectionStatus.segundo_turno)
elif election_status == "elected":
# Filtra candidaturas eleitas
queryset = queryset.filter(election_results__status=ElectionStatus.eleita)

return queryset

def get_context_data(self, **kwargs):
Expand Down

0 comments on commit 5a0b5d8

Please sign in to comment.