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 re
import traceback import traceback
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict, namedtuple
from datetime import date from datetime import date
from functools import lru_cache, partial from functools import lru_cache, partial
from itertools import groupby from itertools import groupby
@ -8,18 +8,16 @@ from operator import xor
from subprocess import PIPE, call from subprocess import PIPE, call
import pkg_resources import pkg_resources
import pytz
import reversion import reversion
import yaml import yaml
from django.apps import apps from django.apps import apps
from django.apps.config import AppConfig
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import connections, transaction from django.db import connections, transaction
from django.db.models import Max, Q from django.db.models import Max, Q
from django.db.models.base import ModelBase
from pytz import timezone
from unipath import Path from unipath import Path
from sapl.base.models import AppConfig as AppConf 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.comissoes.models import Comissao, Composicao, Participacao
from sapl.legacy.models import NormaJuridica as OldNormaJuridica from sapl.legacy.models import NormaJuridica as OldNormaJuridica
from sapl.legacy.models import TipoNumeracaoProtocolo from sapl.legacy.models import TipoNumeracaoProtocolo
from sapl.materia.models import (AcompanhamentoMateria, Proposicao, from sapl.materia.models import (AcompanhamentoMateria, MateriaLegislativa,
StatusTramitacao, TipoDocumento, Proposicao, StatusTramitacao, TipoDocumento,
TipoMateriaLegislativa, TipoProposicao, TipoMateriaLegislativa, TipoProposicao,
Tramitacao) Tramitacao)
from sapl.norma.models import (AssuntoNorma, NormaJuridica, NormaRelacionada, from sapl.norma.models import (AssuntoNorma, NormaJuridica, NormaRelacionada,
@ -117,12 +115,31 @@ models_novos_para_antigos = {
for model in field_renames} for model in field_renames}
models_novos_para_antigos[Composicao] = models_novos_para_antigos[Participacao] 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 = { campos_novos_para_antigos = {
model._meta.get_field(nome_novo): nome_antigo model._meta.get_field(nome_novo): nome_antigo
for model, renames in field_renames.items() for model, renames in field_renames.items()
for nome_novo, nome_antigo in 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 ################################################################# # MIGRATION #################################################################
@ -168,10 +185,14 @@ class ForeignKeyFaltando(ObjectDoesNotExist):
campo = campos_novos_para_antigos[self.field] campo = campos_novos_para_antigos[self.field]
_, tabela, campos_pk = get_estrutura_legado(self.field.model) _, tabela, campos_pk = get_estrutura_legado(self.field.model)
pk = {c: getattr(self.old, c) for c in campos_pk} 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), return OrderedDict((('campo', campo),
('valor', self.valor), ('valor', self.valor),
('tabela', tabela), ('tabela', tabela),
('pk', pk))) ('pk', pk),
('sql', sql)))
@lru_cache() @lru_cache()
@ -180,8 +201,8 @@ def _get_all_ids_from_model(model):
return set(model.objects.values_list('id', flat=True)) return set(model.objects.values_list('id', flat=True))
def get_fk_related(field, old, old_field_name): def get_fk_related(field, old):
valor = getattr(old, old_field_name) valor = getattr(old, campos_novos_para_antigos[field])
if valor is None and field.null: if valor is None and field.null:
return None return None
if valor in _get_all_ids_from_model(field.related_model): 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) 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 def populate_renamed_fields(new, old):
self.nome_banco_legado = DATABASES['legacy']['NAME'] renames = field_renames[type(new)]
match = re.match('sapl_cm_(.*)', self.nome_banco_legado)
sigla_casa = match.group(1) for field in new._meta.fields:
with open(PATH_TABELA_TIMEZONES, 'r') as arq: old_field_name = renames.get(field.name)
tabela_timezones = yaml.load(arq) if old_field_name:
municipio, uf, nome_timezone = tabela_timezones[sigla_casa] field_type = field.get_internal_type()
if nome_timezone:
self.timezone = timezone(nome_timezone) if field_type == 'ForeignKey':
else: fk_field_name = '{}_id'.format(field.name)
self.timezone = get_timezone(municipio, uf) value = get_fk_related(field, old)
setattr(new, fk_field_name, value)
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
else: else:
info('Migração cancelada.') value = getattr(old, old_field_name)
return 0
info('Excluindo entradas antigas do banco destino.') if (field_type in ['CharField', 'TextField']
call([PROJECT_DIR.child('manage.py'), 'flush', and value in [None, 'None']):
'--database=default', '--no-input'], stdout=PIPE) value = ''
# apaga tipos de autor padrão (criados no flush acima) # adiciona timezone faltante aos campos com tempo
TipoAutor.objects.all().delete() # os campos TIMESTAMP do mysql são gravados em UTC
# os DATETIME e TIME não têm timezone
fill_vinculo_norma_juridica() def campo_tempo_sem_timezone(tipo):
fill_dados_basicos() return (field_type == tipo
info('Começando migração: %s...' % obj) and value and not value.tzinfo)
try: if campo_tempo_sem_timezone('DateTimeField'):
ocorrencias.clear() value = timezone.localize(value)
dir_ocorrencias = DIR_RESULTADOS.child(date.today().isoformat()) if campo_tempo_sem_timezone('TimeField'):
dir_ocorrencias.mkdir(parents=True) value = value.replace(tzinfo=timezone)
self._do_migrate(obj)
except Exception as e: setattr(new, field.name, value)
ocorrencias['traceback'] = str(traceback.format_exc())
raise e
finally: def migrar_dados(interativo=True):
# grava ocorrências uniformiza_banco()
arq_ocorrencias = dir_ocorrencias.child(
self.nome_banco_legado + '.yaml') # excluindo database antigo.
with open(arq_ocorrencias, 'w') as arq: if interativo:
dump = yaml.dump(dict(ocorrencias), allow_unicode=True) info('Todos os dados do banco serão excluidos. '
arq.write(dump.replace('\n- ', '\n\n- ')) 'Recomendamos que faça backup do banco sapl '
info('Ocorrências salvas em\n {}'.format(arq_ocorrencias)) 'antes de continuar.')
info('Deseja continuar? [s/n]')
# recria tipos de autor padrão que não foram criados pela migração resposta = input()
cria_models_tipo_autor() if resposta.lower() in ['s', 'sim', 'y', 'yes']:
pass
def _do_migrate(self, obj): else:
if isinstance(obj, AppConfig): info('Migração cancelada.')
models = [model for model in obj.models.values() return 0
if model in field_renames] info('Excluindo entradas antigas do banco destino.')
call([PROJECT_DIR.child('manage.py'), 'flush',
if obj.label == 'materia': '--database=default', '--no-input'], stdout=PIPE)
# Devido à referência TipoProposicao.tipo_conteudo_related
# a migração de TipoProposicao precisa ser feita # apaga tipos de autor padrão (criados no flush acima)
# após TipoMateriaLegislativa e TipoDocumento TipoAutor.objects.all().delete()
# (porém antes de Proposicao)
models.remove(TipoProposicao) fill_vinculo_norma_juridica()
pos_tipo_proposicao = max( fill_dados_basicos()
models.index(TipoMateriaLegislativa), info('Começando migração: ...')
models.index(TipoDocumento)) + 1 try:
models.insert(pos_tipo_proposicao, TipoProposicao) ocorrencias.clear()
assert models.index(TipoProposicao) < models.index(Proposicao) dir_ocorrencias = DIR_RESULTADOS.child(date.today().isoformat())
dir_ocorrencias.mkdir(parents=True)
self._do_migrate(models) migrar_todos_os_models()
elif isinstance(obj, ModelBase): except Exception as e:
self.migrate_model(obj) ocorrencias['traceback'] = str(traceback.format_exc())
elif hasattr(obj, '__iter__'): raise e
for item in obj: finally:
self._do_migrate(item) # 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: else:
raise TypeError( old_records = model_legado.objects.all()
'Parameter must be a Model, AppConfig or a sequence of them') old_records = old_records.order_by(nome_pk)
def migrate_model(self, model): def get_id_do_legado(old):
print('Migrando %s...' % model.__name__) return getattr(old, nome_pk)
else:
model_legado, tabela_legado, campos_pk_legado = \ # a pk no legado tem mais de um campo
get_estrutura_legado(model) old_records = iter_sql_records(tabela_legado)
get_id_do_legado = None
if len(campos_pk_legado) == 1:
# a pk no legado tem um único campo ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model)
nome_pk = model_legado._meta.pk.name ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model)
if 'ind_excluido' in {f.name for f in model_legado._meta.fields}:
# se o model legado tem o campo ind_excluido # convert old records to new ones
# enumera apenas os não excluídos with transaction.atomic():
old_records = model_legado.objects.filter(~Q(ind_excluido=1)) 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: else:
old_records = model_legado.objects.all() if get_id_do_legado:
old_records = old_records.order_by(nome_pk) new.id = get_id_do_legado(old)
def get_id_do_legado(old): new.clean() # valida model
return getattr(old, nome_pk) novos.append(new) # guarda para salvar
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')
if ajuste_depois_salvar: # acumula deleção do registro no legado
ajuste_depois_salvar() 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 # salva novos registros
if get_id_do_legado: with reversion.create_revision():
last_pk = get_last_pk(model) model.objects.bulk_create(novos)
reinicia_sequence(model, last_pk + 1) reversion.set_comment('Objetos criados pela migração')
# apaga registros migrados do legado if ajuste_depois_salvar:
if sql_delete_legado: ajuste_depois_salvar()
exec_legado(sql_delete_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)
def migrar_dados(obj=appconfs, interativo=True): # apaga registros migrados do legado
dm = DataMigrator() if sql_delete_legado:
dm.migrar(obj, interativo) exec_legado(sql_delete_legado)
# MIGRATION_ADJUSTMENTS ##################################################### # MIGRATION_ADJUSTMENTS #####################################################
@ -993,9 +1003,8 @@ def adjust_parlamentar(new, old):
def adjust_participacao(new, old): def adjust_participacao(new, old):
comissao_id, periodo_id = [ comissao_id, periodo_id = [
get_fk_related(Composicao._meta.get_field(name), old, old_field_name) get_fk_related(Composicao._meta.get_field(name), old)
for name, old_field_name in (('comissao', 'cod_comissao'), for name in ('comissao', 'periodo')]
('periodo', 'cod_periodo_comp'))]
with reversion.create_revision(): with reversion.create_revision():
composicao, _ = Composicao.objects.get_or_create( composicao, _ = Composicao.objects.get_or_create(
comissao_id=comissao_id, periodo_id=periodo_id) comissao_id=comissao_id, periodo_id=periodo_id)
@ -1003,11 +1012,6 @@ def adjust_participacao(new, old):
new.composicao = composicao 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): def adjust_normarelacionada(new, old):
new.tipo_vinculo = TipoVinculoNormaJuridica.objects.get( new.tipo_vinculo = TipoVinculoNormaJuridica.objects.get(
sigla=old.tip_vinculo) sigla=old.tip_vinculo)
@ -1042,14 +1046,14 @@ def adjust_tipoafastamento(new, old):
new.indicador = 'F' new.indicador = 'F'
MODEL_TIPO_MATERIA_OU_DOCUMENTO = {'M': TipoMateriaLegislativa, TIPO_MATERIA_OU_TIPO_DOCUMENTO = {'M': TipoMateriaLegislativa,
'D': TipoDocumento} 'D': TipoDocumento}
def adjust_tipoproposicao(new, old): def adjust_tipoproposicao(new, old):
"Aponta para o tipo relacionado de matéria ou documento" "Aponta para o tipo relacionado de matéria ou documento"
value = old.tip_mat_ou_doc 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) tipo = model_tipo.objects.filter(pk=value)
if tipo: if tipo:
new.tipo_conteudo_related = tipo[0] 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}) 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): def adjust_statustramitacao(new, old):
if old.ind_fim_tramitacao: if old.ind_fim_tramitacao:
new.indicador = 'F' new.indicador = 'F'

4
sapl/materia/models.py

@ -686,16 +686,12 @@ class Proposicao(models.Model):
texto_articulado = GenericRelation( texto_articulado = GenericRelation(
TextoArticulado, related_query_name='texto_articulado') 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( materia_de_vinculo = models.ForeignKey(
MateriaLegislativa, blank=True, null=True, MateriaLegislativa, blank=True, null=True,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('Matéria anexadora'), verbose_name=_('Matéria anexadora'),
related_name=_('proposicao_set')) related_name=_('proposicao_set'))
# FIXME - para a rotina de migração - estes campos mudaram
# retire o comentário quando resolver
content_type = models.ForeignKey( content_type = models.ForeignKey(
ContentType, default=None, blank=True, null=True, ContentType, default=None, blank=True, null=True,
verbose_name=_('Tipo de Material Gerado')) verbose_name=_('Tipo de Material Gerado'))

Loading…
Cancel
Save