From 781dcc3bbd7e5ca6f02c51e6c795f9f6793fb78a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ses=C3=B3stris=20Vieira?= Date: Thu, 29 Apr 2021 11:23:54 -0300 Subject: [PATCH] Fix #54 --- sigi/apps/casas/forms.py | 6 + sigi/apps/casas/templates/casas/importar.html | 72 ++++++ .../templates/casas/importar_result.html | 18 ++ sigi/apps/casas/urls.py | 4 + sigi/apps/casas/views.py | 236 +++++++++++++++++- templates/admin/base_site.html | 1 + 6 files changed, 333 insertions(+), 4 deletions(-) create mode 100644 sigi/apps/casas/templates/casas/importar.html create mode 100644 sigi/apps/casas/templates/casas/importar_result.html diff --git a/sigi/apps/casas/forms.py b/sigi/apps/casas/forms.py index dca4889..a8396c4 100644 --- a/sigi/apps/casas/forms.py +++ b/sigi/apps/casas/forms.py @@ -6,6 +6,12 @@ from localflavor.br.forms import BRZipCodeField from sigi.apps.casas.models import Orgao from sigi.apps.servidores.models import Servidor +class AtualizaCasaForm(forms.Form): + arquivo = forms.FileField( + required=True, + label=_(u"arquivo a importar"), + help_text=_(u"Envie um arquivo no formato CSV"), + ) class OrgaoForm(forms.ModelForm): # cnpj = BRCNPJField( diff --git a/sigi/apps/casas/templates/casas/importar.html b/sigi/apps/casas/templates/casas/importar.html new file mode 100644 index 0000000..065f1d4 --- /dev/null +++ b/sigi/apps/casas/templates/casas/importar.html @@ -0,0 +1,72 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} + +{% block content_title %} +

{% trans 'Importar dados para atualização de órgãos' %}

+{% endblock %} + +{% block content %} +{% if error %} + +{% endif %} +
+
+ {% csrf_token %} +
+ {{ form }} +
+ +
+
+

Padrões do arquivo:

+
    +
  • O arquivo deve ter o formato de texto, no padrão CSV
  • +
  • Deve-se usar pt-br.UTF-8 como codificação do arquivo
  • +
  • A primeira linha do arquivo teve conter o cabeçalho das colunas, conforme explicado a seguir
  • +
  • Cada linha subsequente deve referir-se a um e somente um órgão
  • +
  • Campos deixados em branco serão ignorados
  • +
+
+

Colunas do arquivo:

+
    +
  • O arquivo deve possuir no mínimo as colunas marcadas como obrigatórias. + A primeira linha deve conter os cabeçalhos, que devem ser grafados + EXATAMENTE como descrito na tabela abaixo (minúsculas, sem acentos, sem espaços).
  • +
  • É desejável que as colunas estejam na mesma ordem definida aqui.
  • +
  • Colunas adicionais podem estar presentes, e serão ignoradas
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ObrigatórioTítulo do campoConteúdo esperado
*tipoDeve conter CM para Câmara Municipal ou AL para Assembleia Legislativa
*municipioO nome do município. Não use abreviações!!!
*ufA sigla da Unidade da Federação (Estado) em letras maiúsculas (ex: MG, SP, PA)
orgao_enderecoO novo endereço do órgão.
orgao_bairroO novo bairro do órgão
orgao_cepO novo CEP do órgão
orgao_emailO novo e-mail institucional do órgão
orgao_portalO novo endereço do portal institucional do órgão
orgao_telefonesOs telefones do órgão. Pode conter quantos telefones forem necessários, separando-os por espaço. Use a formatação padrão de telefones, mas não use espaço entre os dígitos de um telefone, senão o sistema interpretará como dois telefones diferentes.
presidente_nomeO nome completo do presidente
presidente_data_nascimentoA data de nascimento do presidente no formato DD/MM/AAAA
presidente_telefonesOs telefones ou whatsapp do presidente. Pode conter quantos telefones forem necessários, separando-os por espaço. Use a formatação padrão de telefones, mas não use espaço entre os dígitos de um telefone, senão o sistema interpretará como dois telefones diferentes.
presidente_emailsOs e-mails do presidente. Pode conter quantos e-mails forem necessários. Utilize espaço para separar um e-mail do outro
presidente_enderecoO novo endereço do presidente
presidente_municipioO nome do município onde mora o presidente
presidente_bairroO nome do bairro onde mora o presidente
presidente_cepO novo CEP do presidente
presidente_redes_sociaisAs redes sociais do presidente. Pode conter quantas redes sociais forem necessárias. Utilize espaço para separar uma rede da outra.
+
+
+{% endblock %} + diff --git a/sigi/apps/casas/templates/casas/importar_result.html b/sigi/apps/casas/templates/casas/importar_result.html new file mode 100644 index 0000000..1b0cda5 --- /dev/null +++ b/sigi/apps/casas/templates/casas/importar_result.html @@ -0,0 +1,18 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} + +{% block content_title %} +

