From 4ef092b19fbb504fad389681f3b3f04ca89f6886 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Wed, 28 Feb 2018 17:33:41 -0300 Subject: [PATCH 01/18] =?UTF-8?q?Reorganiza=20c=C3=B3digo=20de=20migra?= =?UTF-8?q?=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao.py | 1160 +---------------- sapl/legacy/migracao_dados.py | 1155 ++++++++++++++++ sapl/legacy/migracao_documentos.py | 2 +- sapl/legacy/migracao_usuarios.py | 2 +- sapl/legacy/scripts/migra_um_db.sh | 6 +- sapl/legacy/scripts/scrap_original_forms.py | 2 +- sapl/legacy/scripts/study.py | 2 +- sapl/legacy/scripts/utils.py | 2 +- ...st_migration.py => test_migracao_dados.py} | 5 +- sapl/legacy/test_renames.py | 2 +- 10 files changed, 1174 insertions(+), 1164 deletions(-) create mode 100644 sapl/legacy/migracao_dados.py rename sapl/legacy/{test_migration.py => test_migracao_dados.py} (91%) diff --git a/sapl/legacy/migracao.py b/sapl/legacy/migracao.py index b228acbc1..a8fed932f 100644 --- a/sapl/legacy/migracao.py +++ b/sapl/legacy/migracao.py @@ -1,1155 +1,9 @@ -import os -import re -from datetime import date -from functools import lru_cache, partial -from itertools import groupby -from subprocess import PIPE, call +from sapl.legacy.migracao_dados import migrar_dados +from sapl.legacy.migracao_documentos import migrar_documentos +from sapl.legacy.migracao_usuarios import migrar_usuarios -import pkg_resources -import reversion -import yaml -from django.apps import apps -from django.apps.config import AppConfig -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.db import connections, transaction -from django.db.models import Count, Max -from django.db.models.base import ModelBase -from pytz import timezone -from sapl.base.models import AppConfig as AppConf -from sapl.base.models import (Autor, ProblemaMigracao, TipoAutor, - cria_models_tipo_autor) -from sapl.comissoes.models import Comissao, Composicao, Participacao -from sapl.legacy.models import TipoNumeracaoProtocolo -from sapl.materia.models import (AcompanhamentoMateria, 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, OrdemDia, RegistroVotacao -from sapl.settings import DATABASES, PROJECT_DIR -from sapl.utils import normalize - -from .timezonesbrasil import get_timezone - -# BASE ###################################################################### -# apps to be migrated, in app dependency order (very important) -appconfs = [apps.get_app_config(n) for n in [ - 'parlamentares', - 'comissoes', - 'base', - 'materia', - 'norma', - 'sessao', - 'lexml', - 'protocoloadm', ]] - -unique_constraints = [] -one_to_one_constraints = [] -primeira_vez = [] - -name_sets = [set(m.__name__ for m in ac.get_models()) for ac in appconfs] - -# apps do not overlap -for s1 in name_sets: - for s2 in name_sets: - if s1 is not s2: - assert not s1.intersection(s2) - -# apps include all legacy models -legacy_app = apps.get_app_config('legacy') -legacy_model_names = set(m.__name__ for m in legacy_app.get_models()) - -model_dict = {m.__name__: m for ac in appconfs for m in ac.get_models()} - - -# RENAMES ################################################################### - -MODEL_RENAME_PATTERN = re.compile('(.+) \((.+)\)') - - -def get_renames(): - field_renames = {} - model_renames = {} - 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(): - match = MODEL_RENAME_PATTERN.match(model_name) - if match: - model_name, old_name = match.groups() - else: - old_name = None - model = getattr(app.models_module, model_name) - if old_name: - model_renames[model] = old_name - field_renames[model] = renames - - # collect renames from parent classes - for model, renames in field_renames.items(): - if any(parent in field_renames for parent in model.__mro__[1:]): - renames = {} - for parent in reversed(model.__mro__): - if parent in field_renames: - renames.update(field_renames[parent]) - field_renames[model] = renames - - # remove abstract classes - field_renames = {m: r for m, r in field_renames.items() - if not m._meta.abstract} - - return field_renames, model_renames - -# MIGRATION ################################################################# - - -def info(msg): - print('INFO: ' + msg) - - -def warn(msg): - print('CUIDADO! ' + msg) - - -class ForeignKeyFaltando(ObjectDoesNotExist): - 'Uma FK aponta para um registro inexistente' - - def __init__(self, msg=''): - self.msg = msg - - -@lru_cache() -def _get_all_ids_from_model(model): - # esta função para uso apenas em get_fk_related - return set(model.objects.values_list('id', flat=True)) - - -def get_fk_related(field, value, label=None): - if value is None and field.null: - return None - - # if field.related_model.objects.filter(id=value).exists(): - if value in _get_all_ids_from_model(field.related_model): - return value - elif value == 0 and field.null: - # consideramos zeros como nulos, se não está entre os ids anteriores - return None - else: - msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % ( - field.name, value, field.model.__name__, label or '---') - warn(msg) - raise ForeignKeyFaltando(msg) - - -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 = [ - # , - ('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) - - -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) - - # 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) - 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) - 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) - - -PROPAGACOES_DE_EXCLUSAO = [ - # sessao_legislativa - ('composicao_mesa', 'sessao_legislativa', 'cod_sessao_leg'), - - # parlamentar - ('dependente', 'parlamentar', 'cod_parlamentar'), - ('filiacao', 'parlamentar', 'cod_parlamentar'), - ('mandato', 'parlamentar', 'cod_parlamentar'), - - # comissao - ('composicao_comissao', 'comissao', 'cod_comissao'), - - # sessao - ('ordem_dia', 'sessao_plenaria', 'cod_sessao_plen'), - ('expediente_materia', 'sessao_plenaria', 'cod_sessao_plen'), - ('expediente_sessao_plenaria', 'sessao_plenaria', 'cod_sessao_plen'), - ('registro_votacao_parlamentar', 'registro_votacao', 'cod_votacao'), - # 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... - ('registro_votacao', 'materia_legislativa', '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 - ('tramitacao', 'materia_legislativa', 'cod_materia'), - ('autoria', 'materia_legislativa', 'cod_materia'), - ('anexada', 'materia_legislativa', 'cod_materia_principal'), - ('anexada', 'materia_legislativa', 'cod_materia_anexada'), - ('documento_acessorio', 'materia_legislativa', 'cod_materia'), - - # documento administrativo - ('tramitacao_administrativo', 'documento_administrativo', 'cod_documento'), -] - - -def propaga_exclusoes(): - for tabela_filha, tabela_pai, fk in PROPAGACOES_DE_EXCLUSAO: - [pk_pai] = get_pk_legado(tabela_pai) - exec_legado(''' - update {} set ind_excluido = 1 where {} not in ( - select {} from {} where ind_excluido != 1) - '''.format(tabela_filha, fk, pk_pai, tabela_pai)) - - -def uniformiza_banco(): - exec_legado('SET SESSION sql_mode = "";') # desliga checagens do mysql - - checa_registros_votacao_ambiguos_e_remove_nao_usados() - propaga_exclusoes() - - 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') - - # 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 - '''.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') - - # é 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() - - -def iter_sql_records(sql): - class Record: - pass - 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 save_relation(obj, nome_campo='', problema='', descricao='', - eh_stub=False, critico=False): - link = ProblemaMigracao( - content_object=obj, nome_campo=nome_campo, problema=problema, - descricao=descricao, eh_stub=eh_stub, critico=critico) - link.save() - - -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 fill_dados_basicos(): - # Ajusta sequencia numérica e cria base.AppConfig - letra = 'A' - try: - tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo') - if 'POR ANO' in tipo.des_numeracao_protocolo: - letra = 'A' - elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo: - letra = 'L' - elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo: - letra = 'U' - except Exception as e: - pass - appconf = AppConf(sequencia_numeracao=letra) - appconf.save() - - -# Uma anomalia no sapl 2.5 causa a duplicação de registros de votação. -# Essa duplicação deve ser eliminada para que não haja erro no sapl 3.1 -def excluir_registrovotacao_duplicados(): - duplicatas_ids = RegistroVotacao.objects.values( - 'materia', 'ordem', 'expediente').annotate( - Count('id')).order_by().filter(id__count__gt=1) - duplicatas_queryset = RegistroVotacao.objects.filter( - materia__in=[item['materia'] for item in duplicatas_ids]) - - for dup in duplicatas_queryset: - lista_dups = duplicatas_queryset.filter( - materia=dup.materia, expediente=dup.expediente, ordem=dup.ordem) - primeiro_registro = lista_dups[0] - lista_dups = lista_dups.exclude(pk=primeiro_registro.pk) - for objeto in lista_dups: - if (objeto.pk > primeiro_registro.pk): - try: - objeto.delete() - except: - assert 0 - else: - try: - primeiro_registro.delete() - primeiro_registro = objeto - except: - assert 0 - - -def get_last_pk(model): - last_value = model.objects.all().aggregate(Max('pk')) - return last_value['pk__max'] or 0 - - -def reinicia_sequence(model, id): - sequence_name = '%s_id_seq' % model._meta.db_table - exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % ( - 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): - self.field_renames, self.model_renames = get_renames() - self.choice_valida = {} - - # configura timezone de migração - nome_legado = DATABASES['legacy']['NAME'] - match = re.match('sapl_cm_(.*)', nome_legado) - sigla_casa = match.group(1) - with open(os.path.expanduser('~/sapl_dumps/tabela_timezones.yaml'), 'r') as arq: - tabela_timezones = yaml.load(arq) - municipio, uf, nome_timezone = tabela_timezones[sigla_casa] - if nome_timezone: - self.timezone = timezone(nome_timezone) - else: - self.timezone = get_timezone(municipio, uf) - - def populate_renamed_fields(self, new, old): - renames = self.field_renames[type(new)] - - for field in new._meta.fields: - old_field_name = renames.get(field.name) - field_type = field.get_internal_type() - if old_field_name: - old_value = getattr(old, old_field_name) - - if field_type == 'ForeignKey': - # not necessarily a model - if hasattr(old, '_meta') and old._meta.pk.name != 'id': - label = old.pk - else: - label = '-- SEM PK --' - fk_field_name = '{}_id'.format(field.name) - value = get_fk_related(field, old_value, label) - setattr(new, fk_field_name, value) - else: - value = getattr(old, old_field_name) - - if (field_type in ['CharField', 'TextField'] - and value in [None, 'None']): - value = '' - - # adiciona timezone faltante aos campos com tempo - # os campos TIMESTAMP do mysql são gravados em UTC - # os DATETIME e TIME não têm timezone - def campo_tempo_sem_timezone(tipo): - return (field_type == tipo - and value and not value.tzinfo) - if campo_tempo_sem_timezone('DateTimeField'): - value = self.timezone.localize(value) - if campo_tempo_sem_timezone('TimeField'): - value = value.replace(tzinfo=self.timezone) - - setattr(new, field.name, value) - - def migrar(self, obj=appconfs, interativo=True): - # warning: model/app migration order is of utmost importance - - uniformiza_banco() - - # excluindo database antigo. - if interativo: - info('Todos os dados do banco serão excluidos. ' - 'Recomendamos que faça backup do banco sapl ' - 'antes de continuar.') - info('Deseja continuar? [s/n]') - resposta = input() - if resposta.lower() in ['s', 'sim', 'y', 'yes']: - pass - else: - info('Migração cancelada.') - return 0 - info('Excluindo entradas antigas do banco.') - call([PROJECT_DIR.child('manage.py'), 'flush', - '--database=default', '--no-input'], stdout=PIPE) - - # apaga tipos de autor padrão (criados no flush acima) - TipoAutor.objects.all().delete() - - fill_vinculo_norma_juridica() - fill_dados_basicos() - info('Começando migração: %s...' % obj) - self._do_migrate(obj) - - # 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() - - def _do_migrate(self, obj): - if isinstance(obj, AppConfig): - models_to_migrate = (model for model in obj.models.values() - if model in self.field_renames) - self._do_migrate(models_to_migrate) - elif isinstance(obj, ModelBase): - # A migração vai pular TipoProposicao e só vai migrar essa model - # antes de migrar Proposicao. Isso deve acontecer por causa da - # GenericRelation existente em TipoProposicao. - if not obj.__name__ == 'TipoProposicao': - if obj.__name__ == 'Proposicao': - self.migrate_model(TipoProposicao) - self.migrate_model(obj) - elif hasattr(obj, '__iter__'): - for item in obj: - self._do_migrate(item) - else: - raise TypeError( - 'Parameter must be a Model, AppConfig or a sequence of them') - - def migrate_model(self, model): - print('Migrando %s...' % model.__name__) - - 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) - - 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) - - def get_id_do_legado(old): - return getattr(old, nome_pk) - else: - # 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 - continue - new = model() - try: - self.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(e.msg) - continue - else: - 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) - - # 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 migrar(obj=appconfs, interativo=True): - dm = DataMigrator() - dm.migrar(obj, interativo) - - -# MIGRATION_ADJUSTMENTS ##################################################### - -def adjust_acompanhamentomateria(new, old): - new.confirmado = True - - -NOTA_DOCADM = ''' -## NOTA DE MIGRAÇÃO DE DADOS DO SAPL 2.5 ## -O número de protocolo original deste documento era [{num_protocolo}], ano {ano_original}. -'''.strip() # noqa - - -def adjust_documentoadministrativo(new, old): - if old.num_protocolo: - nota = None - ano_original = new.ano - protocolo = Protocolo.objects.filter( - numero=old.num_protocolo, ano=new.ano) - if not protocolo: - # tentamos encontrar o protocolo no ano seguinte - ano_novo = ano_original + 1 - protocolo = Protocolo.objects.filter(numero=old.num_protocolo, - ano=ano_novo) - if protocolo: - nota = NOTA_DOCADM + ''' -O protocolo vinculado é o de mesmo número, porém do ano seguinte ({ano_novo}), -pois não existe protocolo no sistema com este número no ano {ano_original}. -''' - nota = nota.strip().format(num_protocolo=old.num_protocolo, - ano_original=ano_original, - ano_novo=ano_novo) - warn('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! ' - 'DocumentoAdministrativo: {}, numero_protocolo: {}, ' - 'ano doc adm: {}'.format( - old.cod_documento, old.num_protocolo, ano_original)) - else: - nota = NOTA_DOCADM + ''' -Não existe no sistema nenhum protocolo com estes dados -e portanto nenhum protocolo foi vinculado a este documento.''' - nota = nota.format( - num_protocolo=old.num_protocolo, - ano_original=ano_original) - 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 - # adiciona nota ao final da observação - if 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 - - -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' \ - ' nulo.' % old.pk - descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem - warn(problema + ' => ' + descricao) - save_relation(obj=new, problema=problema, - descricao=descricao, eh_stub=False) - reversion.set_comment('OrdemDia sem número da ordem.') - - -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('nulo convertido para falso') - 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): - composicao = Composicao() - composicao.comissao_id, composicao.periodo_id = [ - get_fk_related(Composicao._meta.get_field(name), value) - for name, value in (('comissao', old.cod_comissao), - ('periodo', old.cod_periodo_comp))] - # check if there is already an "equal" one in the db - already_created = Composicao.objects.filter( - comissao=composicao.comissao, periodo=composicao.periodo) - if already_created: - assert len(already_created) == 1 # we must never have made 2 copies - [composicao] = already_created - else: - with reversion.create_revision(): - composicao.save() - reversion.set_comment('Objeto criado pela migração') - new.composicao = composicao - - -def adjust_proposicao_antes_salvar(new, old): - if new.data_envio: - new.ano = new.data_envio.year - - -def adjust_normarelacionada(new, old): - tipo = TipoVinculoNormaJuridica.objects.filter(sigla=old.tip_vinculo) - assert len(tipo) == 1 - new.tipo_vinculo = tipo[0] - - -def adjust_protocolo_antes_salvar(new, old): - if old.num_protocolo is None: - new.numero = old.cod_protocolo - - -def adjust_protocolo_depois_salvar(new, old): - if old.num_protocolo is None: - with reversion.create_revision(): - problema = 'Número do protocolo de PK %s é nulo' % new.pk - descricao = 'Número do protocolo alterado para %s!' % new.numero - warn(problema + ' => ' + descricao) - save_relation(obj=new, problema=problema, - descricao=descricao, eh_stub=False) - reversion.set_comment('Número de protocolo teve que ser alterado') - - -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] - - -def adjust_tipoafastamento(new, old): - if old.ind_afastamento == 1: - new.indicador = 'A' - - -def adjust_tipoproposicao(new, old): - if old.ind_mat_ou_doc == 'M': - tipo_materia = TipoMateriaLegislativa.objects.filter( - pk=old.tip_mat_ou_doc) - if tipo_materia: - new.tipo_conteudo_related = tipo_materia[0] - else: - raise ForeignKeyFaltando - elif old.ind_mat_ou_doc == 'D': - tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc) - if tipo_documento: - new.tipo_conteudo_related = tipo_documento[0] - else: - raise ForeignKeyFaltando - - -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(new, old): - # Ajusta relação M2M - - if not old.cod_assunto: # it can be null or empty - return - - # lista de pks separadas por vírgulas (ignorando strings vazias) - lista_pks_assunto = [int(pk) for pk in old.cod_assunto.split(',') if pk] - - for pk_assunto in lista_pks_assunto: - try: - new.assuntos.add(AssuntoNorma.objects.get(pk=pk_assunto)) - except ObjectDoesNotExist: - pass # ignora assuntos inexistentes - - -def vincula_autor(new, old, model_relacionado, campo_relacionado, campo_nome): - pk_rel = getattr(old, campo_relacionado) - if pk_rel: - try: - new.autor_related = model_relacionado.objects.get(pk=pk_rel) - except ObjectDoesNotExist: - # ignoramos o autor órfão - 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 - - -def adjust_autor(new, old): - for args in [ - # essa ordem é importante - (Parlamentar, 'cod_parlamentar', 'nome_parlamentar'), - (Comissao, 'cod_comissao', 'nome'), - (Partido, 'cod_partido', 'nome')]: - if vincula_autor(new, old, *args): - break - - if old.col_username: - user_model = get_user_model() - if not user_model.objects.filter(username=old.col_username).exists(): - # cria um novo ususaŕio para o autor - user = user_model(username=old.col_username) - # 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) - - -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 - - -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, -} - -AJUSTE_DEPOIS_SALVAR = { - NormaJuridica: adjust_normajuridica_depois_salvar, - OrdemDia: adjust_ordemdia_depois_salvar, - Protocolo: adjust_protocolo_depois_salvar, -} - -# CHECKS #################################################################### - - -def get_ind_excluido(new): - 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) - - -def check_app_no_ind_excluido(app): - for model in app.models.values(): - assert not any(get_ind_excluido(new) for new in model.objects.all()) - print('OK!') +def migrar(interativo=False): + migrar_dados(interativo) + migrar_usuarios() + migrar_documentos() diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py new file mode 100644 index 000000000..374ac9f2c --- /dev/null +++ b/sapl/legacy/migracao_dados.py @@ -0,0 +1,1155 @@ +import os +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 reversion +import yaml +from django.apps import apps +from django.apps.config import AppConfig +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.db import connections, transaction +from django.db.models import Count, Max +from django.db.models.base import ModelBase +from pytz import timezone + +from sapl.base.models import AppConfig as AppConf +from sapl.base.models import (Autor, ProblemaMigracao, TipoAutor, + cria_models_tipo_autor) +from sapl.comissoes.models import Comissao, Composicao, Participacao +from sapl.legacy.models import TipoNumeracaoProtocolo +from sapl.materia.models import (AcompanhamentoMateria, 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, OrdemDia, RegistroVotacao +from sapl.settings import DATABASES, PROJECT_DIR +from sapl.utils import normalize + +from .timezonesbrasil import get_timezone + +# BASE ###################################################################### +# apps to be migrated, in app dependency order (very important) +appconfs = [apps.get_app_config(n) for n in [ + 'parlamentares', + 'comissoes', + 'base', + 'materia', + 'norma', + 'sessao', + 'lexml', + 'protocoloadm', ]] + +unique_constraints = [] +one_to_one_constraints = [] +primeira_vez = [] + +name_sets = [set(m.__name__ for m in ac.get_models()) for ac in appconfs] + +# apps do not overlap +for s1 in name_sets: + for s2 in name_sets: + if s1 is not s2: + assert not s1.intersection(s2) + +# apps include all legacy models +legacy_app = apps.get_app_config('legacy') +legacy_model_names = set(m.__name__ for m in legacy_app.get_models()) + +model_dict = {m.__name__: m for ac in appconfs for m in ac.get_models()} + + +# RENAMES ################################################################### + +MODEL_RENAME_PATTERN = re.compile('(.+) \((.+)\)') + + +def get_renames(): + field_renames = {} + model_renames = {} + 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(): + match = MODEL_RENAME_PATTERN.match(model_name) + if match: + model_name, old_name = match.groups() + else: + old_name = None + model = getattr(app.models_module, model_name) + if old_name: + model_renames[model] = old_name + field_renames[model] = renames + + # collect renames from parent classes + for model, renames in field_renames.items(): + if any(parent in field_renames for parent in model.__mro__[1:]): + renames = {} + for parent in reversed(model.__mro__): + if parent in field_renames: + renames.update(field_renames[parent]) + field_renames[model] = renames + + # remove abstract classes + field_renames = {m: r for m, r in field_renames.items() + if not m._meta.abstract} + + return field_renames, model_renames + +# MIGRATION ################################################################# + + +def info(msg): + print('INFO: ' + msg) + + +def warn(msg): + print('CUIDADO! ' + msg) + + +class ForeignKeyFaltando(ObjectDoesNotExist): + 'Uma FK aponta para um registro inexistente' + + def __init__(self, msg=''): + self.msg = msg + + +@lru_cache() +def _get_all_ids_from_model(model): + # esta função para uso apenas em get_fk_related + return set(model.objects.values_list('id', flat=True)) + + +def get_fk_related(field, value, label=None): + if value is None and field.null: + return None + + # if field.related_model.objects.filter(id=value).exists(): + if value in _get_all_ids_from_model(field.related_model): + return value + elif value == 0 and field.null: + # consideramos zeros como nulos, se não está entre os ids anteriores + return None + else: + msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % ( + field.name, value, field.model.__name__, label or '---') + warn(msg) + raise ForeignKeyFaltando(msg) + + +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 = [ + # , + ('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) + + +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) + + # 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) + 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) + 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) + + +PROPAGACOES_DE_EXCLUSAO = [ + # sessao_legislativa + ('composicao_mesa', 'sessao_legislativa', 'cod_sessao_leg'), + + # parlamentar + ('dependente', 'parlamentar', 'cod_parlamentar'), + ('filiacao', 'parlamentar', 'cod_parlamentar'), + ('mandato', 'parlamentar', 'cod_parlamentar'), + + # comissao + ('composicao_comissao', 'comissao', 'cod_comissao'), + + # sessao + ('ordem_dia', 'sessao_plenaria', 'cod_sessao_plen'), + ('expediente_materia', 'sessao_plenaria', 'cod_sessao_plen'), + ('expediente_sessao_plenaria', 'sessao_plenaria', 'cod_sessao_plen'), + ('registro_votacao_parlamentar', 'registro_votacao', 'cod_votacao'), + # 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... + ('registro_votacao', 'materia_legislativa', '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 + ('tramitacao', 'materia_legislativa', 'cod_materia'), + ('autoria', 'materia_legislativa', 'cod_materia'), + ('anexada', 'materia_legislativa', 'cod_materia_principal'), + ('anexada', 'materia_legislativa', 'cod_materia_anexada'), + ('documento_acessorio', 'materia_legislativa', 'cod_materia'), + + # documento administrativo + ('tramitacao_administrativo', 'documento_administrativo', 'cod_documento'), +] + + +def propaga_exclusoes(): + for tabela_filha, tabela_pai, fk in PROPAGACOES_DE_EXCLUSAO: + [pk_pai] = get_pk_legado(tabela_pai) + exec_legado(''' + update {} set ind_excluido = 1 where {} not in ( + select {} from {} where ind_excluido != 1) + '''.format(tabela_filha, fk, pk_pai, tabela_pai)) + + +def uniformiza_banco(): + exec_legado('SET SESSION sql_mode = "";') # desliga checagens do mysql + + checa_registros_votacao_ambiguos_e_remove_nao_usados() + propaga_exclusoes() + + 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') + + # 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 + '''.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') + + # é 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() + + +def iter_sql_records(sql): + class Record: + pass + 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 save_relation(obj, nome_campo='', problema='', descricao='', + eh_stub=False, critico=False): + link = ProblemaMigracao( + content_object=obj, nome_campo=nome_campo, problema=problema, + descricao=descricao, eh_stub=eh_stub, critico=critico) + link.save() + + +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 fill_dados_basicos(): + # Ajusta sequencia numérica e cria base.AppConfig + letra = 'A' + try: + tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo') + if 'POR ANO' in tipo.des_numeracao_protocolo: + letra = 'A' + elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo: + letra = 'L' + elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo: + letra = 'U' + except Exception as e: + pass + appconf = AppConf(sequencia_numeracao=letra) + appconf.save() + + +# Uma anomalia no sapl 2.5 causa a duplicação de registros de votação. +# Essa duplicação deve ser eliminada para que não haja erro no sapl 3.1 +def excluir_registrovotacao_duplicados(): + duplicatas_ids = RegistroVotacao.objects.values( + 'materia', 'ordem', 'expediente').annotate( + Count('id')).order_by().filter(id__count__gt=1) + duplicatas_queryset = RegistroVotacao.objects.filter( + materia__in=[item['materia'] for item in duplicatas_ids]) + + for dup in duplicatas_queryset: + lista_dups = duplicatas_queryset.filter( + materia=dup.materia, expediente=dup.expediente, ordem=dup.ordem) + primeiro_registro = lista_dups[0] + lista_dups = lista_dups.exclude(pk=primeiro_registro.pk) + for objeto in lista_dups: + if (objeto.pk > primeiro_registro.pk): + try: + objeto.delete() + except: + assert 0 + else: + try: + primeiro_registro.delete() + primeiro_registro = objeto + except: + assert 0 + + +def get_last_pk(model): + last_value = model.objects.all().aggregate(Max('pk')) + return last_value['pk__max'] or 0 + + +def reinicia_sequence(model, id): + sequence_name = '%s_id_seq' % model._meta.db_table + exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % ( + 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): + self.field_renames, self.model_renames = get_renames() + self.choice_valida = {} + + # configura timezone de migração + nome_legado = DATABASES['legacy']['NAME'] + match = re.match('sapl_cm_(.*)', nome_legado) + sigla_casa = match.group(1) + with open(os.path.expanduser('~/migracao_sapl/sapl_dumps/tabela_timezones.yaml'), 'r') as arq: + tabela_timezones = yaml.load(arq) + municipio, uf, nome_timezone = tabela_timezones[sigla_casa] + if nome_timezone: + self.timezone = timezone(nome_timezone) + else: + self.timezone = get_timezone(municipio, uf) + + def populate_renamed_fields(self, new, old): + renames = self.field_renames[type(new)] + + for field in new._meta.fields: + old_field_name = renames.get(field.name) + field_type = field.get_internal_type() + if old_field_name: + old_value = getattr(old, old_field_name) + + if field_type == 'ForeignKey': + # not necessarily a model + if hasattr(old, '_meta') and old._meta.pk.name != 'id': + label = old.pk + else: + label = '-- SEM PK --' + fk_field_name = '{}_id'.format(field.name) + value = get_fk_related(field, old_value, label) + setattr(new, fk_field_name, value) + else: + value = getattr(old, old_field_name) + + if (field_type in ['CharField', 'TextField'] + and value in [None, 'None']): + value = '' + + # adiciona timezone faltante aos campos com tempo + # os campos TIMESTAMP do mysql são gravados em UTC + # os DATETIME e TIME não têm timezone + def campo_tempo_sem_timezone(tipo): + return (field_type == tipo + and value and not value.tzinfo) + if campo_tempo_sem_timezone('DateTimeField'): + value = self.timezone.localize(value) + if campo_tempo_sem_timezone('TimeField'): + value = value.replace(tzinfo=self.timezone) + + setattr(new, field.name, value) + + def migrar(self, obj=appconfs, interativo=True): + # warning: model/app migration order is of utmost importance + + uniformiza_banco() + + # excluindo database antigo. + if interativo: + info('Todos os dados do banco serão excluidos. ' + 'Recomendamos que faça backup do banco sapl ' + 'antes de continuar.') + info('Deseja continuar? [s/n]') + resposta = input() + if resposta.lower() in ['s', 'sim', 'y', 'yes']: + pass + else: + info('Migração cancelada.') + return 0 + info('Excluindo entradas antigas do banco.') + call([PROJECT_DIR.child('manage.py'), 'flush', + '--database=default', '--no-input'], stdout=PIPE) + + # apaga tipos de autor padrão (criados no flush acima) + TipoAutor.objects.all().delete() + + fill_vinculo_norma_juridica() + fill_dados_basicos() + info('Começando migração: %s...' % obj) + self._do_migrate(obj) + + # 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() + + def _do_migrate(self, obj): + if isinstance(obj, AppConfig): + models_to_migrate = (model for model in obj.models.values() + if model in self.field_renames) + self._do_migrate(models_to_migrate) + elif isinstance(obj, ModelBase): + # A migração vai pular TipoProposicao e só vai migrar essa model + # antes de migrar Proposicao. Isso deve acontecer por causa da + # GenericRelation existente em TipoProposicao. + if not obj.__name__ == 'TipoProposicao': + if obj.__name__ == 'Proposicao': + self.migrate_model(TipoProposicao) + self.migrate_model(obj) + elif hasattr(obj, '__iter__'): + for item in obj: + self._do_migrate(item) + else: + raise TypeError( + 'Parameter must be a Model, AppConfig or a sequence of them') + + def migrate_model(self, model): + print('Migrando %s...' % model.__name__) + + 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) + + 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) + + def get_id_do_legado(old): + return getattr(old, nome_pk) + else: + # 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 + continue + new = model() + try: + self.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(e.msg) + continue + else: + 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) + + # 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 migrar_dados(obj=appconfs, interativo=True): + dm = DataMigrator() + dm.migrar(obj, interativo) + + +# MIGRATION_ADJUSTMENTS ##################################################### + +def adjust_acompanhamentomateria(new, old): + new.confirmado = True + + +NOTA_DOCADM = ''' +## NOTA DE MIGRAÇÃO DE DADOS DO SAPL 2.5 ## +O número de protocolo original deste documento era [{num_protocolo}], ano {ano_original}. +'''.strip() # noqa + + +def adjust_documentoadministrativo(new, old): + if old.num_protocolo: + nota = None + ano_original = new.ano + protocolo = Protocolo.objects.filter( + numero=old.num_protocolo, ano=new.ano) + if not protocolo: + # tentamos encontrar o protocolo no ano seguinte + ano_novo = ano_original + 1 + protocolo = Protocolo.objects.filter(numero=old.num_protocolo, + ano=ano_novo) + if protocolo: + nota = NOTA_DOCADM + ''' +O protocolo vinculado é o de mesmo número, porém do ano seguinte ({ano_novo}), +pois não existe protocolo no sistema com este número no ano {ano_original}. +''' + nota = nota.strip().format(num_protocolo=old.num_protocolo, + ano_original=ano_original, + ano_novo=ano_novo) + warn('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! ' + 'DocumentoAdministrativo: {}, numero_protocolo: {}, ' + 'ano doc adm: {}'.format( + old.cod_documento, old.num_protocolo, ano_original)) + else: + nota = NOTA_DOCADM + ''' +Não existe no sistema nenhum protocolo com estes dados +e portanto nenhum protocolo foi vinculado a este documento.''' + nota = nota.format( + num_protocolo=old.num_protocolo, + ano_original=ano_original) + 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 + # adiciona nota ao final da observação + if 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 + + +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' \ + ' nulo.' % old.pk + descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem + warn(problema + ' => ' + descricao) + save_relation(obj=new, problema=problema, + descricao=descricao, eh_stub=False) + reversion.set_comment('OrdemDia sem número da ordem.') + + +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('nulo convertido para falso') + 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): + composicao = Composicao() + composicao.comissao_id, composicao.periodo_id = [ + get_fk_related(Composicao._meta.get_field(name), value) + for name, value in (('comissao', old.cod_comissao), + ('periodo', old.cod_periodo_comp))] + # check if there is already an "equal" one in the db + already_created = Composicao.objects.filter( + comissao=composicao.comissao, periodo=composicao.periodo) + if already_created: + assert len(already_created) == 1 # we must never have made 2 copies + [composicao] = already_created + else: + with reversion.create_revision(): + composicao.save() + reversion.set_comment('Objeto criado pela migração') + new.composicao = composicao + + +def adjust_proposicao_antes_salvar(new, old): + if new.data_envio: + new.ano = new.data_envio.year + + +def adjust_normarelacionada(new, old): + tipo = TipoVinculoNormaJuridica.objects.filter(sigla=old.tip_vinculo) + assert len(tipo) == 1 + new.tipo_vinculo = tipo[0] + + +def adjust_protocolo_antes_salvar(new, old): + if old.num_protocolo is None: + new.numero = old.cod_protocolo + + +def adjust_protocolo_depois_salvar(new, old): + if old.num_protocolo is None: + with reversion.create_revision(): + problema = 'Número do protocolo de PK %s é nulo' % new.pk + descricao = 'Número do protocolo alterado para %s!' % new.numero + warn(problema + ' => ' + descricao) + save_relation(obj=new, problema=problema, + descricao=descricao, eh_stub=False) + reversion.set_comment('Número de protocolo teve que ser alterado') + + +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] + + +def adjust_tipoafastamento(new, old): + if old.ind_afastamento == 1: + new.indicador = 'A' + + +def adjust_tipoproposicao(new, old): + if old.ind_mat_ou_doc == 'M': + tipo_materia = TipoMateriaLegislativa.objects.filter( + pk=old.tip_mat_ou_doc) + if tipo_materia: + new.tipo_conteudo_related = tipo_materia[0] + else: + raise ForeignKeyFaltando + elif old.ind_mat_ou_doc == 'D': + tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc) + if tipo_documento: + new.tipo_conteudo_related = tipo_documento[0] + else: + raise ForeignKeyFaltando + + +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(new, old): + # Ajusta relação M2M + + if not old.cod_assunto: # it can be null or empty + return + + # lista de pks separadas por vírgulas (ignorando strings vazias) + lista_pks_assunto = [int(pk) for pk in old.cod_assunto.split(',') if pk] + + for pk_assunto in lista_pks_assunto: + try: + new.assuntos.add(AssuntoNorma.objects.get(pk=pk_assunto)) + except ObjectDoesNotExist: + pass # ignora assuntos inexistentes + + +def vincula_autor(new, old, model_relacionado, campo_relacionado, campo_nome): + pk_rel = getattr(old, campo_relacionado) + if pk_rel: + try: + new.autor_related = model_relacionado.objects.get(pk=pk_rel) + except ObjectDoesNotExist: + # ignoramos o autor órfão + 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 + + +def adjust_autor(new, old): + for args in [ + # essa ordem é importante + (Parlamentar, 'cod_parlamentar', 'nome_parlamentar'), + (Comissao, 'cod_comissao', 'nome'), + (Partido, 'cod_partido', 'nome')]: + if vincula_autor(new, old, *args): + break + + if old.col_username: + user_model = get_user_model() + if not user_model.objects.filter(username=old.col_username).exists(): + # cria um novo ususaŕio para o autor + user = user_model(username=old.col_username) + # 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) + + +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 + + +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, +} + +AJUSTE_DEPOIS_SALVAR = { + NormaJuridica: adjust_normajuridica_depois_salvar, + OrdemDia: adjust_ordemdia_depois_salvar, + Protocolo: adjust_protocolo_depois_salvar, +} + +# CHECKS #################################################################### + + +def get_ind_excluido(new): + 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) + + +def check_app_no_ind_excluido(app): + for model in app.models.values(): + assert not any(get_ind_excluido(new) for new in model.objects.all()) + print('OK!') diff --git a/sapl/legacy/migracao_documentos.py b/sapl/legacy/migracao_documentos.py index 9de759348..9ae46ef5d 100644 --- a/sapl/legacy/migracao_documentos.py +++ b/sapl/legacy/migracao_documentos.py @@ -6,7 +6,7 @@ from glob import glob import yaml from sapl.base.models import CasaLegislativa -from sapl.legacy.migracao import exec_legado, warn +from sapl.legacy.migracao_dados import exec_legado, warn from sapl.materia.models import (DocumentoAcessorio, MateriaLegislativa, Proposicao) from sapl.norma.models import NormaJuridica diff --git a/sapl/legacy/migracao_usuarios.py b/sapl/legacy/migracao_usuarios.py index 39d6df4e9..962dfad6d 100644 --- a/sapl/legacy/migracao_usuarios.py +++ b/sapl/legacy/migracao_usuarios.py @@ -44,7 +44,7 @@ def decode_nome(nome): return nome -def migra_usuarios(): +def migrar_usuarios(): """ Lê o arquivo media/usuarios.yaml e importa os usuários nele listados, com senhas e perfis. diff --git a/sapl/legacy/scripts/migra_um_db.sh b/sapl/legacy/scripts/migra_um_db.sh index 3d41624d3..1a4e29920 100755 --- a/sapl/legacy/scripts/migra_um_db.sh +++ b/sapl/legacy/scripts/migra_um_db.sh @@ -4,7 +4,7 @@ if [ $# -ge 2 ]; then # proteje pasta com dumps de alterações acidentais - chmod -R -w ~/sapl_dumps + chmod -R -w ~/migracao_sapl/sapl_dumps DATE=$(date +%Y-%m-%d) DIR=~/${DATE}_logs_migracao @@ -21,11 +21,11 @@ if [ $# -ge 2 ]; then if [ $3 ]; then # se há senha do mysql mysql -u $2 -p "$3" -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;" - mysql -u $2 -p "$3" < ~/sapl_dumps/$1.sql + mysql -u $2 -p "$3" < ~/migracao_sapl/sapl_dumps/$1.sql else # se não há senha do mysql mysql -u $2 -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;" - mysql -u $2 < ~/sapl_dumps/$1.sql + mysql -u $2 < ~/migracao_sapl/sapl_dumps/$1.sql fi; echo "O banco legado foi restaurado" |& tee -a $LOG echo >> $LOG diff --git a/sapl/legacy/scripts/scrap_original_forms.py b/sapl/legacy/scripts/scrap_original_forms.py index dbe8e4219..e7a7f3162 100644 --- a/sapl/legacy/scripts/scrap_original_forms.py +++ b/sapl/legacy/scripts/scrap_original_forms.py @@ -10,7 +10,7 @@ from bs4.element import NavigableString, Tag from django.apps.config import AppConfig from sapl.crispy_layout_mixin import heads_and_tails -from sapl.legacy.migracao import appconfs, get_renames +from sapl.legacy.migracao_dados import appconfs, get_renames from sapl.legacy.scripts.utils import getsourcelines from sapl.utils import listify diff --git a/sapl/legacy/scripts/study.py b/sapl/legacy/scripts/study.py index 9a4149fe5..88838d36c 100644 --- a/sapl/legacy/scripts/study.py +++ b/sapl/legacy/scripts/study.py @@ -1,6 +1,6 @@ from django.apps import apps -from sapl.legacy.migracao import legacy_app +from sapl.legacy.migracao_dados import legacy_app for model in apps.get_app_config('legacy').get_models(): if 'ind_excluido' in [f.name for f in model._meta.fields]: diff --git a/sapl/legacy/scripts/utils.py b/sapl/legacy/scripts/utils.py index c426f1b3b..a983ef2b0 100644 --- a/sapl/legacy/scripts/utils.py +++ b/sapl/legacy/scripts/utils.py @@ -1,7 +1,7 @@ import inspect from sapl.base.models import Autor -from sapl.legacy.migracao import appconfs +from sapl.legacy.migracao_dados import appconfs def getsourcelines(model): diff --git a/sapl/legacy/test_migration.py b/sapl/legacy/test_migracao_dados.py similarity index 91% rename from sapl/legacy/test_migration.py rename to sapl/legacy/test_migracao_dados.py index ad1ad86d3..061264165 100644 --- a/sapl/legacy/test_migration.py +++ b/sapl/legacy/test_migracao_dados.py @@ -1,7 +1,8 @@ from random import shuffle -from .migracao import (_formatar_lista_para_sql, get_autorias_sem_repeticoes, - get_reapontamento_de_autores_repetidos) +from .migracao_dados import (_formatar_lista_para_sql, + get_autorias_sem_repeticoes, + get_reapontamento_de_autores_repetidos) def test_unifica_autores_repetidos_no_legado(): diff --git a/sapl/legacy/test_renames.py b/sapl/legacy/test_renames.py index f275efa2f..7a8766da0 100644 --- a/sapl/legacy/test_renames.py +++ b/sapl/legacy/test_renames.py @@ -3,7 +3,7 @@ import sapl.materia import sapl.norma import sapl.sessao -from .migracao import appconfs, get_renames, legacy_app +from .migracao_dados import appconfs, get_renames, legacy_app RENAMING_IGNORED_MODELS = [ sapl.comissoes.models.Composicao, From 0e667a67f381f919922c5b64912271d8f3e0f0e8 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Fri, 2 Mar 2018 10:02:17 -0300 Subject: [PATCH 02/18] =?UTF-8?q?Gera=20pacotes=20de=20migra=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/commands/migracao_25_31.py | 17 +++++++-- sapl/legacy/migracao.py | 35 ++++++++++++++++++- sapl/legacy/scripts/migra_um_db.sh | 4 +-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/sapl/legacy/management/commands/migracao_25_31.py b/sapl/legacy/management/commands/migracao_25_31.py index 1298feeb3..27591e058 100644 --- a/sapl/legacy/management/commands/migracao_25_31.py +++ b/sapl/legacy/management/commands/migracao_25_31.py @@ -1,7 +1,7 @@ from django.core import management from django.core.management.base import BaseCommand -from sapl.legacy.migracao import migrar +from sapl.legacy.migracao import migrar, migrar_dados class Command(BaseCommand): @@ -10,13 +10,24 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - '-f', + '--force', action='store_true', default=False, dest='force', help='Não interativa: pula confirmação de exclusão dos dados', ) + parser.add_argument( + '--dados', + action='store_true', + default=False, + dest='dados', + help='migra somente dados', + ) def handle(self, *args, **options): management.call_command('migrate') - migrar(interativo=not options['force']) + somente_dados, interativo = options['dados'], not options['force'] + if somente_dados: + migrar_dados(interativo=interativo) + else: + migrar(interativo=interativo) diff --git a/sapl/legacy/migracao.py b/sapl/legacy/migracao.py index a8fed932f..2690e9a53 100644 --- a/sapl/legacy/migracao.py +++ b/sapl/legacy/migracao.py @@ -1,9 +1,42 @@ +import subprocess +import tarfile + +from django.conf import settings + from sapl.legacy.migracao_dados import migrar_dados from sapl.legacy.migracao_documentos import migrar_documentos from sapl.legacy.migracao_usuarios import migrar_usuarios def migrar(interativo=False): - migrar_dados(interativo) + migrar_dados(interativo=interativo) migrar_usuarios() migrar_documentos() + + +# fonte: https://stackoverflow.com/a/17081026/1877490 +def make_tarfile(output_filename, source_dir): + with tarfile.open(output_filename, "w:gz") as tar: + tar.add(source_dir, arcname=os.path.basename(source_dir)) + + +def gerar_pacote(): + banco = settings.DATABASES['legacy']['NAME'] + + # backup do banco + print('Gerando backup do banco... ', end='', flush=True) + arq_backup = settings.MEDIA_ROOT.child('{}.backup'.format(banco)) + backup_cmd = ''' + pg_dump --host localhost --port 5432 --username postgres --no-password + --format custom --blobs --verbose --file {} {}'''.format( + arq_backup, banco) + subprocess.check_output(backup_cmd.split(), stderr=subprocess.DEVNULL) + print('SUCESSO') + + # tar de media/sapl + print('Criando tar de media... ', end='', flush=True) + tar_media = settings.MEDIA_ROOT.child('{}.media.tgz'.format(banco)) + dir_media = settings.MEDIA_ROOT.child('sapl') + with tarfile.open(tar_media, "w:gz") as tar: + tar.add(dir_media, arcname=dir_media.name) + print('SUCESSO') diff --git a/sapl/legacy/scripts/migra_um_db.sh b/sapl/legacy/scripts/migra_um_db.sh index 1a4e29920..aab47c20e 100755 --- a/sapl/legacy/scripts/migra_um_db.sh +++ b/sapl/legacy/scripts/migra_um_db.sh @@ -4,7 +4,7 @@ if [ $# -ge 2 ]; then # proteje pasta com dumps de alterações acidentais - chmod -R -w ~/migracao_sapl/sapl_dumps + # chmod -R -w ~/migracao_sapl/sapl_dumps DATE=$(date +%Y-%m-%d) DIR=~/${DATE}_logs_migracao @@ -37,7 +37,7 @@ if [ $# -ge 2 ]; then echo "--- MIGRACAO DE DADOS ---" | tee -a $LOG echo >> $LOG - DATABASE_NAME=$1 ./manage.py migracao_25_31 -f --settings sapl.legacy_migration_settings |& tee -a $LOG + DATABASE_NAME=$1 ./manage.py migracao_25_31 --force --dados --settings sapl.legacy_migration_settings 2>&1 | tee -a $LOG echo >> $LOG else echo "USO:" From aa3b6b293651e54a02dcc23bacea76b3cf032f0f Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Fri, 2 Mar 2018 14:02:25 -0300 Subject: [PATCH 03/18] =?UTF-8?q?Corrige=20unifica=C3=A7=C3=A3o=20de=20aut?= =?UTF-8?q?ores=20na=20migra=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao_dados.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 374ac9f2c..f494ec365 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -317,11 +317,12 @@ def unifica_autores_repetidos_no_legado(campo_agregador): 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) - 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]))) + 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: From 0e859fae62c840ee51a312d7479158df0b52d1d6 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Fri, 2 Mar 2018 14:36:32 -0300 Subject: [PATCH 04/18] Relaxa data_materia em materia.Numeracao MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (devido à migração de dados) --- .../migrations/0026_auto_20180302_1411.py | 20 +++++++++++++++++++ sapl/materia/models.py | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 sapl/materia/migrations/0026_auto_20180302_1411.py diff --git a/sapl/materia/migrations/0026_auto_20180302_1411.py b/sapl/materia/migrations/0026_auto_20180302_1411.py new file mode 100644 index 000000000..9d401710e --- /dev/null +++ b/sapl/materia/migrations/0026_auto_20180302_1411.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-03-02 17:11 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0025_auto_20180221_1649'), + ] + + operations = [ + migrations.AlterField( + model_name='numeracao', + name='data_materia', + field=models.DateField(null=True, verbose_name='Data'), + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 51fbcb855..ab4b3d074 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -515,7 +515,7 @@ class Numeracao(models.Model): verbose_name=_('Número')) ano_materia = models.PositiveSmallIntegerField(verbose_name=_('Ano'), choices=RANGE_ANOS) - data_materia = models.DateField(verbose_name=_('Data')) + data_materia = models.DateField(verbose_name=_('Data'), null=True) class Meta: verbose_name = _('Numeração') @@ -529,7 +529,7 @@ class Numeracao(models.Model): def __str__(self): return _('%(numero)s/%(ano)s') % { 'numero': self.numero_materia, - 'ano': self.data_materia.year} + 'ano': self.ano_materia} @reversion.register() From 1b21784329400580db46ad67d8f0ed4cf599c513 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Mon, 5 Mar 2018 16:41:56 -0300 Subject: [PATCH 05/18] =?UTF-8?q?Otimiza=20enumera=C3=A7=C3=A3o=20de=20reg?= =?UTF-8?q?istros=20na=20migra=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao_dados.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index f494ec365..ffde783aa 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -15,9 +15,10 @@ from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db import connections, transaction -from django.db.models import Count, Max +from django.db.models import Count, Max, Q from django.db.models.base import ModelBase from pytz import timezone +from unipath import Path from sapl.base.models import AppConfig as AppConf from sapl.base.models import (Autor, ProblemaMigracao, TipoAutor, @@ -639,6 +640,10 @@ def get_pk_legado(tabela): return [r[4] for r in res] +DIR_DADOS_MIGRACAO = Path('~/migracao_sapl/').expand() +PATH_TABELA_TIMEZONES = DIR_DADOS_MIGRACAO.child('tabela_timezones.yaml') + + class DataMigrator: def __init__(self): @@ -649,7 +654,7 @@ class DataMigrator: nome_legado = DATABASES['legacy']['NAME'] match = re.match('sapl_cm_(.*)', nome_legado) sigla_casa = match.group(1) - with open(os.path.expanduser('~/migracao_sapl/sapl_dumps/tabela_timezones.yaml'), 'r') as arq: + with open(PATH_TABELA_TIMEZONES, 'r') as arq: tabela_timezones = yaml.load(arq) municipio, uf, nome_timezone = tabela_timezones[sigla_casa] if nome_timezone: @@ -712,7 +717,7 @@ class DataMigrator: else: info('Migração cancelada.') return 0 - info('Excluindo entradas antigas do banco.') + info('Excluindo entradas antigas do banco destino.') call([PROJECT_DIR.child('manage.py'), 'flush', '--database=default', '--no-input'], stdout=PIPE) @@ -761,13 +766,23 @@ class DataMigrator: 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) + 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) else: # a pk no legado tem mais de um campo - old_records = iter_sql_records('select * from ' + tabela_legado) + sql = 'select * from ' + tabela_legado + if existe_coluna_no_legado(tabela_legado, 'ind_excluido'): + sql += ' where ind_excluido != 1' + old_records = iter_sql_records(sql) + get_id_do_legado = None ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model) @@ -777,9 +792,6 @@ class DataMigrator: 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 - continue new = model() try: self.populate_renamed_fields(new, old) From 4bd031f6171d5c67aab74a2385a8b1a14d6a27ec Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Wed, 7 Mar 2018 14:15:38 -0300 Subject: [PATCH 06/18] Ajusta util de listagem de refs a model --- sapl/legacy/scripts/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sapl/legacy/scripts/utils.py b/sapl/legacy/scripts/utils.py index a983ef2b0..40f1abb3c 100644 --- a/sapl/legacy/scripts/utils.py +++ b/sapl/legacy/scripts/utils.py @@ -9,10 +9,10 @@ def getsourcelines(model): for line in inspect.getsourcelines(model)[0]] -def get_models_com_referencia_a_autor(): +def get_models_com_referencia_a(apontado): def tem_referencia_a_autor(model): - return any(getattr(field, 'related_model', None) == Autor + return any(getattr(field, 'related_model', None) == apontado for field in model._meta.get_fields()) return [model for app in appconfs for model in app.models.values() From 75ced3afbbfd6ee0b47282dcc033f8a259f6b499 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Wed, 7 Mar 2018 17:38:49 -0300 Subject: [PATCH 07/18] =?UTF-8?q?Melhora=20feedback=20de=20ajuste=20de=20c?= =?UTF-8?q?omposi=C3=A7=C3=A3o=20de=20comiss=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao_dados.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index ffde783aa..7cb2484ca 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -947,7 +947,11 @@ def adjust_parlamentar(new, old): def adjust_participacao(new, old): composicao = Composicao() composicao.comissao_id, composicao.periodo_id = [ - get_fk_related(Composicao._meta.get_field(name), value) + get_fk_related(Composicao._meta.get_field(name), + value, + 'composicao_comissao.cod_comp_comissao = {}'.format( + old.pk + )) for name, value in (('comissao', old.cod_comissao), ('periodo', old.cod_periodo_comp))] # check if there is already an "equal" one in the db From 0e56cf9b0dfd5f82767c87db6fd57bd8ce0ae20c Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Wed, 7 Mar 2018 17:46:29 -0300 Subject: [PATCH 08/18] =?UTF-8?q?Retira=20c=C3=B3digo=20desnecess=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/comissoes/legacy.yaml | 17 ---------------- sapl/legacy/migracao_dados.py | 38 +---------------------------------- 2 files changed, 1 insertion(+), 54 deletions(-) diff --git a/sapl/comissoes/legacy.yaml b/sapl/comissoes/legacy.yaml index 59aacf503..a1093c4d2 100644 --- a/sapl/comissoes/legacy.yaml +++ b/sapl/comissoes/legacy.yaml @@ -43,20 +43,3 @@ Participacao (ComposicaoComissao): observacao: obs_composicao parlamentar: cod_parlamentar titular: ind_titular - -Reuniao: - periodo: periodo_reuniao - comissao: cod_comissao - numero: num_comissao - nome: nom_reuniao - tema: tem_reuniao - data: dat_reuniao - hora_inicio: hora_inicio_reuniao - hora_fim: hora_fim_reuniao - local_reuniao: local - observacao: obs_reuniao - ulr_audio: audio_reuniao - url_video: video_reuniao - upload_pauta: pauta_reuniao - upload_ata: ata_reuniao - upload_anexo: anexo_reuniao diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 7cb2484ca..0f2be3883 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -57,19 +57,14 @@ unique_constraints = [] one_to_one_constraints = [] primeira_vez = [] -name_sets = [set(m.__name__ for m in ac.get_models()) for ac in appconfs] - # apps do not overlap +name_sets = [set(m.__name__ for m in ac.get_models()) for ac in appconfs] for s1 in name_sets: for s2 in name_sets: if s1 is not s2: assert not s1.intersection(s2) -# apps include all legacy models legacy_app = apps.get_app_config('legacy') -legacy_model_names = set(m.__name__ for m in legacy_app.get_models()) - -model_dict = {m.__name__: m for ac in appconfs for m in ac.get_models()} # RENAMES ################################################################### @@ -595,34 +590,6 @@ def fill_dados_basicos(): appconf.save() -# Uma anomalia no sapl 2.5 causa a duplicação de registros de votação. -# Essa duplicação deve ser eliminada para que não haja erro no sapl 3.1 -def excluir_registrovotacao_duplicados(): - duplicatas_ids = RegistroVotacao.objects.values( - 'materia', 'ordem', 'expediente').annotate( - Count('id')).order_by().filter(id__count__gt=1) - duplicatas_queryset = RegistroVotacao.objects.filter( - materia__in=[item['materia'] for item in duplicatas_ids]) - - for dup in duplicatas_queryset: - lista_dups = duplicatas_queryset.filter( - materia=dup.materia, expediente=dup.expediente, ordem=dup.ordem) - primeiro_registro = lista_dups[0] - lista_dups = lista_dups.exclude(pk=primeiro_registro.pk) - for objeto in lista_dups: - if (objeto.pk > primeiro_registro.pk): - try: - objeto.delete() - except: - assert 0 - else: - try: - primeiro_registro.delete() - primeiro_registro = objeto - except: - assert 0 - - def get_last_pk(model): last_value = model.objects.all().aggregate(Max('pk')) return last_value['pk__max'] or 0 @@ -729,9 +696,6 @@ class DataMigrator: info('Começando migração: %s...' % obj) self._do_migrate(obj) - # 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() From 065a1980ce256513177368d390220ef3a627eada Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Thu, 8 Mar 2018 16:28:41 -0300 Subject: [PATCH 09/18] =?UTF-8?q?Refatora=20ordem=20de=20migra=C3=A7=C3=A3?= =?UTF-8?q?o=20de=20TipoProposicao?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit para indicar o motivo correto --- sapl/legacy/migracao_dados.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 0f2be3883..55db4999f 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -701,17 +701,24 @@ class DataMigrator: def _do_migrate(self, obj): if isinstance(obj, AppConfig): - models_to_migrate = (model for model in obj.models.values() - if model in self.field_renames) - self._do_migrate(models_to_migrate) + models = [model for model in obj.models.values() + if model in self.field_renames] + + if obj.label == 'materia': + # 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) + models.remove(TipoProposicao) + pos_tipo_proposicao = max( + models.index(TipoMateriaLegislativa), + models.index(TipoDocumento)) + 1 + models.insert(pos_tipo_proposicao, TipoProposicao) + assert models.index(TipoProposicao) < models.index(Proposicao) + + self._do_migrate(models) elif isinstance(obj, ModelBase): - # A migração vai pular TipoProposicao e só vai migrar essa model - # antes de migrar Proposicao. Isso deve acontecer por causa da - # GenericRelation existente em TipoProposicao. - if not obj.__name__ == 'TipoProposicao': - if obj.__name__ == 'Proposicao': - self.migrate_model(TipoProposicao) - self.migrate_model(obj) + self.migrate_model(obj) elif hasattr(obj, '__iter__'): for item in obj: self._do_migrate(item) From 1abe092e9597103dc2f621da3ea4a817327a1571 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Thu, 8 Mar 2018 16:37:11 -0300 Subject: [PATCH 10/18] Retira warning reduntante --- sapl/legacy/migracao_dados.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 55db4999f..2785deb73 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -141,7 +141,6 @@ def get_fk_related(field, value, label=None): else: msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % ( field.name, value, field.model.__name__, label or '---') - warn(msg) raise ForeignKeyFaltando(msg) From adcb657e9358df6011f8838efcf20a46aaa2415a Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Thu, 8 Mar 2018 16:55:31 -0300 Subject: [PATCH 11/18] =?UTF-8?q?Propaga=20exclus=C3=A3o=20de=20periodo=5F?= =?UTF-8?q?comp=5Fcomissao=20p=20composicao=5Fcomissao?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao_dados.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 2785deb73..7aeb1aa54 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -387,43 +387,44 @@ def checa_registros_votacao_ambiguos_e_remove_nao_usados(): PROPAGACOES_DE_EXCLUSAO = [ # sessao_legislativa - ('composicao_mesa', 'sessao_legislativa', 'cod_sessao_leg'), + ('sessao_legislativa', 'composicao_mesa', 'cod_sessao_leg'), # parlamentar - ('dependente', 'parlamentar', 'cod_parlamentar'), - ('filiacao', 'parlamentar', 'cod_parlamentar'), - ('mandato', 'parlamentar', 'cod_parlamentar'), + ('parlamentar', 'dependente', 'cod_parlamentar'), + ('parlamentar', 'filiacao', 'cod_parlamentar'), + ('parlamentar', 'mandato', 'cod_parlamentar'), # comissao - ('composicao_comissao', 'comissao', 'cod_comissao'), + ('comissao', 'composicao_comissao', 'cod_comissao'), + ('periodo_comp_comissao', 'composicao_comissao', 'cod_periodo_comp'), # sessao - ('ordem_dia', 'sessao_plenaria', 'cod_sessao_plen'), - ('expediente_materia', 'sessao_plenaria', 'cod_sessao_plen'), - ('expediente_sessao_plenaria', 'sessao_plenaria', 'cod_sessao_plen'), - ('registro_votacao_parlamentar', 'registro_votacao', 'cod_votacao'), + ('sessao_plenaria', 'ordem_dia', 'cod_sessao_plen'), + ('sessao_plenaria', 'expediente_materia', 'cod_sessao_plen'), + ('sessao_plenaria', 'expediente_sessao_plenaria', 'cod_sessao_plen'), + ('registro_votacao', 'registro_votacao_parlamentar', 'cod_votacao'), # 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... - ('registro_votacao', 'materia_legislativa', 'cod_materia'), + ('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 - ('tramitacao', 'materia_legislativa', 'cod_materia'), - ('autoria', 'materia_legislativa', 'cod_materia'), - ('anexada', 'materia_legislativa', 'cod_materia_principal'), - ('anexada', 'materia_legislativa', 'cod_materia_anexada'), - ('documento_acessorio', 'materia_legislativa', 'cod_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'), # documento administrativo - ('tramitacao_administrativo', 'documento_administrativo', 'cod_documento'), + ('documento_administrativo', 'tramitacao_administrativo', 'cod_documento'), ] def propaga_exclusoes(): - for tabela_filha, tabela_pai, fk in PROPAGACOES_DE_EXCLUSAO: + for tabela_pai, tabela_filha, fk in PROPAGACOES_DE_EXCLUSAO: [pk_pai] = get_pk_legado(tabela_pai) exec_legado(''' update {} set ind_excluido = 1 where {} not in ( From bc4621fa1558c0e964f01328b65c39e58831218b Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Fri, 9 Mar 2018 20:51:05 -0300 Subject: [PATCH 12/18] =?UTF-8?q?Retirado=20coment=C3=A1rio=20zumbi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/parlamentares/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py index 9d43e6d6a..574c6735f 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -261,12 +261,9 @@ class Parlamentar(models.Model): verbose_name=_('Ativo na Casa?')) biografia = models.TextField( blank=True, verbose_name=_('Biografia')) - # XXX Esse atribuito foi colocado aqui para não atrapalhar a migração - fotografia = ImageCropField( verbose_name=_('Fotografia'), upload_to=foto_upload_path, validators=[restringe_tipos_de_arquivo_img], null=True, blank=True) - cropping = ImageRatioField( 'fotografia', '128x128', verbose_name=_('Avatar'), size_warning=True, help_text=_('A configuração do Avatar ' From c50a30ebf303b966a51a481f630d21e47ac62eef Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Fri, 9 Mar 2018 21:40:58 -0300 Subject: [PATCH 13/18] =?UTF-8?q?Registra=20ocorr=C3=AAncias=20de=20migra?= =?UTF-8?q?=C3=A7=C3=A3o=20em=20arquivo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao_dados.py | 163 ++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 76 deletions(-) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 7aeb1aa54..efa4cf277 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -1,5 +1,5 @@ -import os import re +from collections import defaultdict from datetime import date from functools import lru_cache, partial from itertools import groupby @@ -15,7 +15,7 @@ from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db import connections, transaction -from django.db.models import Count, Max, Q +from django.db.models import Max, Q from django.db.models.base import ModelBase from pytz import timezone from unipath import Path @@ -110,16 +110,30 @@ def get_renames(): def info(msg): print('INFO: ' + msg) +ocorrencias = defaultdict(list) -def warn(msg): - print('CUIDADO! ' + msg) + +def warn(tipo, msg, dados): + ocorrencias[tipo].append(dados) + print('CUIDADO! ' + msg.format(**dados)) class ForeignKeyFaltando(ObjectDoesNotExist): 'Uma FK aponta para um registro inexistente' - def __init__(self, msg=''): - self.msg = msg + def __init__(self, field, value, label): + self.field = field + self.value = value + self.label = label + + msg = 'FK [{field}] não encontrada para o valor {value} (em {model} / {label})' # noqa + + @property + def dados(self): + return {'field': self.field.name, + 'value': self.value, + 'model': self.field.model.__name__, + 'label': self.label} @lru_cache() @@ -128,7 +142,7 @@ def _get_all_ids_from_model(model): return set(model.objects.values_list('id', flat=True)) -def get_fk_related(field, value, label=None): +def get_fk_related(field, value, label='---'): if value is None and field.null: return None @@ -139,9 +153,7 @@ def get_fk_related(field, value, label=None): # consideramos zeros como nulos, se não está entre os ids anteriores return None else: - msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % ( - field.name, value, field.model.__name__, label or '---') - raise ForeignKeyFaltando(msg) + raise ForeignKeyFaltando(field=field, value=value, label=label) def exec_sql(sql, db='default'): @@ -609,6 +621,7 @@ def get_pk_legado(tabela): DIR_DADOS_MIGRACAO = Path('~/migracao_sapl/').expand() PATH_TABELA_TIMEZONES = DIR_DADOS_MIGRACAO.child('tabela_timezones.yaml') +DIR_RESULTADOS = DIR_DADOS_MIGRACAO.child('resultados') class DataMigrator: @@ -618,8 +631,8 @@ class DataMigrator: self.choice_valida = {} # configura timezone de migração - nome_legado = DATABASES['legacy']['NAME'] - match = re.match('sapl_cm_(.*)', nome_legado) + self.nome_banco_legado = DATABASES['legacy']['NAME'] + match = re.match('sapl_cm_(.*)', self.nome_banco_legado) sigla_casa = match.group(1) with open(PATH_TABELA_TIMEZONES, 'r') as arq: tabela_timezones = yaml.load(arq) @@ -641,7 +654,7 @@ class DataMigrator: if field_type == 'ForeignKey': # not necessarily a model if hasattr(old, '_meta') and old._meta.pk.name != 'id': - label = old.pk + label = 'pk = {}'.format(old.pk) else: label = '-- SEM PK --' fk_field_name = '{}_id'.format(field.name) @@ -694,7 +707,18 @@ class DataMigrator: fill_vinculo_norma_juridica() fill_dados_basicos() info('Começando migração: %s...' % obj) - self._do_migrate(obj) + try: + ocorrencias.clear() + dir_ocorrencias = DIR_RESULTADOS.child(date.today().isoformat()) + dir_ocorrencias.mkdir(parents=True) + self._do_migrate(obj) + finally: + # grava ocorrências + arq_ocorrencias = dir_ocorrencias.child( + self.nome_banco_legado + '.yaml') + with open(arq_ocorrencias, 'w') as arq: + yaml.safe_dump(dict(ocorrencias), arq, allow_unicode=True) + info('Ocorrências salvas em\n {}'.format(arq_ocorrencias)) # recria tipos de autor padrão que não foram criados pela migração cria_models_tipo_autor() @@ -772,7 +796,7 @@ class DataMigrator: # tentamos preencher uma FK e o ojeto relacionado # não existe # então este é um objeo órfão: simplesmente ignoramos - warn(e.msg) + warn('fk', e.msg, e.dados) continue else: if get_id_do_legado: @@ -841,10 +865,15 @@ pois não existe protocolo no sistema com este número no ano {ano_original}. nota = nota.strip().format(num_protocolo=old.num_protocolo, ano_original=ano_original, ano_novo=ano_novo) - warn('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! ' - 'DocumentoAdministrativo: {}, numero_protocolo: {}, ' - 'ano doc adm: {}'.format( - old.cod_documento, old.num_protocolo, ano_original)) + msg = 'PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! '\ + 'DocumentoAdministrativo: {cod_documento}, '\ + 'numero_protocolo: {num_protocolo}, '\ + 'ano doc adm: {ano_original}' + warn('protocolo_ano_seguinte', msg, + {'cod_documento': old.cod_documento, + 'num_protocolo': old.num_protocolo, + 'ano_original': ano_original, + 'nota': nota}) else: nota = NOTA_DOCADM + ''' Não existe no sistema nenhum protocolo com estes dados @@ -852,9 +881,12 @@ e portanto nenhum protocolo foi vinculado a este documento.''' nota = nota.format( num_protocolo=old.num_protocolo, ano_original=ano_original) - warn('Protocolo {} faltando ' - '(referenciado no documento administrativo {})'.format( - old.num_protocolo, old.cod_documento)) + msg = 'Protocolo {num_protocolo} faltando (referenciado ' \ + 'no documento administrativo {cod_documento})' + warn('protocolo_faltando', msg, + {'num_protocolo': old.num_protocolo, + 'cod_documento': old.cod_documento, + 'nota': nota}) if protocolo: assert len(protocolo) == 1, 'mais de um protocolo encontrado' [new.protocolo] = protocolo @@ -883,18 +915,10 @@ def adjust_ordemdia_antes_salvar(new, old): if old.num_ordem is None: new.numero_ordem = 999999999 - - -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' \ - ' nulo.' % old.pk - descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem - warn(problema + ' => ' + descricao) - save_relation(obj=new, problema=problema, - descricao=descricao, eh_stub=False) - reversion.set_comment('OrdemDia sem número da ordem.') + 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): @@ -904,7 +928,10 @@ def adjust_parlamentar(new, old): # but data includes null values # => transform None to False if value is None: - warn('nulo convertido para falso') + 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: @@ -950,19 +977,12 @@ def adjust_normarelacionada(new, old): def adjust_protocolo_antes_salvar(new, old): - if old.num_protocolo is None: + if new.numero is None: new.numero = old.cod_protocolo - - -def adjust_protocolo_depois_salvar(new, old): - if old.num_protocolo is None: - with reversion.create_revision(): - problema = 'Número do protocolo de PK %s é nulo' % new.pk - descricao = 'Número do protocolo alterado para %s!' % new.numero - warn(problema + ' => ' + descricao) - save_relation(obj=new, problema=problema, - descricao=descricao, eh_stub=False) - reversion.set_comment('Número de protocolo teve que ser alterado') + 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 adjust_registrovotacao_antes_salvar(new, old): @@ -982,20 +1002,22 @@ def adjust_tipoafastamento(new, old): new.indicador = 'A' +MODEL_TIPO_MATERIA_OU_DOCUMENTO = {'M': TipoMateriaLegislativa, + 'D': TipoDocumento} + + def adjust_tipoproposicao(new, old): - if old.ind_mat_ou_doc == 'M': - tipo_materia = TipoMateriaLegislativa.objects.filter( - pk=old.tip_mat_ou_doc) - if tipo_materia: - new.tipo_conteudo_related = tipo_materia[0] - else: - raise ForeignKeyFaltando - elif old.ind_mat_ou_doc == 'D': - tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc) - if tipo_documento: - new.tipo_conteudo_related = tipo_documento[0] - else: - raise ForeignKeyFaltando + "Aponta para o tipo relacionado de matéria ou documento" + value = old.tip_mat_ou_doc + model_tipo = MODEL_TIPO_MATERIA_OU_DOCUMENTO[old.ind_mat_ou_doc] + tipo = model_tipo.objects.filter(pk=value) + if tipo: + new.tipo_conteudo_related = tipo[0] + else: + raise ForeignKeyFaltando( + field=TipoProposicao.tipo_conteudo_related, + value=(model_tipo.__name__, value), + label='ind_mat_ou_doc = {}'.format(old.ind_mat_ou_doc)) def adjust_statustramitacao(new, old): @@ -1057,9 +1079,12 @@ 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 + nome_model_relacionado = model_relacionado._meta.model.__name__ raise ForeignKeyFaltando( - '{} [pk={}] inexistente para autor'.format( - model_relacionado._meta.verbose_name, pk_rel)) + field=Autor.autor_related, + value=(nome_model_relacionado, pk_rel), + label='{} [pk={}] inexistente para autor'.format( + nome_model_relacionado, pk_rel)) else: new.nome = getattr(new.autor_related, campo_nome) return True @@ -1124,20 +1149,6 @@ AJUSTE_ANTES_SALVAR = { AJUSTE_DEPOIS_SALVAR = { NormaJuridica: adjust_normajuridica_depois_salvar, - OrdemDia: adjust_ordemdia_depois_salvar, - Protocolo: adjust_protocolo_depois_salvar, } # CHECKS #################################################################### - - -def get_ind_excluido(new): - 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) - - -def check_app_no_ind_excluido(app): - for model in app.models.values(): - assert not any(get_ind_excluido(new) for new in model.objects.all()) - print('OK!') From f3aea175309c7d33cfa068009d2ab989d0bc1f15 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Tue, 13 Mar 2018 13:50:02 -0300 Subject: [PATCH 14/18] =?UTF-8?q?Propaga=20exclus=C3=B5es=20para=20autores?= =?UTF-8?q?=20n=C3=A3o=20referenciados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao_dados.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index efa4cf277..51c194bf4 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -252,6 +252,21 @@ def reverte_exclusao_de_autores_referenciados_no_legado(): '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( + tuple(autores_referenciados)) + exec_legado(sql) + def get_reapontamento_de_autores_repetidos(autores): """ Dada uma lista ordenada de pares (cod_zzz, cod_autor) retorna: From f2bcdebc38ee9db76e53d5d5f025057b16f1f3e0 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Tue, 13 Mar 2018 15:07:19 -0300 Subject: [PATCH 15/18] =?UTF-8?q?Ajusta=20scripts=20p=20migra=C3=A7=C3=A3o?= =?UTF-8?q?=20em=20paralelo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/scripts/migra_dbs.sh | 5 +++++ sapl/legacy/scripts/migra_um_db.sh | 16 +++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/sapl/legacy/scripts/migra_dbs.sh b/sapl/legacy/scripts/migra_dbs.sh index 25a78e01c..3620da089 100755 --- a/sapl/legacy/scripts/migra_dbs.sh +++ b/sapl/legacy/scripts/migra_dbs.sh @@ -2,8 +2,13 @@ # rodar esse script na raiz do projeto + if [ $# -ge 1 ]; then + # mysql com senha parallel -eta --verbose -j+0 ./sapl/legacy/scripts/migra_um_db.sh :::: <(mysql -u $1 -p$2 -e 'show databases;' | grep '^sapl_') ::: $1 ::: $2 +elif [ $# -ge 0 ]; then + # mysql sem senha + parallel -eta --verbose -j+0 ./sapl/legacy/scripts/migra_um_db.sh :::: <(mysql -u $1 -e 'show databases;' | grep '^sapl_') ::: $1 else echo "USO:" echo " $0 [senha mysql]" diff --git a/sapl/legacy/scripts/migra_um_db.sh b/sapl/legacy/scripts/migra_um_db.sh index aab47c20e..eca35f0a3 100755 --- a/sapl/legacy/scripts/migra_um_db.sh +++ b/sapl/legacy/scripts/migra_um_db.sh @@ -6,11 +6,13 @@ if [ $# -ge 2 ]; then # proteje pasta com dumps de alterações acidentais # chmod -R -w ~/migracao_sapl/sapl_dumps + DIR_MIGRACAO=~/migracao_sapl + DATE=$(date +%Y-%m-%d) - DIR=~/${DATE}_logs_migracao - mkdir -p $DIR + DIR_LOGS=$DIR_MIGRACAO/logs/$DATE + mkdir -p $DIR_LOGS - LOG="$DIR/$1.migracao.log" + LOG="$DIR_LOGS/$1.migracao.log" rm -f $LOG echo "########################################" | tee -a $LOG @@ -20,12 +22,12 @@ if [ $# -ge 2 ]; then if [ $3 ]; then # se há senha do mysql - mysql -u $2 -p "$3" -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;" - mysql -u $2 -p "$3" < ~/migracao_sapl/sapl_dumps/$1.sql + mysql -u$2 -p"$3" -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;" + mysql -u$2 -p"$3" < $DIR_MIGRACAO/dumps_mysql/$1.sql else # se não há senha do mysql - mysql -u $2 -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;" - mysql -u $2 < ~/migracao_sapl/sapl_dumps/$1.sql + mysql -u$2 -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;" + mysql -u$2 < $DIR_MIGRACAO/dumps_mysql/$1.sql fi; echo "O banco legado foi restaurado" |& tee -a $LOG echo >> $LOG From ce59f543b472bbb5775f23d63f5fb16ea1d3e3f4 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Wed, 14 Mar 2018 17:48:30 -0300 Subject: [PATCH 16/18] =?UTF-8?q?Adapata=20migra=C3=A7=C3=A3o=20a=20doc=20?= =?UTF-8?q?acessorio=20repetido?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao_dados.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 51c194bf4..2ac75c4ef 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -57,12 +57,17 @@ unique_constraints = [] one_to_one_constraints = [] primeira_vez = [] -# apps do not overlap -name_sets = [set(m.__name__ for m in ac.get_models()) for ac in appconfs] -for s1 in name_sets: - for s2 in name_sets: - if s1 is not s2: - assert not s1.intersection(s2) +# 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) legacy_app = apps.get_app_config('legacy') From 9436b371c1ee61965895d6b7c7aaec26b0be2eff Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Thu, 15 Mar 2018 17:25:31 -0300 Subject: [PATCH 17/18] =?UTF-8?q?Corrige=20migra=C3=A7=C3=A3o=20de=20super?= =?UTF-8?q?usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao_usuarios.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sapl/legacy/migracao_usuarios.py b/sapl/legacy/migracao_usuarios.py index 962dfad6d..666bf57ef 100644 --- a/sapl/legacy/migracao_usuarios.py +++ b/sapl/legacy/migracao_usuarios.py @@ -1,6 +1,6 @@ import yaml -from django.contrib.auth.models import Group, User +from django.contrib.auth.models import Group, User from sapl.hashers import zope_encoded_password_to_django from sapl.settings import MEDIA_ROOT @@ -88,10 +88,10 @@ def migrar_usuarios(): for perfil in perfis: if perfil in ADMINISTRADORES: # Manager - usuario.is_staff = True - usuario.save() + usuario.is_superuser = True else: usuario.groups.add(PERFIL_LEGADO_PARA_NOVO[perfil]) + usuario.save() # apaga arquivo (importante pois contém senhas) ARQUIVO_USUARIOS.remove() print('Usuários migrados com sucesso.') From 118b5dc01144836f276cf5c7feb5b8133edd2f6b Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Fri, 16 Mar 2018 12:06:16 -0300 Subject: [PATCH 18/18] =?UTF-8?q?Limita=20n=C3=BAmero=20de=20superusu?= =?UTF-8?q?=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/legacy/migracao_usuarios.py | 32 ++++++++++++++++++++++++------ sapl/legacy/scripts/migra_um_db.sh | 4 ++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/sapl/legacy/migracao_usuarios.py b/sapl/legacy/migracao_usuarios.py index 666bf57ef..106a2def6 100644 --- a/sapl/legacy/migracao_usuarios.py +++ b/sapl/legacy/migracao_usuarios.py @@ -1,13 +1,13 @@ import yaml - from django.contrib.auth.models import Group, User + from sapl.hashers import zope_encoded_password_to_django from sapl.settings import MEDIA_ROOT PERFIL_LEGADO_PARA_NOVO = {legado: Group.objects.get(name=novo) for legado, novo in [ ('Autor', 'Autor'), - ('Operador', 'Operador Geral'), + ('Operador', 'Operador Geral'), ('Operador Comissao', 'Operador de Comissões'), ('Operador Materia', 'Operador de Matéria'), ('Operador Modulo Administrativo', 'Operador Administrativo'), @@ -82,16 +82,36 @@ def migrar_usuarios(): set(dados['roles']) - IGNORADOS) for nome, dados in usuarios.items()] + admins = [] for nome, senha, perfis in usuarios: usuario = User.objects.get_or_create(username=nome)[0] usuario.password = zope_encoded_password_to_django(senha) for perfil in perfis: if perfil in ADMINISTRADORES: - # Manager - usuario.is_superuser = True + # todos os administradores ganham perfil "Operador Geral" + usuario.groups.add(PERFIL_LEGADO_PARA_NOVO['Operador']) + admins.append(usuario) else: usuario.groups.add(PERFIL_LEGADO_PARA_NOVO[perfil]) usuario.save() - # apaga arquivo (importante pois contém senhas) - ARQUIVO_USUARIOS.remove() + + # restringe e configura administradores + if len(admins) > 2: + admins = ( + # ususários com admin no nome + [u for u in admins if 'admin' in u.username] + # senão, o usuário saploper, apenas + or [u for u in admins if 'saploper' == u.username] + # senão, simplesmente até os dois primeiros da lista + or admins[:2] + ) + for admin in admins: + admin.is_superuser = True + admin.save() + print('Usuários migrados com sucesso.') + print('#' * 100) + print('Uusários administradores:') + for admin in admins: + print(admin.username) + print('#' * 100) diff --git a/sapl/legacy/scripts/migra_um_db.sh b/sapl/legacy/scripts/migra_um_db.sh index eca35f0a3..577a2c000 100755 --- a/sapl/legacy/scripts/migra_um_db.sh +++ b/sapl/legacy/scripts/migra_um_db.sh @@ -37,9 +37,9 @@ if [ $# -ge 2 ]; then DATABASE_NAME=$1 ./manage.py migrate --settings sapl.legacy_migration_settings echo >> $LOG - echo "--- MIGRACAO DE DADOS ---" | tee -a $LOG + echo "--- MIGRACAO ---" | tee -a $LOG echo >> $LOG - DATABASE_NAME=$1 ./manage.py migracao_25_31 --force --dados --settings sapl.legacy_migration_settings 2>&1 | tee -a $LOG + DATABASE_NAME=$1 ./manage.py migracao_25_31 --force --settings sapl.legacy_migration_settings 2>&1 | tee -a $LOG echo >> $LOG else echo "USO:"