diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 6f740555b..14663f8ed 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -1,6 +1,6 @@ import re import traceback -from collections import OrderedDict, defaultdict +from collections import OrderedDict, defaultdict, namedtuple from datetime import date from functools import lru_cache, partial from itertools import groupby @@ -8,18 +8,16 @@ from operator import xor from subprocess import PIPE, call import pkg_resources +import pytz 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 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 @@ -27,8 +25,8 @@ from sapl.base.models import Autor, TipoAutor, cria_models_tipo_autor from sapl.comissoes.models import Comissao, Composicao, Participacao from sapl.legacy.models import NormaJuridica as OldNormaJuridica from sapl.legacy.models import TipoNumeracaoProtocolo -from sapl.materia.models import (AcompanhamentoMateria, Proposicao, - StatusTramitacao, TipoDocumento, +from sapl.materia.models import (AcompanhamentoMateria, MateriaLegislativa, + Proposicao, StatusTramitacao, TipoDocumento, TipoMateriaLegislativa, TipoProposicao, Tramitacao) from sapl.norma.models import (AssuntoNorma, NormaJuridica, NormaRelacionada, @@ -117,12 +115,31 @@ models_novos_para_antigos = { for model in field_renames} models_novos_para_antigos[Composicao] = models_novos_para_antigos[Participacao] +content_types = {model: ContentType.objects.get( + app_label=model._meta.app_label, model=model._meta.model_name) + for model in field_renames} campos_novos_para_antigos = { model._meta.get_field(nome_novo): nome_antigo for model, renames in field_renames.items() for nome_novo, nome_antigo in renames.items()} +# campos de Composicao (de Comissao) +for nome_novo, nome_antigo in (('comissao', 'cod_comissao'), + ('periodo', 'cod_periodo_comp')): + campos_novos_para_antigos[ + Composicao._meta.get_field(nome_novo)] = nome_antigo + + +# campos virtuais de Proposicao para funcionarem com get_fk_related +CampoFalso = namedtuple('CampoFalso', ['model', 'related_model']) +CAMPOS_FALSOS_PROPOSICAO = { + TipoMateriaLegislativa: CampoFalso(Proposicao, MateriaLegislativa), + TipoDocumento: CampoFalso(Proposicao, DocumentoAdministrativo) +} +for campo_falso in CAMPOS_FALSOS_PROPOSICAO.values(): + campos_novos_para_antigos[campo_falso] = 'cod_mat_ou_doc' + # MIGRATION ################################################################# @@ -168,10 +185,14 @@ class ForeignKeyFaltando(ObjectDoesNotExist): campo = campos_novos_para_antigos[self.field] _, tabela, campos_pk = get_estrutura_legado(self.field.model) pk = {c: getattr(self.old, c) for c in campos_pk} + sql = 'select * from {} where {}'.format( + tabela, + ' and '.join(['{} = {}'.format(k, v) for k, v in pk.items()])) return OrderedDict((('campo', campo), ('valor', self.valor), ('tabela', tabela), - ('pk', pk))) + ('pk', pk), + ('sql', sql))) @lru_cache() @@ -180,8 +201,8 @@ def _get_all_ids_from_model(model): return set(model.objects.values_list('id', flat=True)) -def get_fk_related(field, old, old_field_name): - valor = getattr(old, old_field_name) +def get_fk_related(field, old): + valor = getattr(old, campos_novos_para_antigos[field]) if valor is None and field.null: return None if valor in _get_all_ids_from_model(field.related_model): @@ -679,208 +700,197 @@ def dict_representer(dumper, data): yaml.add_representer(OrderedDict, dict_representer) -class DataMigrator: +# configura timezone de migração +nome_banco_legado = DATABASES['legacy']['NAME'] +match = re.match('sapl_cm_(.*)', nome_banco_legado) +sigla_casa = match.group(1) +with open(PATH_TABELA_TIMEZONES, 'r') as arq: + tabela_timezones = yaml.load(arq) +municipio, uf, nome_timezone = tabela_timezones[sigla_casa] +if nome_timezone: + timezone = pytz.timezone(nome_timezone) +else: + timezone = get_timezone(municipio, uf) - def __init__(self): - self.choice_valida = {} - # configura timezone de migração - 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) - 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 = field_renames[type(new)] - - for field in new._meta.fields: - old_field_name = renames.get(field.name) - if old_field_name: - field_type = field.get_internal_type() - - if field_type == 'ForeignKey': - fk_field_name = '{}_id'.format(field.name) - value = get_fk_related(field, old, old_field_name) - 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 +def populate_renamed_fields(new, old): + renames = field_renames[type(new)] + + for field in new._meta.fields: + old_field_name = renames.get(field.name) + if old_field_name: + field_type = field.get_internal_type() + + if field_type == 'ForeignKey': + fk_field_name = '{}_id'.format(field.name) + value = get_fk_related(field, old) + setattr(new, fk_field_name, value) else: - info('Migração cancelada.') - return 0 - info('Excluindo entradas antigas do banco destino.') - 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) - try: - ocorrencias.clear() - dir_ocorrencias = DIR_RESULTADOS.child(date.today().isoformat()) - dir_ocorrencias.mkdir(parents=True) - self._do_migrate(obj) - except Exception as e: - ocorrencias['traceback'] = str(traceback.format_exc()) - raise e - finally: - # grava ocorrências - arq_ocorrencias = dir_ocorrencias.child( - self.nome_banco_legado + '.yaml') - with open(arq_ocorrencias, 'w') as arq: - dump = yaml.dump(dict(ocorrencias), allow_unicode=True) - arq.write(dump.replace('\n- ', '\n\n- ')) - 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() - - def _do_migrate(self, obj): - if isinstance(obj, AppConfig): - models = [model for model in obj.models.values() - if model in 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): - self.migrate_model(obj) - elif hasattr(obj, '__iter__'): - for item in obj: - self._do_migrate(item) + 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 = timezone.localize(value) + if campo_tempo_sem_timezone('TimeField'): + value = value.replace(tzinfo=timezone) + + setattr(new, field.name, value) + + +def migrar_dados(interativo=True): + 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 destino.') + 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: ...') + try: + ocorrencias.clear() + dir_ocorrencias = DIR_RESULTADOS.child(date.today().isoformat()) + dir_ocorrencias.mkdir(parents=True) + migrar_todos_os_models() + except Exception as e: + ocorrencias['traceback'] = str(traceback.format_exc()) + raise e + finally: + # grava ocorrências + arq_ocorrencias = dir_ocorrencias.child( + nome_banco_legado + '.yaml') + with open(arq_ocorrencias, 'w') as arq: + dump = yaml.dump(dict(ocorrencias), allow_unicode=True) + arq.write(dump.replace('\n- ', '\n\n- ')) + 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() + + +def move_para_depois_de(lista, movido, referencias): + indice_inicial = lista.index(movido) + lista.remove(movido) + indice_apos_refs = max(lista.index(r) for r in referencias) + 1 + lista.insert(max(indice_inicial, indice_apos_refs), movido) + return lista + + +def migrar_todos_os_models(): + models = [model for app in appconfs for model in app.models.values() + if model in field_renames] + # Devido à referência TipoProposicao.tipo_conteudo_related + # a migração de TipoProposicao precisa ser feita + # após TipoMateriaLegislativa e TipoDocumento + # (porém antes de Proposicao) + move_para_depois_de(models, TipoProposicao, + [TipoMateriaLegislativa, TipoDocumento]) + assert models.index(TipoProposicao) < models.index(Proposicao) + move_para_depois_de(models, Proposicao, + [MateriaLegislativa, DocumentoAdministrativo]) + + for model in models: + migrar_model(model) + + +def migrar_model(model): + print('Migrando %s...' % model.__name__) + + model_legado, tabela_legado, campos_pk_legado = \ + get_estrutura_legado(model) + + if len(campos_pk_legado) == 1: + # a pk no legado tem um único campo + nome_pk = model_legado._meta.pk.name + if 'ind_excluido' in {f.name for f in model_legado._meta.fields}: + # se o model legado tem o campo ind_excluido + # enumera apenas os não excluídos + old_records = model_legado.objects.filter(~Q(ind_excluido=1)) else: - raise TypeError( - 'Parameter must be a Model, AppConfig or a sequence of them') - - def migrate_model(self, model): - print('Migrando %s...' % model.__name__) - - model_legado, tabela_legado, campos_pk_legado = \ - get_estrutura_legado(model) - - if len(campos_pk_legado) == 1: - # a pk no legado tem um único campo - nome_pk = model_legado._meta.pk.name - if 'ind_excluido' in {f.name for f in model_legado._meta.fields}: - # se o model legado tem o campo ind_excluido - # enumera apenas os não excluídos - old_records = model_legado.objects.filter(~Q(ind_excluido=1)) + 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(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(): + novos = [] + sql_delete_legado = '' + for old in old_records: + new = model() + try: + populate_renamed_fields(new, old) + if ajuste_antes_salvar: + ajuste_antes_salvar(new, old) + except ForeignKeyFaltando as e: + # tentamos preencher uma FK e o ojeto relacionado + # não existe + # então este é um objeo órfão: simplesmente ignoramos + warn('fk', e.msg, e.dados) + continue else: - old_records = model_legado.objects.all() - old_records = old_records.order_by(nome_pk) + if get_id_do_legado: + new.id = get_id_do_legado(old) - 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(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(): - novos = [] - sql_delete_legado = '' - for old in old_records: - 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('fk', e.msg, e.dados) - continue - else: - if get_id_do_legado: - new.id = get_id_do_legado(old) - - new.clean() # valida model - novos.append(new) # guarda para salvar - - # 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_legado)) - - # salva novos registros - with reversion.create_revision(): - model.objects.bulk_create(novos) - reversion.set_comment('Objetos criados pela migração') + new.clean() # valida model + novos.append(new) # guarda para salvar - if ajuste_depois_salvar: - ajuste_depois_salvar() + # 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_legado)) - # 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) + # salva novos registros + with reversion.create_revision(): + model.objects.bulk_create(novos) + reversion.set_comment('Objetos criados pela migração') - # apaga registros migrados do legado - if sql_delete_legado: - exec_legado(sql_delete_legado) + if ajuste_depois_salvar: + ajuste_depois_salvar() + # 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) -def migrar_dados(obj=appconfs, interativo=True): - dm = DataMigrator() - dm.migrar(obj, interativo) + # apaga registros migrados do legado + if sql_delete_legado: + exec_legado(sql_delete_legado) # MIGRATION_ADJUSTMENTS ##################################################### @@ -993,9 +1003,8 @@ def adjust_parlamentar(new, old): def adjust_participacao(new, old): comissao_id, periodo_id = [ - get_fk_related(Composicao._meta.get_field(name), old, old_field_name) - for name, old_field_name in (('comissao', 'cod_comissao'), - ('periodo', 'cod_periodo_comp'))] + get_fk_related(Composicao._meta.get_field(name), old) + for name in ('comissao', 'periodo')] with reversion.create_revision(): composicao, _ = Composicao.objects.get_or_create( comissao_id=comissao_id, periodo_id=periodo_id) @@ -1003,11 +1012,6 @@ def adjust_participacao(new, old): 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): new.tipo_vinculo = TipoVinculoNormaJuridica.objects.get( sigla=old.tip_vinculo) @@ -1042,14 +1046,14 @@ def adjust_tipoafastamento(new, old): new.indicador = 'F' -MODEL_TIPO_MATERIA_OU_DOCUMENTO = {'M': TipoMateriaLegislativa, - 'D': TipoDocumento} +TIPO_MATERIA_OU_TIPO_DOCUMENTO = {'M': TipoMateriaLegislativa, + 'D': TipoDocumento} def adjust_tipoproposicao(new, old): "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] + model_tipo = TIPO_MATERIA_OU_TIPO_DOCUMENTO[old.ind_mat_ou_doc] tipo = model_tipo.objects.filter(pk=value) if tipo: new.tipo_conteudo_related = tipo[0] @@ -1060,6 +1064,16 @@ def adjust_tipoproposicao(new, old): label={'ind_mat_ou_doc': old.ind_mat_ou_doc}) +def adjust_proposicao_antes_salvar(new, old): + if new.data_envio: + new.ano = new.data_envio.year + if old.cod_mat_ou_doc: + tipo_mat_ou_doc = type(new.tipo.tipo_conteudo_related) + campo_falso = CAMPOS_FALSOS_PROPOSICAO[tipo_mat_ou_doc] + new.content_type = content_types[campo_falso.related_model] + new.object_id = get_fk_related(campo_falso, old) + + def adjust_statustramitacao(new, old): if old.ind_fim_tramitacao: new.indicador = 'F' diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 23e4a6e3d..e9b366fa9 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -686,16 +686,12 @@ class Proposicao(models.Model): texto_articulado = GenericRelation( TextoArticulado, related_query_name='texto_articulado') - # FIXME - para a rotina de migração - este campo mudou - # retire o comentário quando resolver materia_de_vinculo = models.ForeignKey( MateriaLegislativa, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Matéria anexadora'), related_name=_('proposicao_set')) - # FIXME - para a rotina de migração - estes campos mudaram - # retire o comentário quando resolver content_type = models.ForeignKey( ContentType, default=None, blank=True, null=True, verbose_name=_('Tipo de Material Gerado'))