diff --git a/OpenCand.API/Repository/CandidatoRepository.cs b/OpenCand.API/Repository/CandidatoRepository.cs index 46f710f..8a82c2c 100644 --- a/OpenCand.API/Repository/CandidatoRepository.cs +++ b/OpenCand.API/Repository/CandidatoRepository.cs @@ -90,5 +90,16 @@ namespace OpenCand.Repository return (await connection.QueryAsync(query, new { idcandidato })).AsList(); } } + + public async Task?> GetCandidatoExtById(Guid idcandidato) + { + using (var connection = new NpgsqlConnection(ConnectionString)) + { + var query = @" + SELECT * FROM candidato_ext + WHERE idcandidato = @idcandidato"; + return (await connection.QueryAsync(query, new { idcandidato })).AsList(); + } + } } } diff --git a/OpenCand.API/Services/OpenCandService.cs b/OpenCand.API/Services/OpenCandService.cs index eb3966c..32d4573 100644 --- a/OpenCand.API/Services/OpenCandService.cs +++ b/OpenCand.API/Services/OpenCandService.cs @@ -59,11 +59,7 @@ namespace OpenCand.API.Services { var result = await candidatoRepository.GetCandidatoAsync(idcandidato); var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato); - - foreach (var eleicao in eleicoes) - { - eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido); - } + var candidatoExt = await candidatoRepository.GetCandidatoExtById(idcandidato); if (result == null) { @@ -74,10 +70,16 @@ namespace OpenCand.API.Services throw new KeyNotFoundException($"CandidatoMapping for ID {idcandidato} not found."); } + foreach (var eleicao in eleicoes) + { + eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido); + } + var lastEleicao = eleicoes.OrderByDescending(e => e.Ano).First(); result.FotoUrl = $"{fotoSettings.ApiBasePath}/foto_cand{lastEleicao.Ano}_{lastEleicao.SiglaUF}_div/F{lastEleicao.SiglaUF}{lastEleicao.SqCandidato}_div.jpg"; result.Eleicoes = eleicoes.OrderByDescending(e => e.Ano).ToList(); + result.CandidatoExt = candidatoExt.OrderByDescending(ce => ce.Ano).ToList(); return result; } diff --git a/OpenCand.Core/Models/Candidato.cs b/OpenCand.Core/Models/Candidato.cs index d9716db..5ab741a 100644 --- a/OpenCand.Core/Models/Candidato.cs +++ b/OpenCand.Core/Models/Candidato.cs @@ -20,14 +20,14 @@ namespace OpenCand.Core.Models public string Sexo { get; set; } - public string EstadoCivil { get; set; } + public string Localidade { get; set; } - public string Escolaridade { get; set; } - - public string Ocupacao { get; set; } + public int Ultimoano { get; set; } public List Eleicoes { get; set; } + public List CandidatoExt { get; set; } + // API ONLY public string FotoUrl { get; set; } } @@ -37,7 +37,6 @@ namespace OpenCand.Core.Models public Guid IdCandidato { get; set; } public string Cpf { get; set; } public string Nome { get; set; } - public string Apelido { get; set; } public string SqCandidato { get; set; } public int Ano { get; set; } public string Turno { get; set; } @@ -52,6 +51,17 @@ namespace OpenCand.Core.Models public Partido? Partido { get; set; } // Nullable to allow for candidates without a party } + public class CandidatoExt + { + public Guid IdCandidato { get; set; } + public int Ano { get; set; } + public string Apelido { get; set; } + public string Email { get; set; } + public string EstadoCivil { get; set; } + public string Escolaridade { get; set; } + public string Ocupacao { get; set; } + } + public class RedeSocial { public Guid IdCandidato { get; set; } diff --git a/OpenCand.ETL/Extensions/StringExtensions.cs b/OpenCand.ETL/Extensions/StringExtensions.cs index 00f2f62..f5ea50b 100644 --- a/OpenCand.ETL/Extensions/StringExtensions.cs +++ b/OpenCand.ETL/Extensions/StringExtensions.cs @@ -2,11 +2,14 @@ { public static class StringExtensions { - public static bool IsNullOrEmpty(this string value) + public static bool IsNullOrEmpty(this string? value) { return string.IsNullOrEmpty(value) || value == "#NE" || - value == "#NULO"; + value == "#NULO" || + value == "#NULO#" || + value == "NÃO DIVULGÁVEL" || + value == "-4"; } } } diff --git a/OpenCand.ETL/Parser/Models/CandidatoCSV.cs b/OpenCand.ETL/Parser/Models/CandidatoCSV.cs index 98a987b..cbb3e77 100644 --- a/OpenCand.ETL/Parser/Models/CandidatoCSV.cs +++ b/OpenCand.ETL/Parser/Models/CandidatoCSV.cs @@ -36,10 +36,10 @@ namespace OpenCand.Parser.Models public string Apelido { get; set; } [Name("NR_CPF_CANDIDATO")] - public string CPFCandidato { get; set; } + public string? CPFCandidato { get; set; } [Name("DS_EMAIL", "NM_EMAIL")] - public string Email { get; set; } + public string? Email { get; set; } [Name("DT_NASCIMENTO")] public string DataNascimento { get; set; } @@ -57,7 +57,7 @@ namespace OpenCand.Parser.Models public string GrauInstrucao { get; set; } [Name("DS_SIT_TOT_TURNO")] - public string SituacaoTurno { get; set; } + public string? SituacaoTurno { get; set; } [Name("NR_PARTIDO")] public int NumeroPartido { get; set; } diff --git a/OpenCand.ETL/Parser/ParserServices/CandidatoParserService.cs b/OpenCand.ETL/Parser/ParserServices/CandidatoParserService.cs index 57bb9ef..26a56fb 100644 --- a/OpenCand.ETL/Parser/ParserServices/CandidatoParserService.cs +++ b/OpenCand.ETL/Parser/ParserServices/CandidatoParserService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using OpenCand.Core.Models; using OpenCand.ETL.Contracts; +using OpenCand.ETL.Extensions; using OpenCand.Parser.Models; using OpenCand.Services; @@ -22,62 +23,80 @@ namespace OpenCand.ETL.Parser.ParserServices public async Task ParseObject(CandidatoCSV record) { - if (string.IsNullOrWhiteSpace(record.CPFCandidato) || record.CPFCandidato.Length <= 3) + if (record.CPFCandidato?.Length <= 3 || record.CPFCandidato.IsNullOrEmpty()) { record.CPFCandidato = null; // Handle null/empty/whitespace CPF } + else + { + record.CPFCandidato = record.CPFCandidato.Trim(); + } - if (record.NomeCandidato == "NÃO DIVULGÁVEL" || - string.IsNullOrEmpty(record.NomeCandidato) || - record.NomeCandidato == "#NULO") + if (record.Apelido.IsNullOrEmpty()) + { + record.Apelido = null; + } + + if (record.NomeCandidato.IsNullOrEmpty()) { logger.LogCritical($"ParseCandidatosAsync - Candidate with id {record.SequencialCandidato} with invalid name, skipping..."); return; // Skip candidates with invalid name } - if (string.IsNullOrWhiteSpace(record.Apelido) || - record.Apelido.Contains("#NUL") || - record.Apelido.Contains("NULO#") || - record.Apelido.Contains("#NE")) + if (record.Apelido.IsNullOrEmpty()) { record.Apelido = null; } + if (record.SituacaoTurno.IsNullOrEmpty()) + { + record.SituacaoTurno = null; + } + var candidato = new Candidato { Cpf = record.CPFCandidato, SqCandidato = record.SequencialCandidato, - Nome = record.NomeCandidato, - Apelido = record.Apelido, - Email = record.Email.Contains("@") ? record.Email : null, - Sexo = record.Genero, - EstadoCivil = record.EstadoCivil, - Escolaridade = record.GrauInstrucao, - Ocupacao = record.Ocupacao, + Nome = record.NomeCandidato.Trim(), + Apelido = record.Apelido?.Trim(), + Sexo = record.Genero.Trim(), + Localidade = record.NomeUE.Trim(), + Ultimoano = record.AnoEleicao, Eleicoes = new List() - { - new CandidatoMapping - { - Cpf = record.CPFCandidato, - Nome = record.NomeCandidato, - Apelido = record.Apelido, - SqCandidato = record.SequencialCandidato, - Ano = record.AnoEleicao, - Turno = record.Turno, - TipoEleicao = record.TipoAbrangencia, - NomeUE = record.NomeUE, - SiglaUF = record.SiglaUF, - Cargo = record.DescricaoCargo, - NrCandidato = record.NumeroCandidato, - Resultado = record.SituacaoTurno, - Partido = new Partido - { - Sigla = record.SiglaPartido, - Nome = record.NomePartido, - Numero = record.NumeroPartido, - } - } - } + { + new CandidatoMapping + { + Cpf = record.CPFCandidato, + Nome = record.NomeCandidato.Trim(), + SqCandidato = record.SequencialCandidato.Trim(), + Ano = record.AnoEleicao, + Turno = record.Turno.Trim(), + TipoEleicao = record.TipoAbrangencia.Trim(), + NomeUE = record.NomeUE.Trim(), + SiglaUF = record.SiglaUF.Trim(), + Cargo = record.DescricaoCargo.Trim(), + NrCandidato = record.NumeroCandidato.Trim(), + Resultado = record.SituacaoTurno?.Trim() ?? "-", + Partido = new Partido + { + Sigla = record.SiglaPartido.Trim(), + Nome = record.NomePartido.Trim(), + Numero = record.NumeroPartido, + } + } + }, + CandidatoExt = new List() + { + new CandidatoExt + { + Apelido = record.Apelido?.Trim(), + EstadoCivil = record.EstadoCivil.Trim(), + Escolaridade = record.GrauInstrucao.Trim(), + Ocupacao = record.Ocupacao.Trim(), + Ano = record.AnoEleicao, + Email = record.Email.IsNullOrEmpty() ? null : record.Email.Trim() + } + } }; if (!string.IsNullOrEmpty(record.DataNascimento) && diff --git a/OpenCand.ETL/Repository/CandidatoRepository.cs b/OpenCand.ETL/Repository/CandidatoRepository.cs index c1d2ef3..7dfdb3a 100644 --- a/OpenCand.ETL/Repository/CandidatoRepository.cs +++ b/OpenCand.ETL/Repository/CandidatoRepository.cs @@ -16,18 +16,17 @@ namespace OpenCand.Repository using (var connection = new NpgsqlConnection(ConnectionString)) { await connection.ExecuteAsync(@" - INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, email, sexo, estadocivil, escolaridade, ocupacao) - VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @email, @sexo, @estadocivil, @escolaridade, @ocupacao) + INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, sexo, ultimoano, localidade) + VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @sexo, @ultimoano, @localidade) ON CONFLICT (idcandidato) DO UPDATE SET cpf = EXCLUDED.cpf, nome = EXCLUDED.nome, + apelido = EXCLUDED.apelido, datanascimento = EXCLUDED.datanascimento, - email = EXCLUDED.email, sexo = EXCLUDED.sexo, - estadocivil = EXCLUDED.estadocivil, - escolaridade = EXCLUDED.escolaridade, - ocupacao = EXCLUDED.ocupacao, - apelido = EXCLUDED.apelido;", + localidade = EXCLUDED.localidade, + ultimoano = EXCLUDED.ultimoano + WHERE candidato.ultimoano IS NULL OR EXCLUDED.ultimoano > candidato.ultimoano;", new { idcandidato = candidato.IdCandidato, @@ -35,11 +34,9 @@ namespace OpenCand.Repository nome = candidato.Nome, apelido = candidato.Apelido, datanascimento = candidato.DataNascimento, - email = candidato.Email, sexo = candidato.Sexo, - estadocivil = candidato.EstadoCivil, - escolaridade = candidato.Escolaridade, - ocupacao = candidato.Ocupacao + localidade = candidato.Localidade, + ultimoano = candidato.Ultimoano }); } } @@ -49,15 +46,14 @@ namespace OpenCand.Repository using (var connection = new NpgsqlConnection(ConnectionString)) { await connection.ExecuteAsync(@" - INSERT INTO candidato_mapping (idcandidato, cpf, nome, apelido, sqcandidato, ano, turno, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado) - VALUES (@idcandidato, @cpf, @nome, @apelido, @sqcandidato, @ano, @turno, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado) + INSERT INTO candidato_mapping (idcandidato, cpf, nome, sqcandidato, ano, turno, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado) + VALUES (@idcandidato, @cpf, @nome, @sqcandidato, @ano, @turno, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado) ON CONFLICT DO NOTHING;", new { idcandidato = candidatoMapping.IdCandidato, cpf = candidatoMapping.Cpf, nome = candidatoMapping.Nome, - apelido = candidatoMapping.Apelido, sqcandidato = candidatoMapping.SqCandidato, ano = candidatoMapping.Ano, turno = candidatoMapping.Turno, @@ -72,6 +68,32 @@ namespace OpenCand.Repository } } + public async Task AddCandidatoExtAsync(CandidatoExt candidatoExt) + { + using (var connection = new NpgsqlConnection(ConnectionString)) + { + await connection.ExecuteAsync(@" + INSERT INTO candidato_ext (idcandidato, ano, apelido, email, estadocivil, escolaridade, ocupacao) + VALUES (@idcandidato, @ano, @apelido, @email, @estadocivil, @escolaridade, @ocupacao) + ON CONFLICT (idcandidato, ano) DO UPDATE SET + apelido = EXCLUDED.apelido, + email = EXCLUDED.email, + estadocivil = EXCLUDED.estadocivil, + escolaridade = EXCLUDED.escolaridade, + ocupacao = EXCLUDED.ocupacao;", + new + { + idcandidato = candidatoExt.IdCandidato, + ano = candidatoExt.Ano, + apelido = candidatoExt.Apelido, + email = candidatoExt.Email, + estadocivil = candidatoExt.EstadoCivil, + escolaridade = candidatoExt.Escolaridade, + ocupacao = candidatoExt.Ocupacao + }); + } + } + public async Task GetCandidatoByCpf(string cpf) { using (var connection = new NpgsqlConnection(ConnectionString)) diff --git a/OpenCand.ETL/Services/CandidatoService.cs b/OpenCand.ETL/Services/CandidatoService.cs index c0b1635..2d5fbfa 100644 --- a/OpenCand.ETL/Services/CandidatoService.cs +++ b/OpenCand.ETL/Services/CandidatoService.cs @@ -17,7 +17,7 @@ namespace OpenCand.Services public async Task AddCandidatoAsync(Candidato candidato) { - if (candidato == null) + if (candidato == null || candidato.CandidatoExt == null || candidato.Eleicoes == null) { throw new ArgumentNullException(nameof(candidato), "Candidato cannot be null"); } @@ -28,6 +28,7 @@ namespace OpenCand.Services } var candidatoMapping = candidato.Eleicoes.First(); + var candidatoExt = candidato.CandidatoExt.First(); // Add partido data if (candidatoMapping.Partido != null) @@ -52,18 +53,18 @@ namespace OpenCand.Services candidato.IdCandidato = existingCandidato.IdCandidato; candidato.Cpf = GetNonEmptyString(existingCandidato.Cpf, candidato.Cpf); candidato.Email = GetNonEmptyString(existingCandidato.Email, candidato.Email); - candidato.EstadoCivil = GetNonEmptyString(existingCandidato.EstadoCivil, candidato.EstadoCivil); candidato.Apelido = GetNonEmptyString(existingCandidato.Apelido, candidato.Apelido); - candidato.Escolaridade = GetNonEmptyString(existingCandidato.Escolaridade, candidato.Escolaridade); - candidato.Ocupacao = GetNonEmptyString(existingCandidato.Ocupacao, candidato.Ocupacao); candidato.Sexo = GetNonEmptyString(existingCandidato.Sexo, candidato.Sexo); candidatoMapping.IdCandidato = candidato.IdCandidato; candidatoMapping.Cpf = GetNonEmptyString(candidato.Cpf, candidatoMapping.Cpf); + candidatoExt.IdCandidato = candidato.IdCandidato; + // Update the entries for the existing candidate await candidatoRepository.AddCandidatoAsync(candidato); await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping); + await candidatoRepository.AddCandidatoExtAsync(candidatoExt); return; } @@ -79,10 +80,12 @@ namespace OpenCand.Services if (existingMapping != null) { candidato.IdCandidato = existingMapping.IdCandidato; + candidatoExt.IdCandidato = existingMapping.IdCandidato; + candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf); - candidato.Apelido = GetNonEmptyString(existingMapping.Apelido, candidato.Apelido); + await candidatoRepository.AddCandidatoAsync(candidato); - + await candidatoRepository.AddCandidatoExtAsync(candidatoExt); return; } @@ -94,8 +97,11 @@ namespace OpenCand.Services candidatoMapping.Cpf = candidato.Cpf; candidatoMapping.Nome = candidato.Nome; + candidatoExt.IdCandidato = candidato.IdCandidato; + await candidatoRepository.AddCandidatoAsync(candidato); await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping); + await candidatoRepository.AddCandidatoExtAsync(candidatoExt); } public string GetNonEmptyString(string? value1, string? value2) diff --git a/OpenCand.ETL/Services/RedeSocialService.cs b/OpenCand.ETL/Services/RedeSocialService.cs index d18cd55..51ae5bc 100644 --- a/OpenCand.ETL/Services/RedeSocialService.cs +++ b/OpenCand.ETL/Services/RedeSocialService.cs @@ -30,7 +30,7 @@ namespace OpenCand.Services } redeSocial.IdCandidato = candidato.IdCandidato; - redeSocial.Rede = GetRedeSocialType(redeSocial.Link); + redeSocial.Rede = GetRedeSocialType(redeSocial.Link.Trim()); await redeSocialRepository.AddRedeSocialAsync(redeSocial); } diff --git a/db/db.sql b/db/db.sql index 8fa29e0..2f8fa94 100644 --- a/db/db.sql +++ b/db/db.sql @@ -1,5 +1,6 @@ DROP TABLE IF EXISTS bem_candidato CASCADE; DROP TABLE IF EXISTS candidato_mapping CASCADE; +DROP TABLE IF EXISTS candidato_ext CASCADE; DROP TABLE IF EXISTS rede_social CASCADE; DROP TABLE IF EXISTS candidato CASCADE; DROP TABLE IF EXISTS partido CASCADE; @@ -10,14 +11,12 @@ CREATE TABLE candidato ( idcandidato UUID NOT NULL PRIMARY KEY, cpf VARCHAR(11), nome VARCHAR(255) NOT NULL, - apelido VARCHAR(255), datanascimento TIMESTAMPTZ, - email TEXT, sexo CHAR(15), - estadocivil VARCHAR(50), - escolaridade VARCHAR(50), - ocupacao VARCHAR(150), - popularidade BIGINT DEFAULT 0, + apelido VARCHAR(255), + localidade VARCHAR(100), + ultimoano INT, + popularidade BIGINT DEFAULT 0 ); CREATE INDEX idx_candidato_nome ON candidato (nome); CREATE INDEX idx_candidato_apelido ON candidato (apelido); @@ -30,7 +29,6 @@ CREATE TABLE candidato_mapping ( idcandidato UUID NOT NULL, cpf VARCHAR(11), nome VARCHAR(255) NOT NULL, - apelido VARCHAR(255), sqcandidato VARCHAR(50) NOT NULL, turno VARCHAR(2) NOT NULL, ano INT NOT NULL, @@ -44,12 +42,25 @@ CREATE TABLE candidato_mapping ( CONSTRAINT pk_candidato_mapping PRIMARY KEY (idcandidato, ano, siglauf, nomeue, cargo, nrcandidato, resultado), CONSTRAINT fk_candidato_mapping_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE ); +CREATE INDEX idx_candidato_mapping_idcandidato ON candidato_mapping (idcandidato); CREATE INDEX idx_candidato_mapping_cpf ON candidato_mapping (cpf); CREATE INDEX idx_candidato_mapping_nome ON candidato_mapping (nome); -CREATE INDEX idx_candidato_mapping_apelido ON candidato_mapping (apelido); CREATE INDEX idx_candidato_mapping_ano ON candidato_mapping (ano); CREATE INDEX idx_candidato_mapping_sqcandidato ON candidato_mapping (sqcandidato); +CREATE TABLE candidato_ext ( + idcandidato UUID NOT NULL, + ano INT NOT NULL, + apelido VARCHAR(255), + email TEXT, + estadocivil VARCHAR(50), + escolaridade VARCHAR(50), + ocupacao TEXT, + CONSTRAINT pk_candidato_ext PRIMARY KEY (idcandidato, ano) +); +CREATE INDEX idx_candidato_ext_idcandidato ON candidato_ext (idcandidato); +CREATE INDEX idx_candidato_ext_ano ON candidato_ext (ano); + ---- Table for storing assets of candidates CREATE TABLE bem_candidato ( idcandidato UUID NOT NULL, @@ -137,4 +148,12 @@ CREATE TABLE receitas_candidato ( CREATE INDEX idx_receitas_candidato_idcandidato ON receitas_candidato (idcandidato); CREATE INDEX idx_receitas_candidato_ano ON receitas_candidato (ano); CREATE INDEX idx_receitas_candidato_sqcandidato ON receitas_candidato (sqcandidato); -CREATE INDEX idx_receitas_candidato_sgpartido ON receitas_candidato (sgpartido); \ No newline at end of file +CREATE INDEX idx_receitas_candidato_sgpartido ON receitas_candidato (sgpartido); + + + +-- Search function +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE INDEX idx_candidato_nome_trgm ON candidato USING GIN (nome gin_trgm_ops); +CREATE INDEX idx_candidato_apelido_trgm ON candidato USING GIN (apelido gin_trgm_ops); +CREATE INDEX idx_candidato_cpf_trgm ON candidato USING GIN (cpf gin_trgm_ops); \ No newline at end of file