Importação de órgãos concluída com {% if com_erros %}erros{% else %}sucesso!{% endif %}

+{% endblock %} + +{% block content %} +
+

{{ total }} registros importados do arquivo {{ file_name }}

+ {% if com_erros %} +

{{ com_erros }} registros apresentaram erros

+

Download do arquivo de erros

+ {% endif %} +

Importar outro arquivo

+
+{% endblock %} + diff --git a/sigi/apps/casas/urls.py b/sigi/apps/casas/urls.py index df6bf9f..f94a467 100644 --- a/sigi/apps/casas/urls.py +++ b/sigi/apps/casas/urls.py @@ -1,5 +1,6 @@ # coding: utf-8 from django.conf.urls import patterns, url +from sigi.apps.casas.views import importa_casas urlpatterns = patterns( @@ -35,4 +36,7 @@ urlpatterns = patterns( url(r'^orgao/carrinho/deleta_itens_carrinho$', 'deleta_itens_carrinho', name='deleta-itens-carrinho'), # Error url(r'^portfolio/$', 'portfolio', name='casas-portfolio'), url(r'^carteira/$', 'painel_relacionamento', name='casas-carteira'), + + # Atualização por CSV + url(r'^orgao/importa/$', importa_casas.as_view(), name='importar-casas'), ) diff --git a/sigi/apps/casas/views.py b/sigi/apps/casas/views.py index 03a6243..0a1642d 100644 --- a/sigi/apps/casas/views.py +++ b/sigi/apps/casas/views.py @@ -1,26 +1,254 @@ # -*- coding: utf-8 -*- import csv +from datetime import datetime from functools import reduce from geraldo.generators import PDFGenerator +from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator, InvalidPage, EmptyPage from django.db.models import Count, Q -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden from django.shortcuts import render, get_object_or_404 from django.utils.translation import ugettext as _, ungettext +from django.views.generic import View -from sigi.apps.casas.forms import PortfolioForm -from sigi.apps.casas.models import Orgao, TipoOrgao +from sigi.apps.casas.forms import PortfolioForm, AtualizaCasaForm +from sigi.apps.casas.models import Orgao, TipoOrgao, Funcionario from sigi.apps.casas.reports import (CasasLegislativasLabels, CasasLegislativasLabelsSemPresidente) -from sigi.apps.contatos.models import UnidadeFederativa, Mesorregiao, Microrregiao +from sigi.apps.contatos.models import (UnidadeFederativa, Municipio, + Mesorregiao, Microrregiao) from sigi.apps.ocorrencias.models import Ocorrencia from sigi.apps.parlamentares.reports import ParlamentaresLabels from sigi.apps.servicos.models import TipoServico from sigi.apps.servidores.models import Servidor from sigi.shortcuts import render_to_pdf +class importa_casas(View): + errors = [] + total_registros = 0 + + TIPO = 'tipo' + MUNICIPIO = 'municipio' + UF = 'uf' + ORGAO_ENDERECO = 'orgao_endereco' + ORGAO_BAIRRO = 'orgao_bairro' + ORGAO_CEP = 'orgao_cep' + ORGAO_EMAIL = 'orgao_email' + ORGAO_PORTAL = 'orgao_portal' + ORGAO_TELEFONES = 'orgao_telefones' + PRESIDENTE_NOME = 'presidente_nome' + PRESIDENTE_DATA_NASCIMENTO = 'presidente_data_nascimento' + PRESIDENTE_TELEFONES = 'presidente_telefones' + PRESIDENTE_EMAILS = 'presidente_emails' + PRESIDENTE_ENDERECO = 'presidente_endereco' + PRESIDENTE_MUNICIPIO = 'presidente_municipio' + PRESIDENTE_BAIRRO = 'presidente_bairro' + PRESIDENTE_CEP = 'presidente_cep' + PRESIDENTE_REDES_SOCIAIS = 'presidente_redes_sociais' + ERROS = 'erros_importacao' + + fieldnames = [TIPO, MUNICIPIO, UF, ORGAO_ENDERECO, ORGAO_BAIRRO, ORGAO_CEP, + ORGAO_EMAIL, ORGAO_PORTAL, ORGAO_TELEFONES, PRESIDENTE_NOME, + PRESIDENTE_DATA_NASCIMENTO, PRESIDENTE_TELEFONES, + PRESIDENTE_EMAILS, PRESIDENTE_ENDERECO, PRESIDENTE_MUNICIPIO, + PRESIDENTE_BAIRRO, PRESIDENTE_CEP, PRESIDENTE_REDES_SOCIAIS, + ERROS,] + + ID_FIELDS = {TIPO, MUNICIPIO, UF} + + ORGAO_FIELDS = { + ORGAO_ENDERECO: 'logradouro', + ORGAO_BAIRRO: 'bairro', + ORGAO_CEP: 'cep', + ORGAO_EMAIL: 'email', + ORGAO_PORTAL: 'pagina_web', + ORGAO_TELEFONES: 'telefones', + } + + PRESIDENTE_FIELDS = { + PRESIDENTE_NOME: 'nome', + PRESIDENTE_DATA_NASCIMENTO: 'data_nascimento', + PRESIDENTE_TELEFONES: 'nota', + PRESIDENTE_EMAILS: 'email', + PRESIDENTE_ENDERECO: 'endereco', + PRESIDENTE_MUNICIPIO: 'municipio_id', + PRESIDENTE_BAIRRO: 'bairro', + PRESIDENTE_CEP: 'cep', + PRESIDENTE_REDES_SOCIAIS: 'redes_sociais', + } + + def get(self, request): + form = AtualizaCasaForm() + return render(request, 'casas/importar.html', {'form': form}) + + def post(self, request): + form = AtualizaCasaForm(request.POST, request.FILES) + + if form.is_valid(): + file = form.cleaned_data['arquivo'] + reader = csv.DictReader(file) + if not self.ID_FIELDS.issubset(reader.fieldnames): + return render( + request, + 'casas/importar.html', + {'form': form, 'error': _(u"O arquivo não possui algum dos " + u"campos obrigatórios")} + ) + + if self.importa(reader): + # Importação concluída com êxito + return render( + request, + 'casas/importar_result.html', + {'file_name': file.name, 'total': self.total_registros, + 'com_erros': 0} + ) + else: + # Importado com erros + file_name = "casas-erros-{:%Y-%m-%d-%H%M}.csv".format( + datetime.now()) + fields = self.fieldnames + for f in reader.fieldnames: + if f not in fields: + fields.append(f) + with open(settings.MEDIA_ROOT+'/temp/'+file_name, "w+") as f: + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(self.errors) + return render( + request, + 'casas/importar_result.html', + {'file_name': file.name, 'result_file': file_name, + 'total': self.total_registros, + 'com_erros': len(self.errors)} + ) + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = ( + 'attachment; filename="somefilename.csv"') + return response + else: + return render( + request, + 'casas/importar.html', + {'form': form, 'error': u"Erro no preenchimento do formulário."} + ) + + def importa(self, reader): + self.errors = [] + self.total_registros = 0 + + for reg in reader: + self.total_registros += 1 + reg[self.ERROS] = [] + orgao = Orgao.objects.filter( + tipo__sigla=reg[self.TIPO], + municipio__nome=reg[self.MUNICIPIO], + municipio__uf__sigla=reg[self.UF] + ) + if orgao.count() == 0: + reg[self.ERROS].append("Nao existe orgao com esta identificacao") + self.errors.append(reg) + continue + elif orgao.count() > 1: + reg[self.ERROS].append("Existem {count} orgaos com esta mesma " + "identificacao").format(count=orgao.count()) + self.errors.append(reg) + continue + else: + orgao = orgao.get() + + # Atualiza os dados do órgão + for key in self.ORGAO_FIELDS: + field_name = self.ORGAO_FIELDS[key] + if key in reg: + value = reg[key].strip() + if key == self.ORGAO_TELEFONES: + for numero in value.split(" "): + try: + orgao.telefones.update_or_create(numero=numero) + except: + reg[self.ERROS].append( + 'Telefone {numero} não foi ' + 'atualizado'.format(numero=numero) + ) + elif value != "" and value != getattr(orgao, field_name): + setattr(orgao, field_name, value) + try: + orgao.save() + except Exception as e: + reg[self.ERROS].append( + "Erro salvando o orgao: '{message}'".format( + message=e.message) + ) + + # Atualiza o presidente + presidente = orgao.presidente + + if presidente is None: + presidente = Funcionario( + casa_legislativa=orgao, + setor="presidente" + ) + + for key in self.PRESIDENTE_FIELDS: + field_name = self.PRESIDENTE_FIELDS[key] + if key in reg: + value = reg[key].strip() + else: + value = "" + + if value != "": + if key == self.PRESIDENTE_MUNICIPIO: + if ',' in value: + municipio, uf = value.split(',') + else: + municipio = value + uf = reg[self.UF] + + try: + value = Municipio.objects.get( + nome__iexact=municipio.strip(), + uf__sigla=uf.strip()).pk + except: + value = None + reg[self.ERROS].append( + "Impossivel identificar o Municipio de " + "residencia do Presidente" + ) + continue + if key == self.PRESIDENTE_REDES_SOCIAIS: + value = value.replace(" ", "\r") + if key == self.PRESIDENTE_DATA_NASCIMENTO: + sd = value.split('/') + if len(sd) < 3: + reg[self.ERROS].append( + "Data de nascimento do presidente esta em um " + "formato nao reconhecido. Use DD/MM/AAAA" + ) + continue + else: + value = "{ano}-{mes}-{dia}".format( + ano=sd[2], + mes=sd[1], + dia=sd[0] + ) + if value != getattr(presidente, field_name): + setattr(presidente, field_name, value) + try: + presidente.save() + except Exception as e: + reg[self.ERROS].append( + "Erro salvando presidente: '{message}'".format( + message=e.message) + ) + + if len(reg[self.ERROS]) > 0: + self.errors.append(reg) + + return len(self.errors) == 0 # @param qs: queryset # @param o: (int) number of order field diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html index 179db60..6f61a15 100644 --- a/templates/admin/base_site.html +++ b/templates/admin/base_site.html @@ -40,6 +40,7 @@
  • {% trans 'Usuários' %} & {% trans 'Grupos' %}
  • {% trans 'Sites' %}
  • {% trans 'Diagnósticos' %}
  • +
  • {% trans 'Importar dados de Casas' %}
  • {% endif %}