Browse Source

Migra conteudo_gerado_related de Proposicao

pull/1847/head
Marcio Mazza 7 years ago
parent
commit
901c0ec043
  1. 436
      sapl/legacy/migracao_dados.py
  2. 4
      sapl/materia/models.py

436
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'

4
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'))

Loading…
Cancel
Save