Repository: interlegis/sapl

Issue:
Migrar autores duplicados para único autor
Unificar autores repetidos a um único autor e ele será ativo se ao menos um dele for ativo


Discussion:

Resolving patch:
diff --git a/sapl/legacy/migration.py b/sapl/legacy/migration.py
--- a/sapl/legacy/migration.py
+++ b/sapl/legacy/migration.py
@@ -1,6 +1,7 @@
 import re
 from datetime import date
 from functools import lru_cache, partial
+from itertools import groupby
 from subprocess import PIPE, call
 
 import pkg_resources
@@ -144,6 +145,13 @@ def exec_sql(sql, db='default'):
     cursor.execute(sql)
     return cursor
 
+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 = '''
@@ -152,19 +160,17 @@ def exec_sql(sql, db='default'):
    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):
@@ -181,6 +187,122 @@ def garante_tabela_no_legado(create_table):
         assert existe_tabela_no_legado(tabela)
 
 
+TABELAS_REFERENCIANDO_AUTOR = [
+    # <nome da tabela>, <tem ind excluido>
+    ('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])))
+
+    # 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))
+
+    # 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():
     exec_legado('''
       SELECT replace(@@sql_mode,"STRICT_TRANS_TABLES,","ALLOW_INVALID_DATES");
@@ -261,6 +383,15 @@ def uniformiza_banco():
         spec = spec.split('|')
         exec_legado('UPDATE {} SET {} WHERE {}'.format(*spec))
 
+    # 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):
     class Record:
diff --git a/sapl/legacy/scripts/utils.py b/sapl/legacy/scripts/utils.py
--- 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)]


Test patch:
diff --git a/sapl/legacy/test_migration.py b/sapl/legacy/test_migration.py
new file mode 100644
--- /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),
+                                   ])