diff --git a/sapl/legacy/migration.py b/sapl/legacy/migration.py index 785cb80b9..136517e31 100644 --- a/sapl/legacy/migration.py +++ b/sapl/legacy/migration.py @@ -1,12 +1,12 @@ import re from datetime import date from functools import lru_cache, partial +from itertools import groupby from subprocess import PIPE, call import pkg_resources -import yaml - import reversion +import yaml from django.apps import apps from django.apps.config import AppConfig from django.contrib.auth import get_user_model @@ -16,6 +16,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import connections, transaction from django.db.models import Count, Max from django.db.models.base import ModelBase + from sapl.base.models import AppConfig as AppConf from sapl.base.models import (Autor, CasaLegislativa, ProblemaMigracao, TipoAutor) @@ -144,28 +145,32 @@ def exec_sql(sql, db='default'): cursor.execute(sql) return cursor -# UNIFORMIZAÇÃO DO BANCO ANTES DA MIGRAÇÃO ############################### +exec_legado = partial(exec_sql, db='legacy') + + +def primeira_coluna(cursor): + return (r[0] for r in cursor) +# UNIFORMIZAÇÃO DO BANCO ANTES DA MIGRAÇÃO ############################### + SQL_NAO_TEM_TABELA = ''' SELECT count(*) FROM information_schema.columns WHERE table_schema=database() AND TABLE_NAME="{}" ''' -SQL_NAO_TEM_COLUNA = SQL_NAO_TEM_TABELA + ' AND COLUMN_NAME="{}"' - -exec_legado = partial(exec_sql, db='legacy') def existe_tabela_no_legado(tabela): sql = SQL_NAO_TEM_TABELA.format(tabela) - return exec_legado(sql).fetchone()[0] + return primeira_coluna(exec_legado(sql))[0] def existe_coluna_no_legado(tabela, coluna): - sql = SQL_NAO_TEM_COLUNA.format(tabela, coluna) - return exec_legado(sql).fetchone()[0] > 0 + sql_nao_tem_coluna = SQL_NAO_TEM_TABELA + ' AND COLUMN_NAME="{}"' + sql = sql_nao_tem_coluna.format(tabela, coluna) + return primeira_coluna(exec_legado(sql))[0] > 0 def garante_coluna_no_legado(tabela, spec_coluna): @@ -182,223 +187,120 @@ def garante_tabela_no_legado(create_table): assert existe_tabela_no_legado(tabela) -def migra_autor(): - - SQL_ENUMERA_REPETIDOS = ''' - select cod_parlamentar, COUNT(*) - from autor where cod_parlamentar is not null - group by cod_parlamentar - having 1 < COUNT(*) - order by cod_parlamentar asc; - ''' - - SQL_INFOS_AUTOR = ''' - select cod_autor from autor - where cod_parlamentar = {} - group by cod_autor - order by col_username, des_cargo desc; - ''' - - SQL_UPDATE_AUTOR = "update autoria set cod_autor = {} where cod_autor in ({});" - - SQL_ENUMERA_AUTORIA_REPETIDOS = ''' - select cod_materia, COUNT(*) from autoria where cod_autor in ({}) - group by cod_materia - having 1 < COUNT(*); - ''' - - SQL_DELETE_AUTORIA = ''' - delete from autoria where cod_materia in ({}) and cod_autor in ({}); - ''' - - SQL_UPDATE_DOCUMENTO_ADMINISTRATIVO = ''' - update documento_administrativo - set cod_autor = {} - where cod_autor in ({}); - ''' - - SQL_UPDATE_PROPOSICAO = ''' - update proposicao - set cod_autor = {} - where cod_autor in ({}); - ''' - - SQL_UPDATE_PROTOCOLO = ''' - update protocolo - set cod_autor = {} - where cod_autor in ({}); - ''' - - SQL_DELETE_AUTOR = ''' - delete from autor where cod_autor in ({}) - and cod_autor not in ({}); - ''' - - cursor = exec_legado('update autor set ind_excluido = 0 where cod_autor is not null;') - cursor = exec_legado(SQL_ENUMERA_REPETIDOS) - - autores_parlamentares = [r[0] for r in cursor if r[0]] - - for cod_autor in autores_parlamentares: - - sql = SQL_INFOS_AUTOR.format(cod_autor) - - cursor = exec_legado(sql) - autores = [] - - for response in cursor: - autores.append(response) - - ids = [a[0] for a in autores] - id_ativo, ids_inativos = ids[-1], ids[:-1] - ids = str(ids).strip('[]') - id_ativo = str(id_ativo).strip('[]') - ids_inativos = str(ids_inativos).strip('[]') - - tabelas = ['autoria', 'documento_administrativo', - 'proposicao', 'protocolo'] - for tabela in tabelas: - if tabela == 'autoria' and id_ativo and ids_inativos: - # Para update e delete no MySQL -> SET SQL_SAFE_UPDATES = 0; - sql = SQL_ENUMERA_AUTORIA_REPETIDOS.format(ids) - cursor = exec_legado(sql) - - materias = [] - for response in cursor: - materias.append(response[0]) - - materias = str(materias).strip('[]') - if materias: - sql = SQL_DELETE_AUTORIA.format(materias, ids_inativos) - exec_legado(sql) - - sql = SQL_UPDATE_AUTOR.format(id_ativo, ids_inativos) - exec_legado(sql) - - elif tabela == 'documento_administrativo' and id_ativo and ids_inativos: - sql = SQL_UPDATE_DOCUMENTO_ADMINISTRATIVO.format(id_ativo, ids_inativos) - exec_legado(sql) - - elif tabela == 'proposicao' and id_ativo and ids_inativos: - sql = SQL_UPDATE_PROPOSICAO.format(id_ativo, ids_inativos) - exec_legado(sql) - - elif tabela == 'protocolo' and id_ativo and ids_inativos: - sql = SQL_UPDATE_PROTOCOLO.format(id_ativo, ids_inativos) - exec_legado(sql) - - # Faz a exclusão dos autores que não serão migrados - sql = SQL_DELETE_AUTOR.format(ids, id_ativo) - cursor = exec_legado(sql) - - -def migra_comissao(): - SQL_ENUMERA_REPETIDOS = ''' - select cod_comissao, COUNT(*) - from autor where cod_comissao is not null - group by cod_comissao - having 1 < COUNT(*) - order by cod_comissao asc; - ''' - - SQL_INFOS_COMISSAO = ''' - select cod_autor from autor - where cod_comissao = {} - group by cod_autor; - ''' - - SQL_UPDATE_AUTOR = "update autoria set cod_autor = {} where cod_autor in ({});" - - SQL_ENUMERA_AUTORIA_REPETIDOS = ''' - select cod_materia, COUNT(*) from autoria where cod_autor in ({}) - group by cod_materia - having 1 < COUNT(*); - ''' - - SQL_DELETE_AUTORIA = ''' - delete from autoria where cod_materia in ({}) and cod_autor in ({}); - ''' - - SQL_UPDATE_DOCUMENTO_ADMINISTRATIVO = ''' - update documento_administrativo - set cod_autor = {} - where cod_autor in ({}); - ''' - - SQL_UPDATE_PROPOSICAO = ''' - update proposicao - set cod_autor = {} - where cod_autor in ({}); - ''' - - SQL_UPDATE_PROTOCOLO = ''' - update protocolo - set cod_autor = {} - where cod_autor in ({}); - ''' - - SQL_DELETE_AUTOR = ''' - delete from autor where cod_autor in ({}) - and cod_autor not in ({}); - ''' - - cursor = exec_legado('update autor set ind_excluido = 0 where cod_comissao is not null;') - cursor = exec_legado(SQL_ENUMERA_REPETIDOS) - - comissoes_parlamentares = [r[0] for r in cursor if r[0]] - - for cod_comissao in comissoes_parlamentares: - - sql = SQL_INFOS_COMISSAO.format(cod_comissao) - cursor = exec_legado(sql) - - comissoes = [] - - for response in cursor: - comissoes.append(response) - - ids = [c[0] for c in comissoes] - id_ativo, ids_inativos = ids[-1], ids[:-1] - ids = str(ids).strip('[]') - id_ativo = str(id_ativo).strip('[]') - ids_inativos = str(ids_inativos).strip('[]') - - tabelas = ['autoria', 'documento_administrativo', - 'proposicao', 'protocolo'] - - for tabela in tabelas: - if tabela == 'autoria' and id_ativo and ids_inativos: - # Para update e delete no MySQL -> SET SQL_SAFE_UPDATES = 0; - sql = SQL_ENUMERA_AUTORIA_REPETIDOS.format(ids) - cursor = exec_legado(sql) - - materias = [] - for response in cursor: - materias.append(response[0]) - - materias = str(materias).strip('[]') - if materias: - sql = SQL_DELETE_AUTORIA.format(materias, ids_inativos) - exec_legado(sql) - - sql = SQL_UPDATE_AUTOR.format(id_ativo, ids_inativos) - exec_legado(sql) - - elif tabela == 'documento_administrativo' and id_ativo and ids_inativos: - sql = SQL_UPDATE_DOCUMENTO_ADMINISTRATIVO.format(id_ativo, ids_inativos) - exec_legado(sql) - - elif tabela == 'proposicao' and id_ativo and ids_inativos: - sql = SQL_UPDATE_PROPOSICAO.format(id_ativo, ids_inativos) - exec_legado(sql) +TABELAS_REFERENCIANDO_AUTOR = [ + # , + ('autoria', True), + ('documento_administrativo', True), + ('proposicao', True), + ('protocolo', False)] + + +def reverte_exclusao_de_autores_referenciados_no_legado(): + + def get_autores_referenciados(tabela, tem_ind_excluido): + sql = '''select distinct cod_autor from {} + where cod_autor is not null + '''.format(tabela) + if tem_ind_excluido: + sql += ' and ind_excluido != 1' + return primeira_coluna(exec_legado(sql)) + + # reverte exclusões de autores referenciados por outras tabelas + autores_referenciados = { + cod + for tabela, tem_ind_excluido in TABELAS_REFERENCIANDO_AUTOR + for cod in get_autores_referenciados(tabela, tem_ind_excluido)} + exec_legado( + 'update autor set ind_excluido = 0 where cod_autor in {}'.format( + tuple(autores_referenciados) + )) + + +def get_reapontamento_de_autores_repetidos(autores): + """ Dada uma lista ordenada de pares (cod_zzz, cod_autor) retorna: + + * a lista de grupos de cod_autor'es repetidos + (quando há mais de um cod_autor para um mesmo cod_zzz) + + * a lista de cod_autor'es a serem apagados (todos além do 1o de cada grupo) + """ + grupos_de_repetidos = [ + [cod_autor for _, cod_autor in grupo] + for cod_zzz, grupo in groupby(autores, lambda r: r[0])] + # mantém apenas os grupos com mais de um autor por cod_zzz + grupos_de_repetidos = [g for g in grupos_de_repetidos if len(g) > 1] + # aponta cada autor de cada grupo de repetidos para o 1o do seu grupo + reapontamento = {autor: grupo[0] + for grupo in grupos_de_repetidos + for autor in grupo} + # apagaremos todos menos o primeiro + apagar = [k for k, v in reapontamento.items() if k != v] + return reapontamento, apagar + + +def get_autorias_sem_repeticoes(autoria, reapontamento): + "Autorias sem repetições de autores e com ind_primeiro_autor ajustado" + + # substitui cada autor repetido pelo 1o de seu grupo + autoria = sorted((reapontamento[a], m, i) for a, m, i in autoria) + # agrupa por [autor (1o do grupo de repetidos), materia], com + # ind_primeiro_autor == 1 se isso acontece em qualquer autor do grupo + autoria = [(a, m, max(i for a, m, i in grupo)) + for (a, m), grupo in groupby(autoria, lambda x: x[:2])] + return autoria + + +def unifica_autores_repetidos_no_legado(campo_agregador): + "Reúne autores repetidos em um único, antes da migracão" + + # enumeramos a repeticoes segundo o campo relevante + # (p. ex. cod_parlamentar ou cod_comissao) + # a ordenação prioriza, as entradas: + # - não excluidas, + # - em seguida as que têm col_username, + # - em seguida as que têm des_cargo + autores = exec_legado(''' + select {cod_parlamentar}, cod_autor from autor + where {cod_parlamentar} is not null + order by {cod_parlamentar}, + ind_excluido, col_username desc, des_cargo desc'''.format( + cod_parlamentar=campo_agregador)) + + reapontamento, apagar = get_reapontamento_de_autores_repetidos(autores) + + # Reaponta AUTORIA (many-to-many) + + # simplificamos retirando inicialmente as autorias excluidas + exec_legado('delete from autoria where ind_excluido = 1') + + # selecionamos as autorias envolvidas em repetições de autores + from_autoria = ' from autoria where cod_autor in {}'.format( + tuple(reapontamento)) + autoria = exec_legado( + 'select cod_autor, cod_materia, ind_primeiro_autor' + from_autoria) + + # apagamos todas as autorias envolvidas + exec_legado('delete ' + from_autoria) + # e depois inserimos apenas as sem repetições c ind_primeiro_autor ajustado + nova_autoria = get_autorias_sem_repeticoes(autoria, reapontamento) + exec_legado(''' + insert into autoria + (cod_autor, cod_materia, ind_primeiro_autor, ind_excluido) + values {}'''.format(', '.join([str((a, m, i, 0)) + for a, m, i in nova_autoria]))) - elif tabela == 'protocolo' and id_ativo and ids_inativos: - sql = SQL_UPDATE_PROTOCOLO.format(id_ativo, ids_inativos) - exec_legado(sql) + # Reaponta outras tabelas que referenciam autor + for tabela, _ in TABELAS_REFERENCIANDO_AUTOR: + for antigo, novo in reapontamento.items(): + if antigo != novo: + exec_legado(''' + update {} set cod_autor = {} where cod_autor = {} + '''.format(tabela, novo, antigo)) - # Faz a exclusão dos autores que não serão migrados - sql = SQL_DELETE_AUTOR.format(ids, id_ativo) - cursor = exec_legado(sql) + # Finalmente excluimos os autores redundantes, + # cujas referências foram todas substituídas a essa altura + exec_legado('delete from autor where cod_autor in {}'.format( + tuple(apagar))) def uniformiza_banco(): @@ -481,8 +383,14 @@ relatoria | tip_fim_relatoria = NULL | tip_fim_relatoria = 0 spec = spec.split('|') exec_legado('UPDATE {} SET {} WHERE {}'.format(*spec)) - migra_autor() # Migra autores para um único autor - migra_comissao() # Migra comissões para uma única comissão + # retira apontamentos de materia para assunto inexistente + exec_legado('delete from materia_assunto where cod_assunto = 0') + + # corrige string "None" em autor + exec_legado('update autor set des_cargo = NULL where des_cargo = "None"') + + unifica_autores_repetidos_no_legado('cod_parlamentar') + unifica_autores_repetidos_no_legado('cod_comissao') def iter_sql_records(sql, db): diff --git a/sapl/legacy/scripts/utils.py b/sapl/legacy/scripts/utils.py index 6e2e03723..21f74ce52 100644 --- a/sapl/legacy/scripts/utils.py +++ b/sapl/legacy/scripts/utils.py @@ -1,6 +1,19 @@ import inspect +from sapl.base.models import Autor +from sapl.legacy.migration import appconfs + def getsourcelines(model): return [line.rstrip('\n').decode('utf-8') for line in inspect.getsourcelines(model)[0]] + + +def get_models_com_referencia_a_autor(): + + def tem_referencia_a_autor(model): + return any(getattr(field, 'related_model', None) == Autor + for field in model._meta.get_fields()) + + return [model for app in appconfs for model in app.models.values() + if tem_referencia_a_autor(model)] diff --git a/sapl/legacy/test_migration.py b/sapl/legacy/test_migration.py new file mode 100644 index 000000000..a1ae77365 --- /dev/null +++ b/sapl/legacy/test_migration.py @@ -0,0 +1,55 @@ +from random import shuffle + +from .migration import (get_autorias_sem_repeticoes, + get_reapontamento_de_autores_repetidos) + + +def test_unifica_autores_repetidos_no_legado(): + + # cod_parlamentar, cod_autor + autores = [[0, 0], + [1, 10], + [1, 11], + [1, 12], + [2, 20], + [2, 21], + [2, 22], + [3, 30], + [3, 31], + [4, 40], + [5, 50]] + reapontamento, apagar = get_reapontamento_de_autores_repetidos(autores) + assert reapontamento == {10: 10, 11: 10, 12: 10, + 20: 20, 21: 20, 22: 20, + 30: 30, 31: 30} + assert sorted(apagar) == [11, 12, 21, 22, 31] + + # cod_autor, cod_materia, ind_primeiro_autor + autoria = [[10, 111, 0], # não é repetida, mas envolve um autor repetido + + [22, 222, 1], # não é repetida, mas envolve um autor repetido + + [10, 777, 1], # repetição c ind_primeiro_autor==1 no INÍCIO + [10, 777, 0], + [11, 777, 0], + [12, 777, 0], + + [30, 888, 0], # repetição c ind_primeiro_autor==1 no MEIO + [31, 888, 1], + [30, 888, 0], + + [11, 999, 0], # repetição SEM ind_primeiro_autor==1 + [12, 999, 0], + + [21, 999, 0], # repetição SEM ind_primeiro_autor==1 + [22, 999, 0], + ] + shuffle(autoria) # não devemos supor ordem na autoria + nova_autoria = get_autorias_sem_repeticoes(autoria, reapontamento) + assert nova_autoria == sorted([(10, 111, 0), + (20, 222, 1), + (10, 777, 1), + (30, 888, 1), + (10, 999, 0), + (20, 999, 0), + ])