mirror of https://github.com/interlegis/sapl.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1993 lines
67 KiB
1993 lines
67 KiB
import datetime
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import traceback
|
|
from collections import OrderedDict, defaultdict, namedtuple
|
|
from datetime import date
|
|
from functools import lru_cache, partial
|
|
from itertools import groupby
|
|
from operator import xor
|
|
|
|
import git
|
|
import pkg_resources
|
|
import pyaml
|
|
import pytz
|
|
import reversion
|
|
import yaml
|
|
from bs4 import BeautifulSoup
|
|
from django.apps import apps
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import Group
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.core.management.commands.flush import Command as FlushCommand
|
|
from django.db import connections, transaction
|
|
from django.db.models import Max, Q
|
|
from pyaml import UnsafePrettyYAMLDumper
|
|
from reversion.models import Revision, Version
|
|
from unipath import Path
|
|
|
|
from sapl.base.models import AppConfig as AppConf
|
|
from sapl.base.models import Autor, TipoAutor, cria_models_tipo_autor
|
|
from sapl.comissoes.models import Comissao, Composicao, Participacao, Reuniao
|
|
from sapl.legacy.models import NormaJuridica as OldNormaJuridica
|
|
from sapl.legacy.models import Numeracao, TipoNumeracaoProtocolo
|
|
from sapl.legacy_migration_settings import (DIR_DADOS_MIGRACAO, DIR_REPO,
|
|
NOME_BANCO_LEGADO, PYTZ_TIMEZONE,
|
|
SIGLA_CASA)
|
|
from sapl.materia.models import (AcompanhamentoMateria, DocumentoAcessorio,
|
|
MateriaLegislativa, Proposicao,
|
|
StatusTramitacao, TipoDocumento,
|
|
TipoMateriaLegislativa, TipoProposicao,
|
|
Tramitacao)
|
|
from sapl.norma.models import (AssuntoNorma, NormaJuridica, NormaRelacionada,
|
|
TipoVinculoNormaJuridica)
|
|
from sapl.parlamentares.models import (Legislatura, Mandato, Parlamentar,
|
|
Partido, TipoAfastamento)
|
|
from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo,
|
|
StatusTramitacaoAdministrativo)
|
|
from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, OrdemDia,
|
|
RegistroVotacao, TipoResultadoVotacao)
|
|
from sapl.utils import normalize
|
|
|
|
from .scripts.normaliza_dump_mysql import normaliza_dump_mysql
|
|
|
|
# BASE ######################################################################
|
|
# apps to be migrated, in app dependency order (very important)
|
|
appconfs = [
|
|
apps.get_app_config(n)
|
|
for n in [
|
|
"parlamentares",
|
|
"comissoes",
|
|
# base precisa vir depois dos apps parlamentares e comissoes
|
|
# pois Autor os referencia
|
|
"base",
|
|
"materia",
|
|
"norma",
|
|
"sessao",
|
|
"lexml",
|
|
"protocoloadm",
|
|
]
|
|
]
|
|
|
|
unique_constraints = []
|
|
one_to_one_constraints = []
|
|
primeira_vez = []
|
|
|
|
# apps quase não têm interseção
|
|
name_sets = [
|
|
(ac.label, set(m.__name__ for m in ac.get_models())) for ac in appconfs
|
|
]
|
|
for a1, s1 in name_sets:
|
|
for a2, s2 in name_sets:
|
|
if a1 is not a2:
|
|
# existe uma interseção de nomes entre comissoes e materia
|
|
if {a1, a2} == {"comissoes", "materia"}:
|
|
assert s1.intersection(s2) == {"DocumentoAcessorio"}
|
|
else:
|
|
assert not s1.intersection(s2)
|
|
|
|
|
|
# RENAMES ###################################################################
|
|
|
|
MODEL_RENAME_PATTERN = re.compile("(.+) \((.+)\)")
|
|
MODEL_RENAME_INCLUDE_PATTERN = re.compile("<(.+)>")
|
|
|
|
|
|
def get_renames():
|
|
field_renames = {}
|
|
model_renames = {}
|
|
includes = {}
|
|
for app in appconfs:
|
|
app_rename_data = yaml.load(
|
|
pkg_resources.resource_string(app.module.__name__, "legacy.yaml")
|
|
)
|
|
for model_name, renames in app_rename_data.items():
|
|
# armazena ou substitui includes
|
|
if MODEL_RENAME_INCLUDE_PATTERN.match(model_name):
|
|
includes[model_name] = renames
|
|
continue
|
|
elif isinstance(renames, str):
|
|
renames = includes[renames]
|
|
# detecta mudança de nome
|
|
match = MODEL_RENAME_PATTERN.match(model_name)
|
|
if match:
|
|
model_name, old_name = match.groups()
|
|
else:
|
|
old_name = None
|
|
model = app.get_model(model_name)
|
|
if old_name:
|
|
model_renames[model] = old_name
|
|
field_renames[model] = renames
|
|
|
|
return field_renames, model_renames
|
|
|
|
|
|
field_renames, model_renames = get_renames()
|
|
legacy_app = apps.get_app_config("legacy")
|
|
models_novos_para_antigos = {
|
|
model: legacy_app.get_model(model_renames.get(model, model.__name__))
|
|
for model in field_renames
|
|
}
|
|
models_novos_para_antigos[Composicao] = models_novos_para_antigos[Participacao]
|
|
|
|
campos_novos_para_antigos = {
|
|
model._meta.get_field(nome_novo): nome_antigo
|
|
for model, renames in field_renames.items()
|
|
for nome_novo, nome_antigo in renames.items()
|
|
}
|
|
|
|
# campos de Composicao (de Comissao)
|
|
for nome_novo, nome_antigo in (
|
|
("comissao", "cod_comissao"),
|
|
("periodo", "cod_periodo_comp"),
|
|
):
|
|
campos_novos_para_antigos[
|
|
Composicao._meta.get_field(nome_novo)
|
|
] = nome_antigo
|
|
|
|
|
|
# campos virtuais de Proposicao para funcionar com get_fk_related
|
|
class CampoVirtual(namedtuple("CampoVirtual", "model related_model")):
|
|
null = True
|
|
|
|
|
|
CAMPOS_VIRTUAIS_PROPOSICAO = {
|
|
TipoMateriaLegislativa: CampoVirtual(Proposicao, MateriaLegislativa),
|
|
TipoDocumento: CampoVirtual(Proposicao, DocumentoAcessorio),
|
|
}
|
|
for campo_virtual in CAMPOS_VIRTUAIS_PROPOSICAO.values():
|
|
campos_novos_para_antigos[campo_virtual] = "cod_mat_ou_doc"
|
|
|
|
|
|
CAMPOS_VIRTUAIS_TIPO_PROPOSICAO = {
|
|
"M": CampoVirtual(TipoProposicao, TipoMateriaLegislativa),
|
|
"D": CampoVirtual(TipoProposicao, TipoDocumento),
|
|
}
|
|
for campo_virtual in CAMPOS_VIRTUAIS_TIPO_PROPOSICAO.values():
|
|
campos_novos_para_antigos[campo_virtual] = "tip_mat_ou_doc"
|
|
|
|
|
|
# campos virtuais de Autor para funcionar com get_fk_related
|
|
CAMPOS_VIRTUAIS_AUTOR = {
|
|
related: CampoVirtual(Autor, related)
|
|
for related in (Parlamentar, Comissao, Partido)
|
|
}
|
|
for related, campo_antigo in [
|
|
(Parlamentar, "cod_parlamentar"),
|
|
(Comissao, "cod_comissao"),
|
|
(Partido, "cod_partido"),
|
|
]:
|
|
campo_virtual = CAMPOS_VIRTUAIS_AUTOR[related]
|
|
campos_novos_para_antigos[campo_virtual] = campo_antigo
|
|
|
|
|
|
# MIGRATION #################################################################
|
|
|
|
|
|
def info(msg):
|
|
print("INFO: " + msg)
|
|
|
|
|
|
ocorrencias = defaultdict(list)
|
|
|
|
|
|
def warn(tipo, msg, dados):
|
|
ocorrencias[tipo].append(dados)
|
|
print("CUIDADO! " + msg.format(**dados))
|
|
|
|
|
|
@lru_cache()
|
|
def get_pk_legado(tabela):
|
|
if tabela == "despacho_inicial":
|
|
# adaptação para deleção correta no mysql ao final de migrar_model
|
|
# acompanha o agrupamento de despacho_inicial feito em iter_sql_records
|
|
return "cod_materia", "cod_comissao"
|
|
elif tabela == "mesa_sessao_plenaria":
|
|
# retiramos 'cod_sessao_leg' redundante que da problema
|
|
# ao verificar se o registro já está migrado
|
|
return "cod_cargo", "cod_parlamentar", "cod_sessao_plen"
|
|
elif tabela == "composicao_mesa":
|
|
# em alguns bancos a chave é
|
|
# cod_parlamentar, cod_periodo_comp, cod_cargo
|
|
# mas essa parece sempre ser uma chave candidata
|
|
return "cod_parlamentar", "cod_sessao_leg", "cod_cargo"
|
|
res = exec_legado(
|
|
'show index from {} WHERE Key_name = "PRIMARY"'.format(tabela)
|
|
)
|
|
return [r[4] for r in res]
|
|
|
|
|
|
@lru_cache()
|
|
def get_estrutura_legado(model):
|
|
model_legado = models_novos_para_antigos[model]
|
|
tabela_legado = model_legado._meta.db_table
|
|
campos_pk_legado = get_pk_legado(tabela_legado)
|
|
return model_legado, tabela_legado, campos_pk_legado
|
|
|
|
|
|
def com_aspas_se_necessario(valor):
|
|
if isinstance(valor, int):
|
|
return valor
|
|
else:
|
|
return '"{}"'.format(valor)
|
|
|
|
|
|
class ForeignKeyFaltando(ObjectDoesNotExist):
|
|
"Uma FK aponta para um registro inexistente"
|
|
|
|
def __init__(self, field, valor, old):
|
|
self.field = field
|
|
self.valor = valor
|
|
self.old = old
|
|
|
|
msg = (
|
|
"FK não encontrada para [{campo} = {valor}] (em {tabela} / pk = {pk})"
|
|
) # noqa
|
|
|
|
@property
|
|
def dados(self):
|
|
campo = campos_novos_para_antigos[self.field]
|
|
_, tabela, campos_pk = get_estrutura_legado(self.field.model)
|
|
pk = {c: getattr(self.old, c) for c in campos_pk}
|
|
sql = "select * from {} where {};".format(
|
|
tabela,
|
|
" and ".join(
|
|
[
|
|
"{} = {}".format(k, com_aspas_se_necessario(v))
|
|
for k, v in pk.items()
|
|
]
|
|
),
|
|
)
|
|
return OrderedDict(
|
|
(
|
|
("campo", campo),
|
|
("valor", self.valor),
|
|
("tabela", tabela),
|
|
("pk", pk),
|
|
("sql", sql),
|
|
)
|
|
)
|
|
|
|
|
|
def get_all_ids_from_model(model):
|
|
return set(model.objects.values_list("id", flat=True))
|
|
|
|
|
|
@lru_cache()
|
|
def _cached_get_all_ids_from_model(model):
|
|
# esta função para uso apenas em get_fk_related
|
|
return get_all_ids_from_model(model)
|
|
|
|
|
|
def get_fk_related(field, old):
|
|
valor = getattr(old, campos_novos_para_antigos[field])
|
|
if valor is None and field.null:
|
|
return None
|
|
if valor in _cached_get_all_ids_from_model(field.related_model):
|
|
return valor
|
|
elif valor == 0 and field.null:
|
|
# consideramos zeros como nulos, se não está entre os ids anteriores
|
|
return None
|
|
else:
|
|
raise ForeignKeyFaltando(field=field, valor=valor, old=old)
|
|
|
|
|
|
def exec_sql(sql, db="default"):
|
|
cursor = connections[db].cursor()
|
|
cursor.execute(sql)
|
|
return cursor
|
|
|
|
|
|
exec_legado = partial(exec_sql, db="legacy")
|
|
|
|
|
|
def formatar_lista_para_sql(iteravel):
|
|
lista = list(iteravel)
|
|
if lista:
|
|
return "({})".format(str(lista)[1:-1]) # transforma "[...]" em "(...)"
|
|
else:
|
|
return None
|
|
|
|
|
|
def exec_legado_em_subconjunto(sql, ids):
|
|
"""Executa uma query sql no legado no formato '.... in {}'
|
|
interpolando `ids`, se houver ids"""
|
|
|
|
lista_sql = formatar_lista_para_sql(ids)
|
|
if lista_sql:
|
|
return exec_legado(sql.format(lista_sql))
|
|
else:
|
|
return []
|
|
|
|
|
|
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="{}"
|
|
"""
|
|
|
|
|
|
def existe_tabela_no_legado(tabela):
|
|
sql = SQL_NAO_TEM_TABELA.format(tabela)
|
|
return list(primeira_coluna(exec_legado(sql)))[0]
|
|
|
|
|
|
def existe_coluna_no_legado(tabela, coluna):
|
|
sql_nao_tem_coluna = SQL_NAO_TEM_TABELA + ' AND COLUMN_NAME="{}"'
|
|
sql = sql_nao_tem_coluna.format(tabela, coluna)
|
|
return list(primeira_coluna(exec_legado(sql)))[0] > 0
|
|
|
|
|
|
def garante_coluna_no_legado(tabela, spec_coluna):
|
|
coluna = spec_coluna.split()[0]
|
|
if not existe_coluna_no_legado(tabela, coluna):
|
|
exec_legado("ALTER TABLE {} ADD COLUMN {}".format(tabela, spec_coluna))
|
|
assert existe_coluna_no_legado(tabela, coluna)
|
|
|
|
|
|
def garante_tabela_no_legado(create_table):
|
|
tabela = create_table.strip().splitlines()[0].split()[2]
|
|
if not existe_tabela_no_legado(tabela):
|
|
exec_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():
|
|
"""Reverte a exclusão de autores que sejam referenciados de alguma forma
|
|
na base legada"""
|
|
|
|
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_em_subconjunto(
|
|
"update autor set ind_excluido = 0 where cod_autor in {}",
|
|
autores_referenciados,
|
|
)
|
|
|
|
# propaga exclusões para autores não referenciados
|
|
for tabela, fk in [
|
|
("parlamentar", "cod_parlamentar"),
|
|
("comissao", "cod_comissao"),
|
|
]:
|
|
sql = """
|
|
update autor set ind_excluido = 1
|
|
where {cod_parlamentar} is not null
|
|
and {cod_parlamentar} not in (
|
|
select {cod_parlamentar} from {parlamentar}
|
|
where ind_excluido <> 1)
|
|
""".format(
|
|
parlamentar=tabela, cod_parlamentar=fk
|
|
)
|
|
if autores_referenciados:
|
|
sql += " and cod_autor not in ({})".format(
|
|
", ".join(map(str, autores_referenciados))
|
|
)
|
|
exec_legado(sql)
|
|
|
|
|
|
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"
|
|
|
|
# usamos uma tupla neutra se o conjunto é vazio
|
|
# p q a query seja sintaticamente correta
|
|
ids_ja_migrados = formatar_lista_para_sql(
|
|
get_all_ids_from_model(Autor) or [-1000]
|
|
)
|
|
|
|
# enumeramos a repeticoes segundo o campo relevante
|
|
# (p. ex. cod_parlamentar ou cod_comissao)
|
|
# a ordenação prioriza, as entradas:
|
|
# - ja migradas previamente
|
|
# - não excluidas,
|
|
# - em seguida as que têm col_username,
|
|
# - em seguida as que têm des_cargo
|
|
autores = exec_legado(
|
|
f"""
|
|
select {campo_agregador}, cod_autor,
|
|
(cod_autor in {ids_ja_migrados}) ja_migrado
|
|
from autor
|
|
where {campo_agregador} is not null
|
|
order by {campo_agregador},
|
|
ja_migrado desc,
|
|
ind_excluido, col_username desc, des_cargo desc"""
|
|
)
|
|
# descartamos o último campo, usado apenas p ordenar corretamente
|
|
autores = [a[:-1] for a in autores]
|
|
# ordenamos, pois o order by nesses instalações do mysql parece ignorar case
|
|
# em alguns acasos temos erros estranhos
|
|
autores = sorted(autores)
|
|
|
|
reapontamento, apagar = get_reapontamento_de_autores_repetidos(autores)
|
|
|
|
# se não houver autores repetidos encerramos por aqui
|
|
if not reapontamento:
|
|
return
|
|
|
|
# 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 {}"
|
|
autoria = exec_legado_em_subconjunto(
|
|
"select cod_autor, cod_materia, ind_primeiro_autor" + from_autoria,
|
|
reapontamento,
|
|
)
|
|
|
|
# apagamos todas as autorias envolvidas
|
|
exec_legado_em_subconjunto("delete " + from_autoria, reapontamento)
|
|
# e depois inserimos apenas as sem repetições c ind_primeiro_autor ajustado
|
|
nova_autoria = get_autorias_sem_repeticoes(autoria, reapontamento)
|
|
if nova_autoria:
|
|
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_em_subconjunto(
|
|
"delete from autor where cod_autor in {}", apagar
|
|
)
|
|
|
|
|
|
def anula_tipos_origem_externa_invalidos():
|
|
"""Anula tipos de origem externa inválidos
|
|
para que não impeçam a migração da matéria"""
|
|
|
|
tipos_validos = primeira_coluna(
|
|
exec_legado(
|
|
"""
|
|
select tip_materia
|
|
from tipo_materia_legislativa
|
|
where ind_excluido <> 1;"""
|
|
)
|
|
)
|
|
|
|
exec_legado_em_subconjunto(
|
|
"""
|
|
update materia_legislativa
|
|
set tip_origem_externa = NULL
|
|
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)
|
|
como_resolver = get_como_resolver_registro_votacao_ambiguo()
|
|
ambiguos = ambiguos - set(como_resolver)
|
|
|
|
if ambiguos:
|
|
warn(
|
|
"registro_votacao_ambiguos",
|
|
"Existe(m) RegistroVotacao ambíguo(s): {cod_votacao}",
|
|
{"cod_votacao": 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,
|
|
)
|
|
|
|
|
|
PROPAGACOES_DE_EXCLUSAO = [
|
|
# sessao_legislativa
|
|
("sessao_legislativa", "composicao_mesa", "cod_sessao_leg"),
|
|
# parlamentar
|
|
("parlamentar", "dependente", "cod_parlamentar"),
|
|
("parlamentar", "filiacao", "cod_parlamentar"),
|
|
("parlamentar", "mandato", "cod_parlamentar"),
|
|
("parlamentar", "composicao_mesa", "cod_parlamentar"),
|
|
("parlamentar", "composicao_comissao", "cod_parlamentar"),
|
|
# no 2.5 os parlamentares excluídos não são listados na presença da sessão
|
|
("parlamentar", "sessao_plenaria_presenca", "cod_parlamentar"),
|
|
# ... nem na presença da ordem do dia
|
|
("parlamentar", "ordem_dia_presenca", "cod_parlamentar"),
|
|
# ... nem na mesa da sessão
|
|
("parlamentar", "mesa_sessao_plenaria", "cod_parlamentar"),
|
|
# coligacao
|
|
("coligacao", "composicao_coligacao", "cod_coligacao"),
|
|
# comissao
|
|
("comissao", "composicao_comissao", "cod_comissao"),
|
|
("periodo_comp_comissao", "composicao_comissao", "cod_periodo_comp"),
|
|
# sessao
|
|
("sessao_plenaria", "ordem_dia", "cod_sessao_plen"),
|
|
("sessao_plenaria", "expediente_materia", "cod_sessao_plen"),
|
|
("sessao_plenaria", "expediente_sessao_plenaria", "cod_sessao_plen"),
|
|
("sessao_plenaria", "sessao_plenaria_presenca", "cod_sessao_plen"),
|
|
("sessao_plenaria", "ordem_dia_presenca", "cod_sessao_plen"),
|
|
("sessao_plenaria", "mesa_sessao_plenaria", "cod_sessao_plen"),
|
|
("sessao_plenaria", "oradores", "cod_sessao_plen"),
|
|
("sessao_plenaria", "oradores_expediente", "cod_sessao_plen"),
|
|
# as consultas no código do sapl 2.5
|
|
# votacao_ordem_dia_obter_zsql e votacao_expediente_materia_obter_zsql
|
|
# indicam que os registros de votação de matérias excluídas não são
|
|
# exibidos...
|
|
("materia_legislativa", "registro_votacao", "cod_materia"),
|
|
# as exclusões de registro_votacao sem referência
|
|
# nem a ordem_dia nem a expediente_materia são feitas num método à parte
|
|
# materia
|
|
("materia_legislativa", "tramitacao", "cod_materia"),
|
|
("materia_legislativa", "autoria", "cod_materia"),
|
|
("materia_legislativa", "anexada", "cod_materia_principal"),
|
|
("materia_legislativa", "anexada", "cod_materia_anexada"),
|
|
("materia_legislativa", "documento_acessorio", "cod_materia"),
|
|
("materia_legislativa", "numeracao", "cod_materia"),
|
|
("materia_legislativa", "expediente_materia", "cod_materia"),
|
|
("materia_legislativa", "ordem_dia", "cod_materia"),
|
|
("materia_legislativa", "acomp_materia", "cod_materia"),
|
|
("materia_legislativa", "despacho_inicial", "cod_materia"),
|
|
("materia_legislativa", "legislacao_citada", "cod_materia"),
|
|
("materia_legislativa", "relatoria", "cod_materia"),
|
|
("materia_legislativa", "materia_assunto", "cod_materia"),
|
|
# norma
|
|
("norma_juridica", "vinculo_norma_juridica", "cod_norma_referente"),
|
|
("norma_juridica", "vinculo_norma_juridica", "cod_norma_referida"),
|
|
("norma_juridica", "legislacao_citada", "cod_norma"),
|
|
# documento administrativo
|
|
("documento_administrativo", "tramitacao_administrativo", "cod_documento"),
|
|
]
|
|
|
|
PROPAGACOES_DE_EXCLUSAO_REGISTROS_VOTACAO = [
|
|
("registro_votacao", "registro_votacao_parlamentar", "cod_votacao")
|
|
]
|
|
|
|
|
|
def propaga_exclusoes(propagacoes):
|
|
for tabela_pai, tabela_filha, fk in propagacoes:
|
|
[pk_pai] = get_pk_legado(tabela_pai)
|
|
sql = """
|
|
update {} set ind_excluido = 1 where {} not in (
|
|
select {} from {} where ind_excluido != 1)
|
|
""".format(
|
|
tabela_filha, fk, pk_pai, tabela_pai
|
|
)
|
|
exec_legado(sql)
|
|
|
|
|
|
def corrige_unidades_tramitacao_destino_vazia_como_anterior():
|
|
"""Se uma unidade de tramitação estiver vazia no legado a configura
|
|
como a anterior"""
|
|
|
|
for tabela_tramitacao in ["tramitacao", "tramitacao_administrativo"]:
|
|
exec_legado(
|
|
"""
|
|
update {}
|
|
set cod_unid_tram_dest = cod_unid_tram_local
|
|
where cod_unid_tram_dest is null;
|
|
""".format(
|
|
tabela_tramitacao
|
|
)
|
|
)
|
|
|
|
|
|
def apaga_ref_a_mats_e_docs_inexistentes_em_proposicoes():
|
|
# as referencias a matérias e documentos apagados não aparecem no 3.1
|
|
# além do que, se ressuscitássemos essas matérias e docs,
|
|
# não seria possível apagá-los,
|
|
# pois é impossível para um usuário não autor acessar as proposicões
|
|
# para apagar a referências antes
|
|
exec_legado(
|
|
"""
|
|
update proposicao set cod_materia = NULL where cod_materia not in (
|
|
select cod_materia from materia_legislativa
|
|
where ind_excluido <> 1);
|
|
"""
|
|
)
|
|
props_sem_mats = list(
|
|
primeira_coluna(
|
|
exec_legado(
|
|
"""
|
|
select cod_proposicao from proposicao p inner join tipo_proposicao t
|
|
on p.tip_proposicao = t.tip_proposicao
|
|
where t.ind_mat_ou_doc = 'M' and cod_mat_ou_doc not in (
|
|
select cod_materia from materia_legislativa
|
|
where ind_excluido <> 1)
|
|
"""
|
|
)
|
|
)
|
|
)
|
|
props_sem_docs = list(
|
|
primeira_coluna(
|
|
exec_legado(
|
|
"""
|
|
select cod_proposicao from proposicao p inner join tipo_proposicao t
|
|
on p.tip_proposicao = t.tip_proposicao
|
|
where t.ind_mat_ou_doc = 'D' and cod_mat_ou_doc not in (
|
|
select cod_documento from documento_acessorio
|
|
where ind_excluido <> 1);
|
|
"""
|
|
)
|
|
)
|
|
)
|
|
exec_legado_em_subconjunto(
|
|
"""
|
|
update proposicao set cod_mat_ou_doc = NULL
|
|
where cod_proposicao in {}""",
|
|
props_sem_mats + props_sem_docs,
|
|
)
|
|
|
|
|
|
def reaponta_tipo_autor():
|
|
# e corrige um erro comum
|
|
if TipoAutor.objects.filter(descricao="Comissão").exists():
|
|
exec_legado(
|
|
""" update tipo_autor set des_tipo_autor = 'Comissão'
|
|
where des_tipo_autor = 'Comissao';
|
|
"""
|
|
)
|
|
|
|
conflitos, max_id = encontra_conflitos_tipo_autor()
|
|
|
|
def sql_reaponta_tipo_autor(id_novo, id_antigo):
|
|
return f"""
|
|
update tipo_autor set tip_autor = {id_novo} where tip_autor = {id_antigo};
|
|
update autor set tip_autor = {id_novo} where tip_autor = {id_antigo};
|
|
"""
|
|
|
|
# tenta reapontar para o que é usado agora
|
|
conflitos_restantes = []
|
|
for id_antigo, (descricao_no_legado, *_) in conflitos.items():
|
|
tipo_novo = TipoAutor.objects.filter(descricao=descricao_no_legado)
|
|
if tipo_novo:
|
|
[tipo_novo] = tipo_novo
|
|
id_novo = tipo_novo.id
|
|
exec_legado(sql_reaponta_tipo_autor(id_novo, id_antigo))
|
|
else:
|
|
conflitos_restantes.append(id_antigo)
|
|
|
|
# reaponta para novos ids
|
|
for id_novo, id_antigo in enumerate(conflitos_restantes, max_id + 1):
|
|
exec_legado(sql_reaponta_tipo_autor(id_novo, id_antigo))
|
|
|
|
|
|
def uniformiza_banco(primeira_migracao):
|
|
"Uniformiza e ajusta o banco legado antes de migrar"
|
|
|
|
# restringe TipoAutor somente ao realmente utilizado
|
|
exec_legado(
|
|
""" delete from tipo_autor where tip_autor not in (
|
|
select distinct(tip_autor) from autor);
|
|
"""
|
|
)
|
|
if not primeira_migracao:
|
|
reaponta_tipo_autor()
|
|
|
|
propaga_exclusoes(PROPAGACOES_DE_EXCLUSAO)
|
|
checa_registros_votacao_ambiguos_e_remove_nao_usados()
|
|
propaga_exclusoes(PROPAGACOES_DE_EXCLUSAO_REGISTROS_VOTACAO)
|
|
|
|
garante_coluna_no_legado("proposicao", "num_proposicao int(11) NULL")
|
|
|
|
garante_coluna_no_legado(
|
|
"tipo_materia_legislativa",
|
|
"ind_num_automatica BOOLEAN NULL DEFAULT FALSE",
|
|
)
|
|
|
|
garante_coluna_no_legado(
|
|
"tipo_materia_legislativa", "quorum_minimo_votacao int(11) NULL"
|
|
)
|
|
|
|
garante_coluna_no_legado("materia_legislativa", "txt_resultado TEXT NULL")
|
|
|
|
# Cria campos cod_presenca_sessao (sendo a nova PK da tabela)
|
|
# e dat_sessao em sessao_plenaria_presenca
|
|
if not existe_coluna_no_legado(
|
|
"sessao_plenaria_presenca", "cod_presenca_sessao"
|
|
):
|
|
exec_legado(
|
|
"""
|
|
ALTER TABLE sessao_plenaria_presenca
|
|
DROP PRIMARY KEY,
|
|
ADD cod_presenca_sessao INT auto_increment PRIMARY KEY FIRST;
|
|
"""
|
|
)
|
|
assert existe_coluna_no_legado(
|
|
"sessao_plenaria_presenca", "cod_presenca_sessao"
|
|
)
|
|
|
|
garante_coluna_no_legado(
|
|
"sessao_plenaria_presenca", "dat_sessao DATE NULL"
|
|
)
|
|
|
|
garante_tabela_no_legado(
|
|
"""
|
|
CREATE TABLE lexml_registro_publicador (
|
|
cod_publicador INT auto_increment NOT NULL,
|
|
id_publicador INT, nom_publicador varchar(255),
|
|
adm_email varchar(50),
|
|
sigla varchar(255),
|
|
nom_responsavel varchar(255),
|
|
tipo varchar(50),
|
|
id_responsavel INT, PRIMARY KEY (cod_publicador));
|
|
"""
|
|
)
|
|
|
|
garante_tabela_no_legado(
|
|
"""
|
|
CREATE TABLE lexml_registro_provedor (
|
|
cod_provedor INT auto_increment NOT NULL,
|
|
id_provedor INT, nom_provedor varchar(255),
|
|
sgl_provedor varchar(15),
|
|
adm_email varchar(50),
|
|
nom_responsavel varchar(255),
|
|
tipo varchar(50),
|
|
id_responsavel INT, xml_provedor longtext,
|
|
PRIMARY KEY (cod_provedor));
|
|
"""
|
|
)
|
|
|
|
garante_tabela_no_legado(
|
|
"""
|
|
CREATE TABLE tipo_situacao_militar (
|
|
tip_situacao_militar INT auto_increment NOT NULL,
|
|
des_tipo_situacao varchar(50),
|
|
ind_excluido INT, PRIMARY KEY (tip_situacao_militar));
|
|
"""
|
|
)
|
|
|
|
update_specs = """
|
|
vinculo_norma_juridica | ind_excluido = '' | trim(ind_excluido) = '0'
|
|
unidade_tramitacao | cod_parlamentar = NULL | cod_parlamentar = 0
|
|
parlamentar | cod_nivel_instrucao = NULL | cod_nivel_instrucao = 0
|
|
parlamentar | tip_situacao_militar = NULL | tip_situacao_militar = 0
|
|
mandato | tip_afastamento = NULL | tip_afastamento = 0
|
|
relatoria | tip_fim_relatoria = NULL | tip_fim_relatoria = 0
|
|
sessao_plenaria_presenca | dat_sessao = NULL | dat_sessao = 0
|
|
""".strip().splitlines()
|
|
|
|
for spec in update_specs:
|
|
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")
|
|
unifica_autores_repetidos_no_legado("col_username")
|
|
|
|
# é importante reverter a exclusão de autores somente depois, para que a
|
|
# unificação possa dar prioridade às informações dos autores não excluídos
|
|
reverte_exclusao_de_autores_referenciados_no_legado()
|
|
|
|
anula_tipos_origem_externa_invalidos()
|
|
corrige_unidades_tramitacao_destino_vazia_como_anterior()
|
|
|
|
# matérias inexistentes não são mostradas em norma jurídica => apagamos
|
|
exec_legado(
|
|
"""update norma_juridica set cod_materia = NULL
|
|
where cod_materia not in (
|
|
select cod_materia from materia_legislativa
|
|
where ind_excluido <> 1);"""
|
|
)
|
|
|
|
apaga_ref_a_mats_e_docs_inexistentes_em_proposicoes()
|
|
|
|
|
|
class Record:
|
|
pass
|
|
|
|
|
|
def iter_sql_records(tabela):
|
|
if tabela == "despacho_inicial":
|
|
sql = """ select cod_materia, cod_comissao from despacho_inicial
|
|
where ind_excluido <> 1
|
|
group by cod_materia, cod_comissao
|
|
order by cod_materia, min(num_ordem)
|
|
"""
|
|
else:
|
|
sql = "select * from " + tabela
|
|
if existe_coluna_no_legado(tabela, "ind_excluido"):
|
|
sql += " where ind_excluido <> 1"
|
|
cursor = exec_legado(sql)
|
|
fieldnames = [name[0] for name in cursor.description]
|
|
for row in cursor.fetchall():
|
|
record = Record()
|
|
record.__dict__.update(zip(fieldnames, row))
|
|
yield record
|
|
|
|
|
|
def fill_vinculo_norma_juridica():
|
|
lista = [
|
|
("A", "Altera o(a)", "Alterado(a) pelo(a)"),
|
|
(
|
|
"R",
|
|
"Revoga integralmente o(a)",
|
|
"Revogado(a) integralmente pelo(a)",
|
|
),
|
|
("P", "Revoga parcialmente o(a)", "Revogado(a) parcialmente pelo(a)"),
|
|
(
|
|
"T",
|
|
"Revoga integralmente por consolidação",
|
|
"Revogado(a) integralmente por consolidação",
|
|
),
|
|
("C", "Norma correlata", "Norma correlata"),
|
|
("S", "Ressalva o(a)", "Ressalvada pelo(a)"),
|
|
("E", "Reedita o(a)", "Reeditada pelo(a)"),
|
|
("I", "Reedita com alteração o(a)", "Reeditada com alteração pelo(a)"),
|
|
("G", "Regulamenta o(a)", "Regulamentada pelo(a)"),
|
|
(
|
|
"K",
|
|
"Suspende parcialmente o(a)",
|
|
"Suspenso(a) parcialmente pelo(a)",
|
|
),
|
|
(
|
|
"L",
|
|
"Suspende integralmente o(a)",
|
|
"Suspenso(a) integralmente pelo(a)",
|
|
),
|
|
(
|
|
"N",
|
|
"Julga integralmente inconstitucional",
|
|
"Julgada integralmente inconstitucional",
|
|
),
|
|
(
|
|
"O",
|
|
"Julga parcialmente inconstitucional",
|
|
"Julgada parcialmente inconstitucional",
|
|
),
|
|
]
|
|
lista_objs = [
|
|
TipoVinculoNormaJuridica(
|
|
sigla=item[0], descricao_ativa=item[1], descricao_passiva=item[2]
|
|
)
|
|
for item in lista
|
|
]
|
|
TipoVinculoNormaJuridica.objects.bulk_create(lista_objs)
|
|
|
|
|
|
def criar_configuracao_inicial():
|
|
# só deve ser chamado na primeira migracão
|
|
appconf = AppConf.objects.first()
|
|
if appconf:
|
|
appconf.delete()
|
|
assert not AppConf.objects.exists()
|
|
|
|
# Ajusta sequencia numérica de protocolo e cria base.AppConfig
|
|
if (
|
|
existe_tabela_no_legado(TipoNumeracaoProtocolo._meta.db_table)
|
|
and TipoNumeracaoProtocolo.objects.exists()
|
|
):
|
|
# se este banco legado tem a a configuração de numeração de protocolo
|
|
tipo = TipoNumeracaoProtocolo.objects.latest("dat_inicial_protocolo")
|
|
descricao = tipo.des_numeracao_protocolo
|
|
if "POR ANO" in descricao:
|
|
sequencia_numeracao = "A"
|
|
elif "POR LEGISLATURA" in descricao:
|
|
sequencia_numeracao = "L"
|
|
elif "CONSECUTIVO" in descricao:
|
|
sequencia_numeracao = "U"
|
|
else:
|
|
sequencia_numeracao = "A"
|
|
appconf = AppConf(sequencia_numeracao_protocolo=sequencia_numeracao)
|
|
appconf.save()
|
|
|
|
|
|
def get_sequence_name_and_last_value(model):
|
|
sequence_name = "%s_id_seq" % model._meta.db_table
|
|
[(last_value,)] = exec_sql(f"select last_value from {sequence_name}")
|
|
return sequence_name, last_value
|
|
|
|
|
|
def reinicia_sequence(model, ultima_pk_legado):
|
|
ultimo_id = max(
|
|
ultima_pk_legado,
|
|
model.objects.latest("id").id if model.objects.exists() else 0,
|
|
)
|
|
sequence_name, last_value = get_sequence_name_and_last_value(model)
|
|
if ultimo_id > last_value:
|
|
exec_sql(
|
|
f"""
|
|
ALTER SEQUENCE {sequence_name}
|
|
RESTART WITH {ultimo_id + 1} MINVALUE -1;"""
|
|
)
|
|
|
|
|
|
REPO = git.Repo.init(DIR_REPO)
|
|
|
|
|
|
def populate_renamed_fields(new, old):
|
|
renames = field_renames[type(new)]
|
|
|
|
for field in new._meta.fields:
|
|
old_field_name = renames.get(field.name)
|
|
if old_field_name:
|
|
field_type = field.get_internal_type()
|
|
|
|
if field_type == "ForeignKey":
|
|
fk_field_name = "{}_id".format(field.name)
|
|
value = get_fk_related(field, old)
|
|
setattr(new, fk_field_name, value)
|
|
else:
|
|
value = getattr(old, old_field_name)
|
|
|
|
if field_type in ("CharField", "TextField"):
|
|
if value in (None, "None"):
|
|
value = ""
|
|
elif isinstance(value, str):
|
|
# retira caracters nulos que o postgres não aceita
|
|
# quando usamos bulk_create
|
|
value = value.replace("\0", "")
|
|
|
|
# ajusta tempos segundo timezone
|
|
# os campos TIMESTAMP do mysql são gravados em UTC
|
|
# os DATETIME e TIME não têm timezone
|
|
|
|
if field_type == "DateTimeField" and value:
|
|
# as datas armazenadas no legado na verdade são naive
|
|
sem_tz = value.replace(tzinfo=None)
|
|
value = PYTZ_TIMEZONE.localize(sem_tz).astimezone(pytz.utc)
|
|
|
|
if field_type == "TimeField" and value:
|
|
value = value.replace(tzinfo=PYTZ_TIMEZONE)
|
|
|
|
setattr(new, field.name, value)
|
|
|
|
|
|
def roda_comando_shell(cmd):
|
|
res = os.system(cmd)
|
|
assert res == 0, "O comando falhou: {}".format(cmd)
|
|
|
|
|
|
def get_arquivos_ajustes_pre_migracao():
|
|
return [
|
|
DIR_DADOS_MIGRACAO.child(
|
|
"ajustes_pre_migracao", f"{SIGLA_CASA}.{sufixo}"
|
|
)
|
|
for sufixo in ("sql", "reverter.yaml")
|
|
]
|
|
|
|
|
|
def do_flush():
|
|
# excluindo database antigo.
|
|
info("Excluindo entradas antigas do banco destino.")
|
|
FlushCommand().handle(
|
|
database="default", interactive=False, verbosity=0, allow_cascade=True
|
|
)
|
|
|
|
# apaga tipos de autor padrão (criados no flush acima)
|
|
TipoAutor.objects.all().delete()
|
|
# tb apagamos os dados do reversion, p nao confundir apagados_pelo_usuario
|
|
Revision.objects.all().delete()
|
|
Version.objects.all().delete()
|
|
|
|
fill_vinculo_norma_juridica()
|
|
|
|
|
|
def migrar_dados(primeira_migracao=False, apagar_do_legado=False):
|
|
try:
|
|
# limpa tudo antes de migrar
|
|
_cached_get_all_ids_from_model.cache_clear()
|
|
get_pk_legado.cache_clear()
|
|
ocorrencias.clear()
|
|
ocorrencias.default_factory = list
|
|
|
|
# restaura dump
|
|
arq_dump = Path(
|
|
DIR_DADOS_MIGRACAO.child(
|
|
"dumps_mysql", "{}.sql".format(NOME_BANCO_LEGADO)
|
|
)
|
|
)
|
|
assert arq_dump.exists(), "Dump do mysql faltando: {}".format(arq_dump)
|
|
info("Restaurando dump mysql de [{}]".format(arq_dump))
|
|
normaliza_dump_mysql(arq_dump)
|
|
roda_comando_shell("mysql -uroot < {}".format(arq_dump))
|
|
|
|
# desliga checagens do mysql
|
|
# e possibilita inserir valor zero em campos de autoincremento
|
|
exec_legado('SET SESSION sql_mode = "NO_AUTO_VALUE_ON_ZERO";')
|
|
|
|
# executa ajustes pré-migração, se existirem
|
|
arq_ajustes_sql, arq_ajustes_reverter = (
|
|
get_arquivos_ajustes_pre_migracao()
|
|
)
|
|
if arq_ajustes_sql.exists():
|
|
exec_legado(arq_ajustes_sql.read_file())
|
|
if arq_ajustes_reverter.exists():
|
|
revert_delete_producao(yaml.load(arq_ajustes_reverter.read_file()))
|
|
|
|
uniformiza_banco(primeira_migracao)
|
|
|
|
if primeira_migracao:
|
|
do_flush()
|
|
criar_configuracao_inicial()
|
|
|
|
info("Começando migração: ...")
|
|
migrar_todos_os_models(apagar_do_legado)
|
|
except Exception as e:
|
|
ocorrencias["traceback"] = str(traceback.format_exc())
|
|
raise e
|
|
finally:
|
|
# congela e grava ocorrências
|
|
ocorrencias.default_factory = None
|
|
arq_ocorrencias = Path(REPO.working_dir, "ocorrencias.yaml")
|
|
with open(arq_ocorrencias, "w") as arq:
|
|
pyaml.dump(ocorrencias, arq, vspacing=1, width=200)
|
|
REPO.git.add([arq_ocorrencias.name])
|
|
info("Ocorrências salvas em\n {}".format(arq_ocorrencias))
|
|
if not ocorrencias:
|
|
info("NÃO HOUVE OCORRÊNCIAS !!!")
|
|
|
|
# recria tipos de autor padrão que não foram criados pela migração
|
|
cria_models_tipo_autor()
|
|
return ocorrencias.get("fk", [])
|
|
|
|
|
|
def move_para_depois_de(lista, movido, referencias):
|
|
indice_inicial = lista.index(movido)
|
|
lista.remove(movido)
|
|
indice_apos_refs = max(lista.index(r) for r in referencias) + 1
|
|
lista.insert(max(indice_inicial, indice_apos_refs), movido)
|
|
return lista
|
|
|
|
|
|
def existe_reuniao_no_legado():
|
|
return existe_tabela_no_legado("reuniao_comissao")
|
|
|
|
|
|
def get_models_a_migrar():
|
|
models = [
|
|
model
|
|
for app in appconfs
|
|
for model in app.models.values()
|
|
if model in field_renames
|
|
]
|
|
# retira reuniões quando não existe na base legada
|
|
# (só existe no sapl 3.0)
|
|
if not existe_reuniao_no_legado():
|
|
models.remove(Reuniao)
|
|
# Devido à referência TipoProposicao.tipo_conteudo_related
|
|
# a migração de TipoProposicao precisa ser feita
|
|
# após TipoMateriaLegislativa e TipoDocumento
|
|
# (porém antes de Proposicao)
|
|
move_para_depois_de(
|
|
models, TipoProposicao, [TipoMateriaLegislativa, TipoDocumento]
|
|
)
|
|
assert models.index(TipoProposicao) < models.index(Proposicao)
|
|
move_para_depois_de(
|
|
models, Proposicao, [MateriaLegislativa, DocumentoAcessorio]
|
|
)
|
|
|
|
return models
|
|
|
|
|
|
def migrar_todos_os_models(apagar_do_legado):
|
|
for model in get_models_a_migrar():
|
|
migrar_model(model, apagar_do_legado)
|
|
|
|
|
|
def migrar_model(model, apagar_do_legado):
|
|
print("Migrando %s..." % model.__name__)
|
|
|
|
model_legado, tabela_legado, campos_pk_legado = get_estrutura_legado(model)
|
|
|
|
if len(campos_pk_legado) == 1:
|
|
|
|
# A PK NO LEGADO TEM UM ÚNICO CAMPO
|
|
|
|
nome_pk = model_legado._meta.pk.name
|
|
if "ind_excluido" in {f.name for f in model_legado._meta.fields}:
|
|
# se o model legado tem o campo ind_excluido
|
|
# enumera apenas os não excluídos
|
|
old_records = model_legado.objects.filter(~Q(ind_excluido=1))
|
|
else:
|
|
old_records = model_legado.objects.all()
|
|
old_records = old_records.order_by(nome_pk)
|
|
|
|
def get_id_do_legado(old):
|
|
return getattr(old, nome_pk)
|
|
|
|
ids_ja_migrados = get_all_ids_from_model(model)
|
|
|
|
apagados_pelo_usuario = [
|
|
int(v.object_id) for v in Version.objects.get_deleted(model)
|
|
]
|
|
|
|
def ja_esta_migrado(old):
|
|
id = get_id_do_legado(old)
|
|
return id in ids_ja_migrados or id in apagados_pelo_usuario
|
|
|
|
ultima_pk_legado = (
|
|
model_legado.objects.all().aggregate(Max("pk"))["pk__max"] or 0
|
|
)
|
|
else:
|
|
|
|
# A PK NO LEGADO TEM MAIS DE UM CAMPO
|
|
|
|
old_records = iter_sql_records(tabela_legado)
|
|
get_id_do_legado = None
|
|
|
|
renames = field_renames[model]
|
|
campos_velhos_p_novos = {v: k for k, v in renames.items()}
|
|
|
|
if model_legado == Numeracao:
|
|
# nao usamos cod_numeracao no 3.1 => apelamos p todos os campos
|
|
campos_chave = [
|
|
"cod_materia",
|
|
"tip_materia",
|
|
"num_materia",
|
|
"ano_materia",
|
|
"dat_materia",
|
|
]
|
|
else:
|
|
campos_chave = campos_pk_legado
|
|
|
|
apagados_pelo_usuario = Version.objects.get_deleted(model)
|
|
apagados_pelo_usuario = [
|
|
{k: v for k, v in get_campos_crus_reversion(version).items()}
|
|
for version in apagados_pelo_usuario
|
|
]
|
|
campos_chave_novos = {campos_velhos_p_novos[c] for c in campos_chave}
|
|
apagados_pelo_usuario = [
|
|
{k: v for k, v in apagado.items() if k in campos_chave_novos}
|
|
for apagado in apagados_pelo_usuario
|
|
]
|
|
|
|
def ja_esta_migrado(old):
|
|
chave = {
|
|
campos_velhos_p_novos[c]: getattr(old, c) for c in campos_chave
|
|
}
|
|
return (
|
|
chave in apagados_pelo_usuario
|
|
or model.objects.filter(**chave).exists()
|
|
)
|
|
|
|
ultima_pk_legado = model_legado.objects.count()
|
|
|
|
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():
|
|
novos = []
|
|
sql_delete_legado = ""
|
|
for old in old_records:
|
|
if ja_esta_migrado(old):
|
|
# pulamos esse objeto, pois já foi migrado anteriormente
|
|
continue
|
|
new = model()
|
|
if get_id_do_legado:
|
|
new.id = get_id_do_legado(old)
|
|
try:
|
|
populate_renamed_fields(new, old)
|
|
if ajuste_antes_salvar:
|
|
ajuste_antes_salvar(new, old)
|
|
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("fk", e.msg, e.dados)
|
|
continue
|
|
else:
|
|
new.clean() # valida model
|
|
novos.append(new) # guarda para salvar
|
|
|
|
# acumula deleção do registro no legado
|
|
if apagar_do_legado:
|
|
sql_delete_legado += "delete from {} where {};\n".format(
|
|
tabela_legado,
|
|
" and ".join(
|
|
'{} = "{}"'.format(campo, getattr(old, campo))
|
|
for campo in campos_pk_legado
|
|
),
|
|
)
|
|
|
|
# salva novos registros
|
|
with reversion.create_revision():
|
|
model.objects.bulk_create(novos)
|
|
reversion.set_comment("Objetos criados pela migração")
|
|
|
|
if ajuste_depois_salvar:
|
|
ajuste_depois_salvar()
|
|
|
|
# reiniciamos a sequence logo após a última pk do legado
|
|
#
|
|
# É importante que seja do legado (e não da nova base),
|
|
# pois numa nova versão da migração podemos inserir registros
|
|
# não migrados antes sem conflito com pks criadas até lá
|
|
if get_id_do_legado:
|
|
reinicia_sequence(model, ultima_pk_legado)
|
|
|
|
# apaga registros migrados do legado
|
|
if apagar_do_legado and sql_delete_legado:
|
|
exec_legado(sql_delete_legado)
|
|
|
|
|
|
def get_campos_crus_reversion(version):
|
|
"""Pega campos crus de uma versao do django reversion
|
|
p evitar erros de deserialização"""
|
|
assert version.format == "json"
|
|
[meta] = json.loads(version.serialized_data)
|
|
campos = meta["fields"]
|
|
campos["id"] = meta["pk"]
|
|
return campos
|
|
|
|
|
|
def encontra_conflitos_tipo_autor():
|
|
# Encontrei conflito de ids em TipoAutor entre
|
|
# um registro que existiu na base e foi apagado e um registro ressuscitado.
|
|
# Então testamos p garantir que não associamos um ressuscitado erroneamente
|
|
|
|
from sapl.legacy.models import TipoAutor as TipoAutorLegado
|
|
|
|
atual = {t.id: t.descricao for t in TipoAutor.objects.all()}
|
|
historia = {
|
|
t.field_dict["id"]: t.field_dict["descricao"]
|
|
for t in Version.objects.get_deleted(TipoAutor)
|
|
}
|
|
assert not set(atual) & set(historia)
|
|
legado = {
|
|
t.pk: t.des_tipo_autor
|
|
for t in TipoAutorLegado.objects.filter(ind_excluido=False)
|
|
}
|
|
atual_e_historia = {**atual, **historia}
|
|
so_no_legado = set(legado.items()) - set(atual_e_historia.items())
|
|
|
|
# o que:
|
|
# 1) está so no legado
|
|
# 2) tem um id igual ao de um registro da base atual (incluindo histórico)
|
|
# 3) mas com uma descrição difente
|
|
conflitos = {
|
|
id: (
|
|
descricao_no_legado,
|
|
atual_e_historia[id],
|
|
"atual" if id in atual else "historia",
|
|
)
|
|
for id, descricao_no_legado in so_no_legado
|
|
if id in atual_e_historia
|
|
}
|
|
max_id = max(*atual_e_historia, *legado)
|
|
return conflitos, max_id
|
|
|
|
|
|
# MIGRATION_ADJUSTMENTS #####################################################
|
|
|
|
|
|
def adjust_acompanhamentomateria(new, old):
|
|
new.confirmado = True
|
|
|
|
|
|
def adjust_documentoadministrativo(new, old):
|
|
if old.num_protocolo:
|
|
numero, ano = old.num_protocolo, new.ano
|
|
# False < True => o primeiro será o protocolo não anulado
|
|
protocolos = Protocolo.objects.filter(numero=numero, ano=ano).order_by(
|
|
"anulado"
|
|
)
|
|
if protocolos:
|
|
new.protocolo = protocolos[0]
|
|
else:
|
|
# Se não achamos o protocolo registramos no número externo
|
|
new.numero_externo = numero
|
|
|
|
nota = """
|
|
## NOTA DE MIGRAÇÃO DE DADOS DO SAPL 2.5 ##
|
|
O número de protocolo original deste documento era [{numero}], ano [{ano}].
|
|
|
|
Não existe no sistema nenhum protocolo com estes dados
|
|
e portanto nenhum protocolo foi vinculado a este documento.
|
|
|
|
Colocamos então o número de protocolo no campo "número externo".
|
|
"""
|
|
nota = nota.strip().format(numero=numero, ano=ano)
|
|
msg = (
|
|
"Protocolo {numero} faltando (referenciado "
|
|
"no documento administrativo {cod_documento})"
|
|
)
|
|
warn(
|
|
"protocolo_faltando",
|
|
msg,
|
|
{
|
|
"numero": numero,
|
|
"cod_documento": old.cod_documento,
|
|
"nota": nota,
|
|
},
|
|
)
|
|
new.observacao += ("\n\n" if new.observacao else "") + nota
|
|
|
|
|
|
def adjust_mandato(new, old):
|
|
if old.dat_fim_mandato:
|
|
new.data_fim_mandato = old.dat_fim_mandato
|
|
if not new.data_fim_mandato:
|
|
legislatura = Legislatura.objects.latest("data_fim")
|
|
new.data_fim_mandato = legislatura.data_fim
|
|
new.data_expedicao_diploma = legislatura.data_inicio
|
|
if not new.data_inicio_mandato:
|
|
new.data_inicio_mandato = new.legislatura.data_inicio
|
|
new.data_fim_mandato = new.legislatura.data_fim
|
|
|
|
|
|
def adjust_ordemdia_antes_salvar(new, old):
|
|
new.votacao_aberta = False
|
|
|
|
if not old.tip_votacao:
|
|
new.tipo_votacao = 1
|
|
|
|
if old.num_ordem is None:
|
|
new.numero_ordem = 999999999
|
|
warn(
|
|
"ordem_dia_num_ordem_nulo",
|
|
"OrdemDia de PK {pk} tinha numero ordem nulo. "
|
|
"O valor %s foi colocado no lugar." % new.numero_ordem,
|
|
{"pk": old.pk},
|
|
)
|
|
|
|
|
|
def adjust_parlamentar(new, old):
|
|
if old.ind_unid_deliberativa:
|
|
value = new.unidade_deliberativa
|
|
# Field is defined as not null in legacy db,
|
|
# but data includes null values
|
|
# => transform None to False
|
|
if value is None:
|
|
warn(
|
|
"unidade_deliberativa_nulo_p_false",
|
|
"nulo convertido para falso na unidade_deliberativa "
|
|
"do parlamentar {pk_parlamentar}",
|
|
{"pk_parlamentar": old.cod_parlamentar},
|
|
)
|
|
new.unidade_deliberativa = False
|
|
# migra município de residência
|
|
if old.cod_localidade_resid:
|
|
municipio_uf = list(
|
|
exec_legado(
|
|
"""
|
|
select nom_localidade, sgl_uf from localidade
|
|
where cod_localidade = {}""".format(
|
|
old.cod_localidade_resid
|
|
)
|
|
)
|
|
)
|
|
if municipio_uf:
|
|
new.municipio_residencia, new.uf_residencia = municipio_uf[0]
|
|
|
|
|
|
def adjust_participacao(new, old):
|
|
comissao_id, periodo_id = [
|
|
get_fk_related(Composicao._meta.get_field(name), old)
|
|
for name in ("comissao", "periodo")
|
|
]
|
|
with reversion.create_revision():
|
|
composicao, _ = Composicao.objects.get_or_create(
|
|
comissao_id=comissao_id, periodo_id=periodo_id
|
|
)
|
|
reversion.set_comment("Objeto criado pela migração")
|
|
new.composicao = composicao
|
|
|
|
|
|
def adjust_normarelacionada(new, old):
|
|
new.tipo_vinculo = TipoVinculoNormaJuridica.objects.get(
|
|
sigla=old.tip_vinculo
|
|
)
|
|
|
|
|
|
def adjust_protocolo_antes_salvar(new, old):
|
|
if new.numero is None:
|
|
new.numero = old.cod_protocolo
|
|
warn(
|
|
"num_protocolo_nulo",
|
|
"Número do protocolo de PK {cod_protocolo} era nulo "
|
|
"e foi alterado para sua pk ({cod_protocolo})",
|
|
{"cod_protocolo": old.cod_protocolo},
|
|
)
|
|
|
|
|
|
def get_arquivo_resolve_registro_votacao():
|
|
return DIR_DADOS_MIGRACAO.child(
|
|
"ajustes_pre_migracao",
|
|
"{}_resolve_registro_votacao_ambiguo.yaml".format(SIGLA_CASA),
|
|
)
|
|
|
|
|
|
def get_como_resolver_registro_votacao_ambiguo():
|
|
path = get_arquivo_resolve_registro_votacao()
|
|
if path.exists():
|
|
return yaml.load(path.read_file())
|
|
else:
|
|
return {}
|
|
|
|
|
|
def adjust_registrovotacao_antes_salvar(new, old):
|
|
ordem_dia = OrdemDia.objects.filter(
|
|
pk=old.cod_ordem, materia=old.cod_materia
|
|
)
|
|
expediente_materia = ExpedienteMateria.objects.filter(
|
|
pk=old.cod_ordem, materia=old.cod_materia
|
|
)
|
|
|
|
if ordem_dia and not expediente_materia:
|
|
new.ordem = ordem_dia[0]
|
|
if not ordem_dia and expediente_materia:
|
|
new.expediente = expediente_materia[0]
|
|
# registro de votação ambíguo
|
|
if ordem_dia and expediente_materia:
|
|
como_resolver = get_como_resolver_registro_votacao_ambiguo()
|
|
campo = como_resolver[new.id]
|
|
if campo.startswith("ordem"):
|
|
new.ordem = ordem_dia[0]
|
|
elif campo.startswith("expediente"):
|
|
new.expediente = expediente_materia[0]
|
|
else:
|
|
raise Exception(
|
|
"""
|
|
Registro de Votação ambíguo: {}
|
|
Resolva criando o arquivo {}""".format(
|
|
new.id, get_arquivo_resolve_registro_votacao()
|
|
)
|
|
)
|
|
|
|
|
|
def adjust_tipoafastamento(new, old):
|
|
assert xor(old.ind_afastamento, old.ind_fim_mandato)
|
|
if old.ind_afastamento:
|
|
new.indicador = "A"
|
|
elif old.ind_fim_mandato:
|
|
new.indicador = "F"
|
|
|
|
|
|
def set_generic_fk(new, campo_virtual, old):
|
|
model = campo_virtual.related_model
|
|
new.content_type = ContentType.objects.get(
|
|
app_label=model._meta.app_label, model=model._meta.model_name
|
|
)
|
|
new.object_id = get_fk_related(campo_virtual, old)
|
|
|
|
|
|
def adjust_tipoproposicao(new, old):
|
|
"Aponta para o tipo relacionado de matéria ou documento"
|
|
if old.tip_mat_ou_doc is not None:
|
|
campo_virtual = CAMPOS_VIRTUAIS_TIPO_PROPOSICAO[old.ind_mat_ou_doc]
|
|
set_generic_fk(new, campo_virtual, old)
|
|
|
|
|
|
def adjust_proposicao_antes_salvar(new, old):
|
|
if new.data_envio:
|
|
new.ano = new.data_envio.year
|
|
if old.cod_mat_ou_doc is not None:
|
|
tipo_mat_ou_doc = type(new.tipo.tipo_conteudo_related)
|
|
campo_virtual = CAMPOS_VIRTUAIS_PROPOSICAO[tipo_mat_ou_doc]
|
|
set_generic_fk(new, campo_virtual, old)
|
|
|
|
|
|
def adjust_statustramitacao(new, old):
|
|
if old.ind_fim_tramitacao:
|
|
new.indicador = "F"
|
|
elif old.ind_retorno_tramitacao:
|
|
new.indicador = "R"
|
|
else:
|
|
new.indicador = ""
|
|
|
|
|
|
def adjust_statustramitacaoadm(new, old):
|
|
adjust_statustramitacao(new, old)
|
|
|
|
|
|
def adjust_tramitacao(new, old):
|
|
if old.sgl_turno == "Ú":
|
|
new.turno = "U"
|
|
|
|
|
|
def adjust_tipo_autor(new, old):
|
|
model_apontado = normalize(new.descricao.lower()).replace(" ", "")
|
|
content_types = ContentType.objects.filter(model=model_apontado).exclude(
|
|
app_label="legacy"
|
|
)
|
|
assert len(content_types) <= 1
|
|
new.content_type = content_types[0] if content_types else None
|
|
|
|
|
|
def adjust_normajuridica_antes_salvar(new, old):
|
|
# Ajusta choice de esfera_federacao
|
|
# O 'S' vem de 'Selecionar'. Na versão antiga do SAPL, quando uma opção do
|
|
# combobox era selecionada, o sistema pegava a primeira letra da seleção,
|
|
# sendo F para Federal, E para Estadual, M para Municipal e o S para
|
|
# Selecionar, que era a primeira opção quando nada era selecionado.
|
|
if old.tip_esfera_federacao == "S":
|
|
new.esfera_federacao = ""
|
|
|
|
|
|
def adjust_normajuridica_depois_salvar():
|
|
# Ajusta relação M2M
|
|
ligacao = NormaJuridica.assuntos.through
|
|
|
|
assuntos_migrados, normas_migradas = [
|
|
set(model.objects.values_list("id", flat=True))
|
|
for model in [AssuntoNorma, NormaJuridica]
|
|
]
|
|
|
|
def filtra_assuntos_migrados(cod_assunto):
|
|
"""cod_assunto é uma string com cods separados por vírgulas
|
|
p. ex.: 1,2,3,99
|
|
"""
|
|
if not cod_assunto:
|
|
return []
|
|
cods = {int(a) for a in cod_assunto.split(",") if a}
|
|
return sorted(cods.intersection(assuntos_migrados))
|
|
|
|
old_normajurica_cod_assuntos = OldNormaJuridica.objects.filter(
|
|
pk__in=normas_migradas
|
|
).values_list("pk", "cod_assunto")
|
|
ja_migrados = set(
|
|
ligacao.objects.values_list("normajuridica_id", "assuntonorma_id")
|
|
)
|
|
|
|
normas_assuntos = [
|
|
(norma, assunto)
|
|
for norma, cod_assunto in old_normajurica_cod_assuntos
|
|
for assunto in filtra_assuntos_migrados(cod_assunto)
|
|
if (norma, assunto) not in ja_migrados
|
|
]
|
|
|
|
ligacao.objects.bulk_create(
|
|
ligacao(normajuridica_id=norma, assuntonorma_id=assunto)
|
|
for norma, assunto in normas_assuntos
|
|
)
|
|
|
|
|
|
def adjust_autor(new, old):
|
|
# vincula autor com o objeto relacionado, tentando os três campos antigos
|
|
# o primeiro campo preenchido será usado, podendo lançar ForeignKeyFaltando
|
|
for model_relacionado, campo_nome in [
|
|
(Parlamentar, "nome_parlamentar"),
|
|
(Comissao, "nome"),
|
|
(Partido, "nome"),
|
|
]:
|
|
field = CAMPOS_VIRTUAIS_AUTOR[model_relacionado]
|
|
fk_encontrada = get_fk_related(field, old)
|
|
if fk_encontrada:
|
|
new.autor_related = model_relacionado.objects.get(id=fk_encontrada)
|
|
new.nome = getattr(new.autor_related, campo_nome)
|
|
break
|
|
|
|
if old.col_username:
|
|
user, created = get_user_model().objects.get_or_create(
|
|
username=old.col_username
|
|
)
|
|
if created:
|
|
# gera uma senha inutilizável, que precisará ser trocada
|
|
user.set_password(None)
|
|
with reversion.create_revision():
|
|
user.save()
|
|
reversion.set_comment(
|
|
"Usuário criado pela migração para o autor {}".format(
|
|
old.cod_autor
|
|
)
|
|
)
|
|
grupo_autor = Group.objects.get(name="Autor")
|
|
user.groups.add(grupo_autor)
|
|
new.user = user
|
|
|
|
|
|
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
|
|
):
|
|
new.ativa = True
|
|
else:
|
|
new.ativa = False
|
|
|
|
|
|
def adjust_tiporesultadovotacao(new, old):
|
|
if "aprova" in new.nome.lower():
|
|
new.natureza = TipoResultadoVotacao.NATUREZA_CHOICES.aprovado
|
|
elif "rejeita" in new.nome.lower():
|
|
new.natureza = TipoResultadoVotacao.NATUREZA_CHOICES.rejeitado
|
|
elif "retirado" in new.nome.lower():
|
|
new.natureza = TipoResultadoVotacao.NATUREZA_CHOICES.rejeitado
|
|
else:
|
|
if new.nome != "DESCONHECIDO":
|
|
# ignoramos a natureza de item criado pela migração
|
|
warn(
|
|
"natureza_desconhecida_tipo_resultadovotacao",
|
|
"Não foi possível identificar a natureza do "
|
|
'tipo de resultado de votação [{pk}: "{nome}"]',
|
|
{"pk": new.pk, "nome": new.nome},
|
|
)
|
|
|
|
|
|
def str_to_time(fonte):
|
|
if not fonte.strip():
|
|
return None
|
|
tempo = datetime.datetime.strptime(fonte, "%H:%M")
|
|
return tempo.time() if tempo else None
|
|
|
|
|
|
def adjust_reuniao_comissao(new, old):
|
|
new.hora_inicio = str_to_time(old.hr_inicio_reuniao)
|
|
|
|
|
|
def remove_style(conteudo):
|
|
if "style" not in conteudo:
|
|
return conteudo # atalho que acelera muito os casos sem style
|
|
|
|
soup = BeautifulSoup(conteudo, "html.parser")
|
|
for tag in soup.recursiveChildGenerator():
|
|
if hasattr(tag, "attrs"):
|
|
tag.attrs = {k: v for k, v in tag.attrs.items() if k != "style"}
|
|
return str(soup)
|
|
|
|
|
|
def adjust_expediente_sessao(new, old):
|
|
new.conteudo = remove_style(new.conteudo)
|
|
|
|
|
|
AJUSTE_ANTES_SALVAR = {
|
|
Autor: adjust_autor,
|
|
TipoAutor: adjust_tipo_autor,
|
|
AcompanhamentoMateria: adjust_acompanhamentomateria,
|
|
Comissao: adjust_comissao,
|
|
DocumentoAdministrativo: adjust_documentoadministrativo,
|
|
Mandato: adjust_mandato,
|
|
NormaJuridica: adjust_normajuridica_antes_salvar,
|
|
NormaRelacionada: adjust_normarelacionada,
|
|
OrdemDia: adjust_ordemdia_antes_salvar,
|
|
Parlamentar: adjust_parlamentar,
|
|
Participacao: adjust_participacao,
|
|
Proposicao: adjust_proposicao_antes_salvar,
|
|
Protocolo: adjust_protocolo_antes_salvar,
|
|
RegistroVotacao: adjust_registrovotacao_antes_salvar,
|
|
TipoAfastamento: adjust_tipoafastamento,
|
|
TipoProposicao: adjust_tipoproposicao,
|
|
StatusTramitacao: adjust_statustramitacao,
|
|
StatusTramitacaoAdministrativo: adjust_statustramitacaoadm,
|
|
Tramitacao: adjust_tramitacao,
|
|
TipoResultadoVotacao: adjust_tiporesultadovotacao,
|
|
ExpedienteSessao: adjust_expediente_sessao,
|
|
Reuniao: adjust_reuniao_comissao,
|
|
}
|
|
|
|
AJUSTE_DEPOIS_SALVAR = {NormaJuridica: adjust_normajuridica_depois_salvar}
|
|
|
|
|
|
# MARCO ######################################################################
|
|
|
|
TIME_FORMAT = "%H:%M:%S"
|
|
|
|
|
|
# permite a gravação de tempos puros pelo pretty-yaml
|
|
def time_representer(dumper, data):
|
|
return dumper.represent_scalar("!time", data.strftime(TIME_FORMAT))
|
|
|
|
|
|
UnsafePrettyYAMLDumper.add_representer(datetime.time, time_representer)
|
|
|
|
|
|
# permite a leitura de tempos puros pelo pyyaml (no padrão gravado acima)
|
|
def time_constructor(loader, node):
|
|
value = loader.construct_scalar(node)
|
|
return datetime.datetime.strptime(value, TIME_FORMAT).time()
|
|
|
|
|
|
yaml.add_constructor("!time", time_constructor)
|
|
|
|
TAG_MARCO = "marco"
|
|
|
|
|
|
def gravar_marco(
|
|
nome_dir="dados", pula_se_ja_existe=False, versiona=True, gera_backup=True
|
|
):
|
|
"""Grava um dump de todos os dados como arquivos yaml no repo de marco
|
|
"""
|
|
# prepara ou localiza repositorio
|
|
dir_dados = Path(REPO.working_dir, nome_dir)
|
|
if pula_se_ja_existe and dir_dados.exists():
|
|
return
|
|
# limpa todo o conteúdo antes
|
|
dir_dados.rmtree()
|
|
dir_dados.mkdir()
|
|
|
|
# exporta dados como arquivos yaml
|
|
user_model = get_user_model()
|
|
models = get_models_a_migrar() + [
|
|
Composicao,
|
|
user_model,
|
|
Group,
|
|
ContentType,
|
|
]
|
|
sequences = []
|
|
for model in models:
|
|
info(f"Gravando marco de [{model.__name__}]")
|
|
dir_model = dir_dados.child(model._meta.app_label, model.__name__)
|
|
dir_model.mkdir(parents=True)
|
|
for data in model.objects.all().values():
|
|
nome_arq = Path(dir_model, f"{data['id']}.yaml")
|
|
with open(nome_arq, "w") as arq:
|
|
pyaml.dump(data, arq)
|
|
sequences.append(get_sequence_name_and_last_value(model))
|
|
# grava valores das seqeunces
|
|
sequences = dict(sorted(sequences))
|
|
Path(dir_dados, "sequences.yaml").write_file(pyaml.dump(sequences))
|
|
|
|
# backup do banco
|
|
if gera_backup:
|
|
print("Gerando backup do banco... ", end="", flush=True)
|
|
arq_backup = DIR_REPO.child("{}.backup".format(NOME_BANCO_LEGADO))
|
|
arq_backup.remove()
|
|
backup_cmd = f"""
|
|
pg_dump --host localhost --port 5432 --username postgres
|
|
--no-password --format custom --blobs --verbose --file
|
|
{arq_backup} {NOME_BANCO_LEGADO}"""
|
|
subprocess.check_output(backup_cmd.split(), stderr=subprocess.DEVNULL)
|
|
print("SUCESSO")
|
|
|
|
# versiona mudanças
|
|
if versiona:
|
|
REPO.git.add([dir_dados.name])
|
|
if gera_backup:
|
|
REPO.git.add([arq_backup.name])
|
|
if "master" not in REPO.heads or REPO.index.diff("HEAD"):
|
|
# se de fato existe mudança
|
|
REPO.index.commit(f"Grava marco (em {nome_dir})")
|
|
REPO.git.execute("git tag -f".split() + [TAG_MARCO])
|
|
|
|
|
|
def encode_version(version):
|
|
# version.id seria suficiente
|
|
# os campos reduntandes servem como conferência tanto visual
|
|
# como de consistencia da restauracao posterior
|
|
return {
|
|
"id": version.id,
|
|
"content_type__model": version.content_type.model,
|
|
"object_id": version.object_id,
|
|
}
|
|
|
|
|
|
def get_apagados_que_geram_ocorrencias_fk(fks_faltando):
|
|
def get_tabela_legado_do_model(model):
|
|
_, tabela_legado, _ = get_estrutura_legado(model)
|
|
return tabela_legado
|
|
|
|
tabela_legado_p_model = {
|
|
get_tabela_legado_do_model(model): model
|
|
for model in get_models_a_migrar()
|
|
}
|
|
|
|
apagados = set()
|
|
for fk in fks_faltando:
|
|
model_dependente = tabela_legado_p_model[fk["tabela"]]
|
|
# não funciona para models em que o mapeamento de campos nao é direto
|
|
if model_dependente in (Participacao, Autor):
|
|
model_relacionado = {
|
|
"cod_comissao": Comissao,
|
|
"cod_parlamentar": Parlamentar,
|
|
}[fk["campo"]]
|
|
elif model_dependente == TipoProposicao:
|
|
ind_mat_ou_doc = list(exec_legado(fk["sql"]))[0][2]
|
|
model_relacionado = CAMPOS_VIRTUAIS_TIPO_PROPOSICAO[
|
|
ind_mat_ou_doc
|
|
].related_model
|
|
else:
|
|
nome_campo_fk = {
|
|
v: k for k, v in field_renames[model_dependente].items()
|
|
}[fk["campo"]]
|
|
campo_fk = model_dependente._meta.get_field(nome_campo_fk)
|
|
model_relacionado = campo_fk.related_model
|
|
|
|
_, tabela_relacionada, [campo_pk] = get_estrutura_legado(
|
|
model_relacionado
|
|
)
|
|
deleted = Version.objects.get_deleted(model_relacionado)
|
|
versions = deleted.filter(object_id=fk["valor"])
|
|
if versions:
|
|
[version] = versions # se há, deve ser único
|
|
apagados.add((tabela_relacionada, campo_pk, version))
|
|
# XXX poderíamos gerar aqui os parlementares apontados por autor
|
|
# para listar como restaurado para o usuário
|
|
# ... mas já está demais
|
|
# nós restauramos no método abaixo, mesmo sem o feedback desse detalhe
|
|
return [(*_, encode_version(version)) for *_, version in apagados]
|
|
|
|
|
|
def revert_delete_producao(dados_versions):
|
|
if not dados_versions:
|
|
return
|
|
print("Revertendo registros apagados em produção...")
|
|
for dados in dados_versions:
|
|
print(dados)
|
|
version = Version.objects.get(**dados)
|
|
version.revert()
|
|
reverted = version.object
|
|
assert reverted
|
|
# restauramos objetos relacinados ao autor
|
|
# teoricamente precisaríamos fazer isso pra todas as generic relations
|
|
if isinstance(reverted, Autor) and reverted.content_type:
|
|
apagados_relacionados = Version.objects.get_deleted(
|
|
reverted.content_type.model_class()
|
|
)
|
|
rel = apagados_relacionados.filter(object_id=reverted.object_id)
|
|
if rel:
|
|
[rel] = rel
|
|
rel.revert()
|
|
assert reverted.autor_related
|
|
assert reverted.autor_related == rel.object
|
|
|
|
print("... sucesso")
|
|
|
|
|
|
# UTILS
|
|
|
|
|
|
def porids(model):
|
|
return {o.id: o for o in model.objects.all()}
|
|
|
|
|
|
def deletados(model):
|
|
deletados = Version.objects.get_deleted(model)
|
|
return {v.object_id: v for v in deletados}
|
|
|
|
|
|
def get_conflitos_materias_legado_e_producao():
|
|
"""
|
|
Analisa conflitos entre materias nao migradas e em producao
|
|
"""
|
|
res = list(
|
|
exec_legado(
|
|
"""
|
|
select cod_materia, tip_id_basica, num_ident_basica, ano_ident_basica
|
|
from materia_legislativa where ind_excluido <> 1"""
|
|
)
|
|
)
|
|
materias_legado = {(t, n, a): id for id, t, n, a in res}
|
|
materias_producao = {
|
|
(m.tipo_id, m.numero, m.ano): m.id
|
|
for m in MateriaLegislativa.objects.all()
|
|
}
|
|
comuns = set(materias_legado) & set(materias_producao)
|
|
comuns = {k: (materias_legado[k], materias_producao[k]) for k in comuns}
|
|
conflitos = {
|
|
k: (id_legado, id_producao)
|
|
for k, (id_legado, id_producao) in comuns.items()
|
|
if id_legado != id_producao
|
|
}
|
|
return conflitos
|
|
|