From 2ad74d7e501ae88d59ebd56a87597028d885a029 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Fri, 23 Feb 2018 15:10:21 -0300 Subject: [PATCH 1/5] =?UTF-8?q?Adiciona=20valida=C3=A7=C3=A3o=20de=20Regis?= =?UTF-8?q?troVotacao?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/sessao/models.py | 14 ++++++++++++++ sapl/sessao/tests/test_sessao.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index db7c50d9b..f4f25cc14 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -1,7 +1,11 @@ +from operator import xor + import reversion +from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils import Choices + from sapl.base.models import Autor from sapl.materia.models import MateriaLegislativa from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar, @@ -429,6 +433,16 @@ class RegistroVotacao(models.Model): 'votacao': self.tipo_resultado_votacao, 'materia': self.materia} + def clean(self): + """Exatamente um dos campos ordem ou expediente deve estar preenchido. + """ + # TODO remover esse método quando OrdemDia e ExpedienteMateria + # forem reestruturados e os campos ordem e expediente forem unificados + if not xor(bool(self.ordem), bool(self.expediente)): + raise ValidationError( + 'RegistroVotacao deve ter exatamente um dos campos ' + 'ordem ou expediente deve estar preenchido') + @reversion.register() class VotoParlamentar(models.Model): # RegistroVotacaoParlamentar diff --git a/sapl/sessao/tests/test_sessao.py b/sapl/sessao/tests/test_sessao.py index 37a17d1ed..d83c56e86 100644 --- a/sapl/sessao/tests/test_sessao.py +++ b/sapl/sessao/tests/test_sessao.py @@ -1,11 +1,13 @@ import pytest +from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from model_mommy import mommy + from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.parlamentares.models import Legislatura, Partido, SessaoLegislativa from sapl.sessao import forms -from sapl.sessao.models import (ExpedienteMateria, SessaoPlenaria, - TipoSessaoPlenaria) +from sapl.sessao.models import (ExpedienteMateria, OrdemDia, RegistroVotacao, + SessaoPlenaria, TipoSessaoPlenaria) def test_valida_campos_obrigatorios_sessao_plenaria_form(): @@ -138,3 +140,25 @@ def test_expediente_materia_form_valido(): }, instance=instance) assert form.is_valid() + + +@pytest.mark.django_db(transaction=False) +def test_registro_votacao_tem_ordem_xor_expediente(): + + def registro_votacao_com(ordem, expediente): + return mommy.make(RegistroVotacao, ordem=ordem, expediente=expediente) + + ordem = mommy.make(OrdemDia) + expediente = mommy.make(ExpedienteMateria) + + # a validação funciona com exatamente um dos campos preenchido + registro_votacao_com(ordem, None).full_clean() + registro_votacao_com(None, expediente).full_clean() + + # a validação NÃO funciona quando nenhum deles é preenchido + with pytest.raises(ValidationError): + registro_votacao_com(None, None).full_clean() + + # a validação NÃO funciona quando ambos são preenchidos + with pytest.raises(ValidationError): + registro_votacao_com(ordem, expediente).full_clean() From 8e1af5765f2888a42fefe242dbcd486193a38c29 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Mon, 26 Feb 2018 12:31:33 -0300 Subject: [PATCH 2/5] =?UTF-8?q?Revisa=20migra=C3=A7=C3=A3o=20de=20Registro?= =?UTF-8?q?Votacao?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Não exclui registro de votação duplicados. --- sapl/legacy/migration.py | 126 ++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 74 deletions(-) diff --git a/sapl/legacy/migration.py b/sapl/legacy/migration.py index e5d98147b..1ab22da4c 100644 --- a/sapl/legacy/migration.py +++ b/sapl/legacy/migration.py @@ -437,10 +437,10 @@ relatoria | tip_fim_relatoria = NULL | tip_fim_relatoria = 0 anula_tipos_origem_externa_invalidos() -def iter_sql_records(sql, db): +def iter_sql_records(sql): class Record: pass - cursor = exec_sql(sql, db) + cursor = exec_legado(sql) fieldnames = [name[0] for name in cursor.description] for row in cursor.fetchall(): record = Record() @@ -534,24 +534,6 @@ def excluir_registrovotacao_duplicados(): assert 0 -def delete_old(legacy_model, cols_values): - # ajuste necessário por conta de cósigos html em txt_expediente - if legacy_model.__name__ == 'ExpedienteSessaoPlenaria': - cols_values.pop('txt_expediente') - - def eq_clause(col, value): - if value is None: - return '{} IS NULL'.format(col) - else: - return '{}="{}"'.format(col, value) - - delete_sql = 'delete from {} where {}'.format( - legacy_model._meta.db_table, - ' and '.join([eq_clause(col, value) - for col, value in cols_values.items()])) - exec_sql(delete_sql, 'legacy') - - def get_last_pk(model): last_value = model.objects.all().aggregate(Max('pk')) return last_value['pk__max'] or 0 @@ -563,6 +545,12 @@ def reinicia_sequence(model, id): sequence_name, id)) +def get_pk_legado(tabela): + res = exec_legado( + 'show index from {} WHERE Key_name = "PRIMARY"'.format(tabela)) + return [r[4] for r in res] + + class DataMigrator: def __init__(self): @@ -648,8 +636,8 @@ class DataMigrator: info('Começando migração: %s...' % obj) self._do_migrate(obj) - info('Excluindo possíveis duplicações em RegistroVotacao...') - excluir_registrovotacao_duplicados() + # info('Excluindo possíveis duplicações em RegistroVotacao...') + # excluir_registrovotacao_duplicados() # recria tipos de autor padrão que não foram criados pela migração cria_models_tipo_autor() @@ -677,45 +665,29 @@ class DataMigrator: def migrate_model(self, model): print('Migrando %s...' % model.__name__) - legacy_model_name = self.model_renames.get(model, model.__name__) - legacy_model = legacy_app.get_model(legacy_model_name) - legacy_pk_name = legacy_model._meta.pk.name - - # setup migration strategy for tables with or without a pk - if legacy_pk_name == 'id': - deve_ajustar_sequence_ao_final = False - # There is no pk in the legacy table + nome_model = self.model_renames.get(model, model.__name__) + model_legado = legacy_app.get_model(nome_model) + tabela_legado = model_legado._meta.db_table + campos_pk = get_pk_legado(tabela_legado) - def save(new, old): - with reversion.create_revision(): - new.save() - reversion.set_comment('Objeto criado pela migração') + if len(campos_pk) == 1: + # a pk no legado tem um único campo + nome_pk = model_legado._meta.pk.name + old_records = model_legado.objects.all().order_by(nome_pk) - # apaga registro do legado - delete_old(legacy_model, old.__dict__) - - old_records = iter_sql_records( - 'select * from ' + legacy_model._meta.db_table, 'legacy') + def get_id_do_legado(old): + return getattr(old, nome_pk) else: - deve_ajustar_sequence_ao_final = True - - def save(new, old): - with reversion.create_revision(): - # salva new com id de old - new.id = getattr(old, legacy_pk_name) - new.save() - reversion.set_comment('Objeto criado pela migração') - - # apaga registro do legado - delete_old(legacy_model, {legacy_pk_name: new.id}) - - old_records = legacy_model.objects.all().order_by(legacy_pk_name) + # a pk no legado tem mais de um campo + old_records = iter_sql_records('select * from ' + tabela_legado) + get_id_do_legado = None ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model) ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model) # convert old records to new ones with transaction.atomic(): + sql_delete_legado = '' for old in old_records: if getattr(old, 'ind_excluido', False): # não migramos registros marcados como excluídos @@ -731,15 +703,35 @@ class DataMigrator: # então este é um objeo órfão: simplesmente ignoramos continue else: - save(new, old) + if get_id_do_legado: + new.id = get_id_do_legado(old) + # validação do model + new.clean() + # salva novo registro + with reversion.create_revision(): + new.save() + reversion.set_comment('Objeto criado pela migração') + + # acumula deleção do registro no legado + sql_delete_legado += 'delete from {} where {};\n'.format( + tabela_legado, + ' and '.join( + '{} = "{}"'.format(campo, + getattr(old, campo)) + for campo in campos_pk)) + if ajuste_depois_salvar: ajuste_depois_salvar(new, old) - # reinicia sequence - if deve_ajustar_sequence_ao_final: + # se configuramos ids explicitamente devemos reiniciar a sequence + if get_id_do_legado: last_pk = get_last_pk(model) reinicia_sequence(model, last_pk + 1) + # apaga registros migrados do legado + if sql_delete_legado: + exec_legado(sql_delete_legado) + def migrate(obj=appconfs, interativo=True): dm = DataMigrator() @@ -797,7 +789,7 @@ def adjust_ordemdia_antes_salvar(new, old): def adjust_ordemdia_depois_salvar(new, old): if old.num_ordem is None and new.numero_ordem == 999999999: with reversion.create_revision(): - problema = 'OrdemDia de PK %s tinha seu valor de numero ordem'\ + problema = 'OrdemDia de PK %s tinha seu valor de numero ordem' \ ' nulo.' % old.pk descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem warn(problema + ' => ' + descricao) @@ -882,19 +874,6 @@ def adjust_registrovotacao_antes_salvar(new, old): new.expediente = expediente_materia[0] -def adjust_registrovotacao_depois_salvar(new, old): - if not new.ordem and not new.expediente: - with reversion.create_revision(): - problema = 'RegistroVotacao de PK %s não possui nenhuma OrdemDia'\ - ' ou ExpedienteMateria.' % old.pk - descricao = 'RevistroVotacao deve ter no mínimo uma ordem do dia'\ - ' ou expediente vinculado.' - warn(problema + ' => ' + descricao) - save_relation(obj=new, problema=problema, - descricao=descricao, eh_stub=False) - reversion.set_comment('RegistroVotacao sem ordem ou expediente') - - def adjust_tipoafastamento(new, old): if old.ind_afastamento == 1: new.indicador = 'A' @@ -1010,8 +989,8 @@ def adjust_autor(new, old): def adjust_comissao(new, old): if not old.dat_extincao and not old.dat_fim_comissao: new.ativa = True - elif old.dat_extincao and date.today() < new.data_extincao or \ - old.dat_fim_comissao and date.today() < new.data_fim_comissao: + elif (old.dat_extincao and date.today() < new.data_extincao or + old.dat_fim_comissao and date.today() < new.data_fim_comissao): new.ativa = True else: new.ativa = False @@ -1043,15 +1022,14 @@ AJUSTE_DEPOIS_SALVAR = { NormaJuridica: adjust_normajuridica_depois_salvar, OrdemDia: adjust_ordemdia_depois_salvar, Protocolo: adjust_protocolo_depois_salvar, - RegistroVotacao: adjust_registrovotacao_depois_salvar, } # CHECKS #################################################################### def get_ind_excluido(new): - legacy_model = legacy_app.get_model(type(new).__name__) - old = legacy_model.objects.get(**{legacy_model._meta.pk.name: new.id}) + model_legado = legacy_app.get_model(type(new).__name__) + old = model_legado.objects.get(**{model_legado._meta.pk.name: new.id}) return getattr(old, 'ind_excluido', False) From 65354f0e20d9193d6fad117fc7a11a669e03e469 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Mon, 26 Feb 2018 17:01:06 -0300 Subject: [PATCH 3/5] =?UTF-8?q?Adicionado=20mimetype=20do=20excel=20=C3=A0?= =?UTF-8?q?=20exporta=C3=A7=C3=A3o=20de=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/scripts/exporta_zope/exporta_zope.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sapl/legacy/scripts/exporta_zope/exporta_zope.py b/sapl/legacy/scripts/exporta_zope/exporta_zope.py index e31aa4d81..c097c1a1a 100755 --- a/sapl/legacy/scripts/exporta_zope/exporta_zope.py +++ b/sapl/legacy/scripts/exporta_zope/exporta_zope.py @@ -22,6 +22,7 @@ EXTENSOES = { 'application/msword': '.doc', 'application/pdf': '.pdf', 'application/vnd.oasis.opendocument.text': '.odt', + 'application/vnd.ms-excel': '.xls', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', # noqa 'application/xml': '.xml', 'text/xml': '.xml', @@ -219,6 +220,7 @@ DUMP_FUNCTIONS = { 'Folder': partial(dump_folder, enum=enumerate_folder), 'BTreeFolder2': partial(dump_folder, enum=enumerate_btree), 'SDE-Document': partial(dump_sde, tipo='sde.document'), + 'StrDoc': partial(dump_sde, tipo='sde.document'), 'SDE-Template': partial(dump_sde, tipo='sde.template'), # explicitamente ignorados From 2fb499fedf6e47e7af21b913f2861ccc6858f5f7 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Tue, 27 Feb 2018 07:16:35 -0300 Subject: [PATCH 4/5] Migra documento adm mesmo sem protocolo --- sapl/legacy/migracao_usuarios.py | 1 + sapl/legacy/migration.py | 36 +++++++++++++++++++------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/sapl/legacy/migracao_usuarios.py b/sapl/legacy/migracao_usuarios.py index bd7d413e5..591f08b20 100644 --- a/sapl/legacy/migracao_usuarios.py +++ b/sapl/legacy/migracao_usuarios.py @@ -92,3 +92,4 @@ def migra_usuarios(): usuario.groups.add(PERFIL_LEGADO_PARA_NOVO[perfil]) # apaga arquivo (importante pois contém senhas) ARQUIVO_USUARIOS.remove() + print('Usuários migrados com sucesso.') diff --git a/sapl/legacy/migration.py b/sapl/legacy/migration.py index 1ab22da4c..b67501ab9 100644 --- a/sapl/legacy/migration.py +++ b/sapl/legacy/migration.py @@ -121,7 +121,9 @@ def warn(msg): class ForeignKeyFaltando(ObjectDoesNotExist): 'Uma FK aponta para um registro inexistente' - pass + + def __init__(self, msg=''): + self.msg = msg @lru_cache() @@ -697,10 +699,11 @@ class DataMigrator: self.populate_renamed_fields(new, old) if ajuste_antes_salvar: ajuste_antes_salvar(new, old) - except ForeignKeyFaltando: + except ForeignKeyFaltando as e: # tentamos preencher uma FK e o ojeto relacionado # não existe # então este é um objeo órfão: simplesmente ignoramos + warn(e.msg) continue else: if get_id_do_legado: @@ -749,19 +752,21 @@ def adjust_documentoadministrativo(new, old): protocolo = Protocolo.objects.filter( numero=old.num_protocolo, ano=new.ano) if not protocolo: + # tentamos encontrar o protocolo no ano seguinte protocolo = Protocolo.objects.filter( numero=old.num_protocolo, ano=new.ano + 1) - print('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! ' - 'DocumentoAdministrativo: {}, numero_protocolo: {}, ' - 'ano doc adm: {}'.format( - old.cod_documento, old.num_protocolo, new.ano)) - if not protocolo: - raise ForeignKeyFaltando( - 'Protocolo {} faltando ' - '(referenciado no documento administrativo {}'.format( - old.num_protocolo, old.cod_documento)) - assert len(protocolo) == 1 - new.protocolo = protocolo[0] + if protocolo: + print('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! ' + 'DocumentoAdministrativo: {}, numero_protocolo: {}, ' + 'ano doc adm: {}'.format( + old.cod_documento, old.num_protocolo, new.ano)) + else: + warn('Protocolo {} faltando ' + '(referenciado no documento administrativo {}'.format( + old.num_protocolo, old.cod_documento)) + if protocolo: + assert len(protocolo) == 1, 'mais de um protocolo encontrado' + [new.protocolo] = protocolo def adjust_mandato(new, old): @@ -954,8 +959,9 @@ def vincula_autor(new, old, model_relacionado, campo_relacionado, campo_nome): new.autor_related = model_relacionado.objects.get(pk=pk_rel) except ObjectDoesNotExist: # ignoramos o autor órfão - raise ForeignKeyFaltando('{} inexiste para autor'.format( - model_relacionado._meta.verbose_name)) + raise ForeignKeyFaltando( + '{} [pk={}] inexistente para autor'.format( + model_relacionado._meta.verbose_name, pk_rel)) else: new.nome = getattr(new.autor_related, campo_nome) return True From 2310316488e4d781326dbc35103c3fc28f43cd8d Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Tue, 27 Feb 2018 10:18:55 -0300 Subject: [PATCH 5/5] =?UTF-8?q?Checa=20registros=20de=20vota=C3=A7=C3=A3o?= =?UTF-8?q?=20na=20pr=C3=A9-migra=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migration.py | 42 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/sapl/legacy/migration.py b/sapl/legacy/migration.py index b67501ab9..abd232d47 100644 --- a/sapl/legacy/migration.py +++ b/sapl/legacy/migration.py @@ -352,9 +352,47 @@ def anula_tipos_origem_externa_invalidos(): where tip_origem_externa not in {};''', tipos_validos) +def get_ids_registros_votacao_para(tabela): + sql = ''' + select r.cod_votacao from {} o + inner join registro_votacao r on + o.cod_ordem = r.cod_ordem and o.cod_materia = r.cod_materia + where o.ind_excluido != 1 and r.ind_excluido != 1 + order by o.cod_sessao_plen, num_ordem + '''.format(tabela) + return set(primeira_coluna(exec_legado(sql))) + + +def checa_registros_votacao_ambiguos_e_remove_nao_usados(): + """Interrompe a migração caso restem registros de votação + que apontam para uma ordem_dia e um expediente_materia ao mesmo tempo. + + Remove do legado registros de votação que não têm + nem ordem_dia nem expediente_materia associados.""" + + ordem, expediente = [ + get_ids_registros_votacao_para(tabela) + for tabela in ('ordem_dia', 'expediente_materia')] + + # interrompe migração se houver registros ambíguos + ambiguos = ordem.intersection(expediente) + assert not ambiguos, '''Existe(m) RegistroVotacao ambíguo(s): {} + Corrija os dados originais antes de migrar!'''.format( + ambiguos) + + # exclui registros não usados (zumbis) + todos = set(primeira_coluna(exec_legado( + 'select cod_votacao from registro_votacao'))) + nao_usados = todos - ordem.union(expediente) + exec_legado_em_subconjunto(''' + update registro_votacao set ind_excluido = 1 + where cod_votacao in {}''', nao_usados) + + def uniformiza_banco(): - # desliga todas as checagens do mysql - exec_legado('SET SESSION sql_mode = "";') + exec_legado('SET SESSION sql_mode = "";') # desliga checagens do mysql + + checa_registros_votacao_ambiguos_e_remove_nao_usados() garante_coluna_no_legado('proposicao', 'num_proposicao int(11) NULL')