diff --git a/sapl/base/legacy.yaml b/sapl/base/legacy.yaml index a54c4ae06..2d866226e 100644 --- a/sapl/base/legacy.yaml +++ b/sapl/base/legacy.yaml @@ -5,4 +5,3 @@ Autor: nome: nom_autor cargo: des_cargo tipo: tip_autor - username: col_username diff --git a/sapl/comissoes/legacy.yaml b/sapl/comissoes/legacy.yaml index a1093c4d2..66ed8b060 100644 --- a/sapl/comissoes/legacy.yaml +++ b/sapl/comissoes/legacy.yaml @@ -25,7 +25,6 @@ Comissao: telefone_secretaria: num_tel_secretaria tipo: tip_comissao unidade_deliberativa: ind_unid_deliberativa - ativa: Periodo (PeriodoCompComissao): data_fim: dat_fim_periodo diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 6aa051e62..22abf6dd1 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -1,31 +1,32 @@ import re -from collections import defaultdict +import traceback +from collections import OrderedDict, defaultdict, namedtuple from datetime import date from functools import lru_cache, partial from itertools import groupby +from operator import xor 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 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, @@ -68,8 +69,6 @@ for a1, s1 in name_sets: else: assert not s1.intersection(s2) -legacy_app = apps.get_app_config('legacy') - # RENAMES ################################################################### @@ -108,6 +107,51 @@ def get_renames(): return field_renames, model_renames + +field_renames, model_renames = get_renames() +legacy_app = apps.get_app_config('legacy') +models_novos_para_antigos = { + model: legacy_app.get_model(model_renames.get(model, model.__name__)) + for model in field_renames} +models_novos_para_antigos[Composicao] = models_novos_para_antigos[Participacao] + +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 funcionar com get_fk_related +class CampoVirtual(namedtuple('CampoVirtual', 'model related_model')): + null = True + +CAMPOS_VIRTUAIS_PROPOSICAO = { + TipoMateriaLegislativa: CampoVirtual(Proposicao, MateriaLegislativa), + TipoDocumento: CampoVirtual(Proposicao, DocumentoAdministrativo) +} +for campo_virtual in CAMPOS_VIRTUAIS_PROPOSICAO.values(): + campos_novos_para_antigos[campo_virtual] = 'cod_mat_ou_doc' + +# campos virtuais de Autor para funcionar com get_fk_related +CAMPOS_VIRTUAIS_AUTOR = {related: CampoVirtual(Autor, related) + for related in (Parlamentar, Comissao, Partido)} +for related, campo_antigo in [(Parlamentar, 'cod_parlamentar'), + (Comissao, 'cod_comissao'), + (Partido, 'cod_partido')]: + campo_virtual = CAMPOS_VIRTUAIS_AUTOR[related] + campos_novos_para_antigos[campo_virtual] = campo_antigo + + # MIGRATION ################################################################# @@ -122,22 +166,48 @@ def warn(tipo, msg, dados): print('CUIDADO! ' + msg.format(**dados)) +@lru_cache() +def get_pk_legado(tabela): + if tabela == 'despacho_inicial': + # adaptação para deleção correta no mysql ao final de migrar_model + # acompanha o agrupamento de despacho_inicial feito em iter_sql_records + return 'cod_materia', 'cod_comissao' + res = exec_legado( + 'show index from {} WHERE Key_name = "PRIMARY"'.format(tabela)) + return [r[4] for r in res] + + +@lru_cache() +def get_estrutura_legado(model): + model_legado = models_novos_para_antigos[model] + tabela_legado = model_legado._meta.db_table + campos_pk_legado = get_pk_legado(tabela_legado) + return model_legado, tabela_legado, campos_pk_legado + + class ForeignKeyFaltando(ObjectDoesNotExist): 'Uma FK aponta para um registro inexistente' - def __init__(self, field, value, label): + def __init__(self, field, valor, old): self.field = field - self.value = value - self.label = label + self.valor = valor + self.old = old - msg = 'FK [{field}] não encontrada para o valor {value} (em {model} / {label})' # noqa + msg = 'FK não encontrada para [{campo} = {valor}] (em {tabela} / pk = {pk})' # noqa @property def dados(self): - return {'field': self.field.name, - 'value': self.value, - 'model': self.field.model.__name__, - 'label': self.label} + 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), + ('sql', sql))) @lru_cache() @@ -146,18 +216,17 @@ def _get_all_ids_from_model(model): return set(model.objects.values_list('id', flat=True)) -def get_fk_related(field, value, label='---'): - if value is None and field.null: +def get_fk_related(field, old): + valor = getattr(old, campos_novos_para_antigos[field]) + if valor 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: + if valor in _get_all_ids_from_model(field.related_model): + return valor + elif valor == 0 and field.null: # consideramos zeros como nulos, se não está entre os ids anteriores return None else: - raise ForeignKeyFaltando(field=field, value=value, label=label) + raise ForeignKeyFaltando(field=field, valor=valor, old=old) def exec_sql(sql, db='default'): @@ -552,9 +621,21 @@ relatoria | tip_fim_relatoria = NULL | tip_fim_relatoria = 0 anula_tipos_origem_externa_invalidos() -def iter_sql_records(sql): - class Record: - pass +class Record: + pass + + +def iter_sql_records(tabela): + if tabela == 'despacho_inicial': + sql = ''' select cod_materia, cod_comissao from despacho_inicial + where ind_excluido <> 1 + group by cod_materia, cod_comissao + order by cod_materia, min(num_ordem) + ''' + else: + sql = 'select * from ' + tabela + if existe_coluna_no_legado(tabela, 'ind_excluido'): + sql += ' where ind_excluido <> 1' cursor = exec_legado(sql) fieldnames = [name[0] for name in cursor.description] for row in cursor.fetchall(): @@ -624,225 +705,207 @@ def reinicia_sequence(model, id): 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] - - 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: +def dict_representer(dumper, data): + return dumper.represent_dict(data.items()) +yaml.add_representer(OrderedDict, dict_representer) - def __init__(self): - self.field_renames, self.model_renames = get_renames() - 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) +# 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 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) +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 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 = 'pk = {}'.format(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 + + 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) - 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() - - def _do_migrate(self, obj): - if isinstance(obj, AppConfig): - 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): - 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: - 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 - 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)) + 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: + 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 - 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) - 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: - 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) - # 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) + 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') + + 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) + + # apaga registros migrados do legado + if sql_delete_legado: + exec_legado(sql_delete_legado) # MIGRATION_ADJUSTMENTS ##################################################### @@ -954,37 +1017,19 @@ 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, - '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 - 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') + comissao_id, periodo_id = [ + get_fk_related(Composicao._meta.get_field(name), old) + for name in ('comissao', 'periodo')] + with reversion.create_revision(): + composicao, _ = Composicao.objects.get_or_create( + comissao_id=comissao_id, periodo_id=periodo_id) + reversion.set_comment('Objeto criado pela migração') new.composicao = composicao -def adjust_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] + new.tipo_vinculo = TipoVinculoNormaJuridica.objects.get( + sigla=old.tip_vinculo) def adjust_protocolo_antes_salvar(new, old): @@ -1009,18 +1054,21 @@ def adjust_registrovotacao_antes_salvar(new, old): def adjust_tipoafastamento(new, old): - if old.ind_afastamento == 1: + assert xor(old.ind_afastamento, old.ind_fim_mandato) + if old.ind_afastamento: new.indicador = 'A' + elif old.ind_fim_mandato: + 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] @@ -1028,7 +1076,17 @@ def adjust_tipoproposicao(new, old): raise ForeignKeyFaltando( field=TipoProposicao.tipo_conteudo_related, value=(model_tipo.__name__, value), - label='ind_mat_ou_doc = {}'.format(old.ind_mat_ou_doc)) + 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_virtual = CAMPOS_VIRTUAIS_PROPOSICAO[tipo_mat_ou_doc] + new.content_type = content_types[campo_virtual.related_model] + new.object_id = get_fk_related(campo_virtual, old) def adjust_statustramitacao(new, old): @@ -1067,47 +1125,40 @@ def adjust_normajuridica_antes_salvar(new, old): new.esfera_federacao = '' -def adjust_normajuridica_depois_salvar(new, old): +def adjust_normajuridica_depois_salvar(): # Ajusta relação M2M + ligacao = NormaJuridica.assuntos.through - if not old.cod_assunto: # it can be null or empty - return + assuntos_migrados, normas_migradas = [ + set(model.objects.values_list('id', flat=True)) + for model in [AssuntoNorma, NormaJuridica]] - # 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 - nome_model_relacionado = model_relacionado._meta.model.__name__ - raise ForeignKeyFaltando( - 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 + def filtra_assuntos_migrados(cod_assunto): + return [a for a in map(int, cod_assunto.split(',')) + if a in assuntos_migrados] + + norma_para_assuntos = [ + (norma, filtra_assuntos_migrados(cod_assunto)) + for norma, cod_assunto in OldNormaJuridica.objects.filter( + pk__in=normas_migradas).values_list('pk', 'cod_assunto')] + + ligacao.objects.bulk_create( + ligacao(normajuridica_id=norma, assuntonorma_id=assunto) + for norma, assuntos in norma_para_assuntos + for assunto in assuntos) 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): + # vincula autor com o objeto relacionado, tentando os três campos antigos + # o primeiro campo preenchido será usado, podendo lançar ForeignKeyFaltando + for model_relacionado, campo_nome in [(Parlamentar, 'nome_parlamentar'), + (Comissao, 'nome'), + (Partido, 'nome')]: + field = CAMPOS_VIRTUAIS_AUTOR[model_relacionado] + fk_encontrada = get_fk_related(field, old) + if fk_encontrada: + new.autor_related = model_relacionado.objects.get(id=fk_encontrada) + new.nome = getattr(new.autor_related, campo_nome) break if old.col_username: diff --git a/sapl/legacy/scripts/exporta_zope/exporta_zope.py b/sapl/legacy/scripts/exporta_zope/exporta_zope.py index 31c4c3188..11ff1ecbb 100755 --- a/sapl/legacy/scripts/exporta_zope/exporta_zope.py +++ b/sapl/legacy/scripts/exporta_zope/exporta_zope.py @@ -14,7 +14,6 @@ from functools import partial import magic import yaml - import ZODB.DB import ZODB.FileStorage from ZODB.broken import Broken @@ -191,7 +190,9 @@ def read_sde(element): ] if meta_type != 'Script (Python)': # ignoramos os scrips python de eventos dos templates - yield id, read_sde(obj) + yield {'id': id, + 'meta_type': meta_type, + 'dados': read_sde(obj)} data = dict(read_properties()) children = list(read_children()) diff --git a/sapl/legacy/scripts/migra_um_db.sh b/sapl/legacy/scripts/migra_um_db.sh index 577a2c000..ed4db9662 100755 --- a/sapl/legacy/scripts/migra_um_db.sh +++ b/sapl/legacy/scripts/migra_um_db.sh @@ -39,7 +39,7 @@ if [ $# -ge 2 ]; then echo "--- MIGRACAO ---" | tee -a $LOG echo >> $LOG - DATABASE_NAME=$1 ./manage.py migracao_25_31 --force --settings sapl.legacy_migration_settings 2>&1 | 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:" diff --git a/sapl/legacy/test_renames.py b/sapl/legacy/test_renames.py index 7a8766da0..573129725 100644 --- a/sapl/legacy/test_renames.py +++ b/sapl/legacy/test_renames.py @@ -1,29 +1,76 @@ -import sapl.comissoes -import sapl.materia -import sapl.norma -import sapl.sessao + +from django.contrib.contenttypes.fields import GenericForeignKey + +from sapl.base.models import AppConfig, Autor, CasaLegislativa, TipoAutor +from sapl.comissoes.models import \ + DocumentoAcessorio as DocumentoAcessorioComissoes +from sapl.comissoes.models import Comissao, Composicao, Participacao, Reuniao +from sapl.materia.models import (AcompanhamentoMateria, DocumentoAcessorio, + MateriaLegislativa, Proposicao, + TipoMateriaLegislativa, TipoProposicao, + Tramitacao) +from sapl.norma.models import (NormaJuridica, NormaRelacionada, + TipoVinculoNormaJuridica) +from sapl.parlamentares.models import (Frente, Mandato, Parlamentar, Partido, + TipoAfastamento, Votante) +from sapl.protocoloadm.models import DocumentoAdministrativo +from sapl.sessao.models import (Bancada, Bloco, CargoBancada, + ExpedienteMateria, Orador, OradorExpediente, + OrdemDia, RegistroVotacao, ResumoOrdenacao, + SessaoPlenaria, TipoResultadoVotacao, + VotoParlamentar) from .migracao_dados import appconfs, get_renames, legacy_app RENAMING_IGNORED_MODELS = [ - sapl.comissoes.models.Composicao, - sapl.norma.models.AssuntoNormaRelationship, + Votante, Frente, Bancada, CargoBancada, Bloco, # parlamentares + Composicao, Reuniao, DocumentoAcessorioComissoes, # commissoes + AppConfig, CasaLegislativa, # base + ResumoOrdenacao, # sessao + TipoVinculoNormaJuridica, # norma - # FIXME retirar daqui depois que a issue #218 for resolvida!!!!!!! - sapl.sessao.models.AcompanharMateria, ] RENAMING_IGNORED_FIELDS = [ - (sapl.comissoes.models.Participacao, {'composicao'}), - (sapl.materia.models.Proposicao, {'documento'}), - (sapl.materia.models.TipoProposicao, {'tipo_documento'}), - (sapl.materia.models.Tramitacao, {'ultima'}), - (sapl.sessao.models.SessaoPlenaria, {'finalizada', - 'upload_pauta', - 'upload_ata', - 'iniciada'}), - (sapl.sessao.models.ExpedienteMateria, {'votacao_aberta'}), - (sapl.sessao.models.OrdemDia, {'votacao_aberta'}), + (TipoAfastamento, {'indicador'}), + (Participacao, {'composicao'}), + (Proposicao, { + 'ano', 'content_type', 'object_id', 'conteudo_gerado_related', + 'status', 'hash_code', 'texto_original'}), + (TipoProposicao, { + 'object_id', 'content_type', 'tipo_conteudo_related', 'perfis', + # não estou entendendo como esses campos são enumerados, + # mas eles não fazem parte da migração + # 'tipomaterialegislativa_set', 'tipodocumento_set', + }), + + (Tramitacao, {'ultima'}), + (SessaoPlenaria, {'finalizada', 'iniciada', 'painel_aberto', 'interativa', + 'upload_ata', + 'upload_anexo', + 'upload_pauta'}), + (ExpedienteMateria, {'votacao_aberta'}), + (OrdemDia, {'votacao_aberta'}), + (NormaJuridica, {'texto_integral', 'data_ultima_atualizacao', 'assuntos'}), + (Parlamentar, { + 'uf_residencia', 'municipio_residencia', 'cropping', 'fotografia'}), + (Partido, {'logo_partido'}), + (MateriaLegislativa, { + 'autores', 'anexadas', 'data_ultima_atualizacao', 'texto_original'}), + (DocumentoAdministrativo, {'protocolo', 'texto_integral'}), + (Mandato, {'titular', 'data_fim_mandato', 'data_inicio_mandato'}), + (TipoMateriaLegislativa, {'sequencia_numeracao'}), + (TipoAutor, {'content_type'}), + (TipoResultadoVotacao, {'natureza'}), + (RegistroVotacao, {'ordem', 'expediente'}), + (DocumentoAcessorio, {'arquivo', 'data_ultima_atualizacao'}), + (OradorExpediente, {'upload_anexo', 'observacao'}), + (Orador, {'upload_anexo', 'observacao'}), + (VotoParlamentar, {'user', 'ip', 'expediente', 'data_hora', 'ordem'}), + (NormaRelacionada, {'tipo_vinculo'}), + (AcompanhamentoMateria, {'confirmado', 'data_cadastro', 'usuario'}), + (Autor, {'user', 'content_type', 'object_id', 'autor_related'}), + (Comissao, {'ativa'}), ] @@ -31,7 +78,9 @@ def test_get_renames(): field_renames, model_renames = get_renames() all_models = {m for ac in appconfs for m in ac.get_models()} for model in all_models: - field_names = {f.name for f in model._meta.fields if f.name != 'id'} + field_names = {f.name for f in model._meta.get_fields() + if f.name != 'id' + and (f.concrete or isinstance(f, GenericForeignKey))} if model not in field_renames: # check ignored models in renaming assert model in RENAMING_IGNORED_MODELS @@ -46,11 +95,12 @@ def test_get_renames(): match_msg_template % ('new', 'current') # ignored fields are explicitly listed - missing_in_renames = field_names - renamed - if missing_in_renames: - assert (model, missing_in_renames) in \ - RENAMING_IGNORED_FIELDS, \ - 'Field(s) missing in renames but not explicitly listed' + missing = field_names - renamed + if missing: + assert (model, missing) in RENAMING_IGNORED_FIELDS, \ + 'Campos faltando na renomeação,' \ + 'mas não listados explicitamente: ({}, {})'.format( + model.__name__, missing) # all old names correspond to a legacy field legacy_model = legacy_app.get_model( diff --git a/sapl/materia/legacy.yaml b/sapl/materia/legacy.yaml index db2c8490f..9ff81287f 100644 --- a/sapl/materia/legacy.yaml +++ b/sapl/materia/legacy.yaml @@ -58,9 +58,8 @@ AssuntoMateria: dispositivo: des_dispositivo DespachoInicial: - comissao: cod_comissao materia: cod_materia - numero_ordem: num_ordem + comissao: cod_comissao TipoDocumento: descricao: des_tipo_documento @@ -112,10 +111,6 @@ Parecer: TipoProposicao: descricao: des_tipo_proposicao - materia_ou_documento: ind_mat_ou_doc - modelo: nom_modelo - tipo_documento: - tipo_materia: Proposicao: autor: cod_autor @@ -124,7 +119,7 @@ Proposicao: data_recebimento: dat_recebimento descricao: txt_descricao justificativa_devolucao: txt_justif_devolucao - materia: cod_mat_ou_doc + materia_de_vinculo: cod_materia numero_proposicao: num_proposicao tipo: tip_proposicao diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 34fd44d55..e9b366fa9 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -40,9 +40,6 @@ class TipoProposicao(models.Model): error_messages={ 'unique': _('Já existe um Tipo de Proposição com esta descrição.') }) - - # FIXME - para a rotina de migração - estes campos mudaram - # retire o comentário quando resolver content_type = models.ForeignKey(ContentType, default=None, on_delete=models.PROTECT, verbose_name=_('Definição de Tipo')) @@ -378,9 +375,6 @@ class AssuntoMateria(models.Model): @reversion.register() class DespachoInicial(models.Model): - # TODO M2M? - # TODO Despachos não são necessáriamente comissoes, podem ser outros - # órgãos, ex: procuradorias materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) comissao = models.ForeignKey(Comissao, on_delete=models.CASCADE) @@ -692,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')) diff --git a/sapl/norma/legacy.yaml b/sapl/norma/legacy.yaml index 3294561b8..23bf5bb80 100644 --- a/sapl/norma/legacy.yaml +++ b/sapl/norma/legacy.yaml @@ -9,7 +9,6 @@ TipoNormaJuridica: NormaJuridica: ano: ano_norma - assunto: cod_assunto complemento: ind_complemento data: dat_norma data_publicacao: dat_publicacao diff --git a/sapl/parlamentares/legacy.yaml b/sapl/parlamentares/legacy.yaml index acaca47de..fc72431f5 100644 --- a/sapl/parlamentares/legacy.yaml +++ b/sapl/parlamentares/legacy.yaml @@ -38,7 +38,6 @@ Parlamentar: ativo: ind_ativo biografia: txt_biografia cep_residencia: num_cep_resid - cod_casa: cod_casa cpf: num_cpf data_nascimento: dat_nascimento email: end_email @@ -58,7 +57,6 @@ Parlamentar: telefone: num_tel_parlamentar telefone_residencia: num_tel_resid titulo_eleitor: num_tit_eleitor - unidade_deliberativa: ind_unid_deliberativa TipoDependente: descricao: des_tipo_dependente @@ -80,10 +78,8 @@ Filiacao: partido: cod_partido TipoAfastamento: - afastamento: ind_afastamento descricao: des_afastamento dispositivo: des_dispositivo - fim_mandato: ind_fim_mandato Mandato: coligacao: cod_coligacao diff --git a/sapl/protocoloadm/legacy.yaml b/sapl/protocoloadm/legacy.yaml index 3772d2f6d..7dae79f73 100644 --- a/sapl/protocoloadm/legacy.yaml +++ b/sapl/protocoloadm/legacy.yaml @@ -11,7 +11,6 @@ DocumentoAdministrativo: dias_prazo: num_dias_prazo interessado: txt_interessado numero: num_documento - numero_protocolo: num_protocolo observacao: txt_observacao tipo: tip_documento tramitacao: ind_tramitacao diff --git a/sapl/sessao/legacy.yaml b/sapl/sessao/legacy.yaml index 90188a76c..e7b55095a 100644 --- a/sapl/sessao/legacy.yaml +++ b/sapl/sessao/legacy.yaml @@ -52,7 +52,6 @@ OradorExpediente (OradoresExpediente): {} OrdemDia: {} PresencaOrdemDia (OrdemDiaPresenca): - data_ordem: dat_ordem parlamentar: cod_parlamentar sessao_plenaria: cod_sessao_plen diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index 90736d6e0..23155b51a 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -436,7 +436,8 @@ class RegistroVotacao(models.Model): if not xor(bool(self.ordem), bool(self.expediente)): raise ValidationError( 'RegistroVotacao deve ter exatamente um dos campos ' - 'ordem ou expediente deve estar preenchido') + 'ordem ou expediente preenchido. Ambos estão preenchidos: ' + '{}, {}'. format(self.ordem, self.expediente)) @reversion.register()