from datetime import date
from functools import lru_cache, partial
from subprocess import PIPE, call
import re

from django.apps import apps
from django.apps.config import AppConfig
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import connections, transaction
from django.db.models import Count, Max
from django.db.models.base import ModelBase
from sapl.base.models import (Autor, CasaLegislativa, ProblemaMigracao,
                              TipoAutor)
from sapl.base.models import AppConfig as AppConf
from sapl.comissoes.models import Comissao, Composicao, Participacao
from sapl.legacy.models import TipoNumeracaoProtocolo
from sapl.materia.models import (AcompanhamentoMateria, Proposicao,
                                 StatusTramitacao, TipoDocumento,
                                 TipoMateriaLegislativa, TipoProposicao,
                                 Tramitacao)
from sapl.norma.models import (AssuntoNorma, NormaJuridica, NormaRelacionada,
                               TipoVinculoNormaJuridica)
from sapl.parlamentares.models import (Legislatura, Mandato, Parlamentar,
                                       Partido, TipoAfastamento)
from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo,
                                      StatusTramitacaoAdministrativo)
from sapl.sessao.models import ExpedienteMateria, OrdemDia, RegistroVotacao
from sapl.utils import normalize
import pkg_resources
import reversion
import yaml

from cmj.settings import PROJECT_DIR


# BASE ######################################################################
#  apps to be migrated, in app dependency order (very important)
appconfs = [apps.get_app_config(n) for n in [
    'parlamentares',
    'comissoes',
    'base',
    'materia',
    'norma',
    'sessao',
    'lexml',
    'protocoloadm', ]]

unique_constraints = []
one_to_one_constraints = []
primeira_vez = []

name_sets = [set(m.__name__ for m in ac.get_models()) for ac in appconfs]

# apps do not overlap
for s1 in name_sets:
    for s2 in name_sets:
        if s1 is not s2:
            assert not s1.intersection(s2)

# apps include all legacy models
legacy_app = apps.get_app_config('legacy')
legacy_model_names = set(m.__name__ for m in legacy_app.get_models())

model_dict = {m.__name__: m for ac in appconfs for m in ac.get_models()}


# RENAMES ###################################################################

MODEL_RENAME_PATTERN = re.compile('(.+) \((.+)\)')


def get_renames():
    field_renames = {}
    model_renames = {}
    for app in appconfs:
        app_rename_data = yaml.load(
            pkg_resources.resource_string(app.module.__name__, 'legacy.yaml'))
        for model_name, renames in app_rename_data.items():
            match = MODEL_RENAME_PATTERN.match(model_name)
            if match:
                model_name, old_name = match.groups()
            else:
                old_name = None
            model = getattr(app.models_module, model_name)
            if old_name:
                model_renames[model] = old_name
            field_renames[model] = renames

    # collect renames from parent classes
    for model, renames in field_renames.items():
        if any(parent in field_renames for parent in model.__mro__[1:]):
            renames = {}
            for parent in reversed(model.__mro__):
                if parent in field_renames:
                    renames.update(field_renames[parent])
            field_renames[model] = renames

    # remove abstract classes
    field_renames = {m: r for m, r in field_renames.items()
                     if not m._meta.abstract}

    return field_renames, model_renames

# MIGRATION #################################################################


def info(msg):
    print('INFO: ' + msg)


def warn(msg):
    print('CUIDADO! ' + msg)


class ForeignKeyFaltando(ObjectDoesNotExist):
    'Uma FK aponta para um registro inexistente'
    pass


@lru_cache()
def _get_all_ids_from_model(model):
    # esta função para uso apenas em get_fk_related
    return set(model.objects.values_list('id', flat=True))


def get_fk_related(field, value, label=None):
    if value is None and field.null:
        return None

    # if field.related_model.objects.filter(id=value).exists():
    if value in _get_all_ids_from_model(field.related_model):
        return value
    else:
        msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % (
            field.name, value, field.model.__name__, label or '---')
        warn(msg)
        raise ForeignKeyFaltando(msg)


def exec_sql(sql, db='default'):
    cursor = connections[db].cursor()
    cursor.execute(sql)
    return cursor

# UNIFORMIZAÇÃO DO BANCO ANTES DA MIGRAÇÃO ###############################


SQL_NAO_TEM_TABELA = '''
   SELECT count(*)
   FROM information_schema.columns
   WHERE table_schema=database()
     AND TABLE_NAME="{}"
'''
SQL_NAO_TEM_COLUNA = SQL_NAO_TEM_TABELA + ' AND COLUMN_NAME="{}"'

exec_legado = partial(exec_sql, db='legacy')


def existe_tabela_no_legado(tabela):
    sql = SQL_NAO_TEM_TABELA.format(tabela)
    return exec_legado(sql).fetchone()[0]


def existe_coluna_no_legado(tabela, coluna):
    sql = SQL_NAO_TEM_COLUNA.format(tabela, coluna)
    return exec_legado(sql).fetchone()[0] > 0


def garante_coluna_no_legado(tabela, spec_coluna):
    coluna = spec_coluna.split()[0]
    if not existe_coluna_no_legado(tabela, coluna):
        exec_legado('ALTER TABLE {} ADD COLUMN {}'.format(tabela, spec_coluna))
    assert existe_coluna_no_legado(tabela, coluna)


def garante_tabela_no_legado(create_table):
    tabela = create_table.strip().splitlines()[0].split()[2]
    if not existe_tabela_no_legado(tabela):
        exec_legado(create_table)
        assert existe_tabela_no_legado(tabela)


def uniformiza_banco():
    exec_legado('''
      SELECT replace(@@sql_mode,"STRICT_TRANS_TABLES,","ALLOW_INVALID_DATES");
    ''')

    # ajusta data zero em proposicao
    # isso é necessário para poder alterar a tabela a seguir
    exec_legado('''
        UPDATE proposicao SET dat_envio = "1800-01-01" WHERE dat_envio = 0;
        alter table proposicao modify dat_envio datetime;
        UPDATE proposicao SET dat_envio = NULL where dat_envio = "1800-01-01";
        ''')

    garante_coluna_no_legado('proposicao',
                             'num_proposicao int(11) NULL')

    garante_coluna_no_legado('tipo_materia_legislativa',
                             'ind_num_automatica BOOLEAN NULL DEFAULT FALSE')

    garante_coluna_no_legado('tipo_materia_legislativa',
                             'quorum_minimo_votacao int(11) NULL')

    # Cria campos cod_presenca_sessao (sendo a nova PK da tabela)
    # e dat_sessao em sessao_plenaria_presenca
    if not existe_coluna_no_legado('sessao_plenaria_presenca',
                                   'cod_presenca_sessao'):
        exec_legado('''
            ALTER TABLE sessao_plenaria_presenca
            DROP PRIMARY KEY,
            ADD cod_presenca_sessao INT auto_increment PRIMARY KEY FIRST;
        ''')
        assert existe_coluna_no_legado('sessao_plenaria_presenca',
                                       'cod_presenca_sessao')

    garante_coluna_no_legado('sessao_plenaria_presenca',
                             'dat_sessao DATE NULL')

    garante_tabela_no_legado('''
        CREATE TABLE lexml_registro_publicador (
            cod_publicador INT auto_increment NOT NULL,
            id_publicador INT, nom_publicador varchar(255),
            adm_email varchar(50),
            sigla varchar(255),
            nom_responsavel varchar(255),
            tipo varchar(50),
            id_responsavel INT, PRIMARY KEY (cod_publicador));
    ''')

    garante_tabela_no_legado('''
        CREATE TABLE lexml_registro_provedor (
            cod_provedor INT auto_increment NOT NULL,
            id_provedor INT, nom_provedor varchar(255),
            sgl_provedor varchar(15),
            adm_email varchar(50),
            nom_responsavel varchar(255),
            tipo varchar(50),
            id_responsavel INT, xml_provedor longtext,
            PRIMARY KEY (cod_provedor));
    ''')

    garante_tabela_no_legado('''
        CREATE TABLE tipo_situacao_militar (
            tip_situacao_militar INT auto_increment NOT NULL,
            des_tipo_situacao varchar(50),
            ind_excluido INT, PRIMARY KEY (tip_situacao_militar));
    ''')

    update_specs = '''
vinculo_norma_juridica| ind_excluido = ''           | trim(ind_excluido) = '0'
unidade_tramitacao    | cod_parlamentar = NULL      | cod_parlamentar = 0
parlamentar           | cod_nivel_instrucao = NULL  | cod_nivel_instrucao = 0
parlamentar           | tip_situacao_militar = NULL | tip_situacao_militar = 0
mandato               | tip_afastamento = NULL      | tip_afastamento = 0
relatoria             | tip_fim_relatoria = NULL    | tip_fim_relatoria = 0
    '''.strip().splitlines()

    for spec in update_specs:
        spec = spec.split('|')
        exec_legado('UPDATE {} SET {} WHERE {}'.format(*spec))


def iter_sql_records(sql, db):
    class Record:
        pass
    cursor = exec_sql(sql, db)
    fieldnames = [name[0] for name in cursor.description]
    for row in cursor.fetchall():
        record = Record()
        record.__dict__.update(zip(fieldnames, row))
        yield record


def save_relation(obj, nome_campo='', problema='', descricao='',
                  eh_stub=False, critico=False):
    link = ProblemaMigracao(
        content_object=obj, nome_campo=nome_campo, problema=problema,
        descricao=descricao, eh_stub=eh_stub, critico=critico)
    link.save()


def fill_vinculo_norma_juridica():
    lista = [('A', 'Altera o(a)',
              'Alterado(a) pelo(a)'),
             ('R', 'Revoga integralmente o(a)',
              'Revogado(a) integralmente pelo(a)'),
             ('P', 'Revoga parcialmente o(a)',
              'Revogado(a) parcialmente pelo(a)'),
             ('T', 'Revoga integralmente por consolidação',
              'Revogado(a) integralmente por consolidação'),
             ('C', 'Norma correlata',
              'Norma correlata'),
             ('S', 'Ressalva o(a)',
              'Ressalvada pelo(a)'),
             ('E', 'Reedita o(a)',
              'Reeditada pelo(a)'),
             ('I', 'Reedita com alteração o(a)',
              'Reeditada com alteração pelo(a)'),
             ('G', 'Regulamenta o(a)',
              'Regulamentada pelo(a)'),
             ('K', 'Suspende parcialmente o(a)',
              'Suspenso(a) parcialmente pelo(a)'),
             ('L', 'Suspende integralmente o(a)',
              'Suspenso(a) integralmente pelo(a)'),
             ('N', 'Julga integralmente inconstitucional',
              'Julgada integralmente inconstitucional'),
             ('O', 'Julga parcialmente inconstitucional',
              'Julgada parcialmente inconstitucional')]
    lista_objs = [TipoVinculoNormaJuridica(
        sigla=item[0], descricao_ativa=item[1], descricao_passiva=item[2])
        for item in lista]
    TipoVinculoNormaJuridica.objects.bulk_create(lista_objs)


def fill_dados_basicos():
    # Ajusta sequencia numérica e cria base.AppConfig
    letra = 'A'
    try:
        tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo')
        if 'POR ANO' in tipo.des_numeracao_protocolo:
            letra = 'A'
        elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo:
            letra = 'L'
        elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo:
            letra = 'U'
    except Exception as e:
        pass
    appconf = AppConf(sequencia_numeracao=letra)
    appconf.save()

    # Cria instância de CasaLegislativa
    casa = CasaLegislativa()
    casa.save()


# Uma anomalia no sapl 2.5 causa a duplicação de registros de votação.
# Essa duplicação deve ser eliminada para que não haja erro no sapl 3.1
def excluir_registrovotacao_duplicados():
    duplicatas_ids = RegistroVotacao.objects.values(
        'materia', 'ordem', 'expediente').annotate(
            Count('id')).order_by().filter(id__count__gt=1)
    duplicatas_queryset = RegistroVotacao.objects.filter(
        materia__in=[item['materia'] for item in duplicatas_ids])

    for dup in duplicatas_queryset:
        lista_dups = duplicatas_queryset.filter(
            materia=dup.materia, expediente=dup.expediente, ordem=dup.ordem)
        primeiro_registro = lista_dups[0]
        lista_dups = lista_dups.exclude(pk=primeiro_registro.pk)
        for objeto in lista_dups:
            if (objeto.pk > primeiro_registro.pk):
                try:
                    objeto.delete()
                except:
                    assert 0
            else:
                try:
                    primeiro_registro.delete()
                    primeiro_registro = objeto
                except:
                    assert 0


def delete_old(legacy_model, cols_values):
    # ajuste necessário por conta de cósigos html em txt_expediente
    if legacy_model.__name__ == 'ExpedienteSessaoPlenaria':
        cols_values.pop('txt_expediente')

    def eq_clause(col, value):
        if value is None:
            return '{} IS NULL'.format(col)
        else:
            return '{}="{}"'.format(col, value)

    delete_sql = 'delete from {} where {}'.format(
        legacy_model._meta.db_table,
        ' and '.join([eq_clause(col, value)
                      for col, value in cols_values.items()]))
    exec_sql(delete_sql, 'legacy')


def get_last_pk(model):
    last_value = model.objects.all().aggregate(Max('pk'))
    return last_value['pk__max'] or 0


def reinicia_sequence(model, id):
    sequence_name = '%s_id_seq' % model._meta.db_table
    exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % (
        sequence_name, id))


class DataMigrator:

    def __init__(self):
        self.field_renames, self.model_renames = get_renames()
        self.data_mudada = {}
        self.choice_valida = {}

    def populate_renamed_fields(self, new, old):
        renames = self.field_renames[type(new)]

        for field in new._meta.fields:
            old_field_name = renames.get(field.name)
            field_type = field.get_internal_type()
            if old_field_name:
                old_value = getattr(old, old_field_name)

                if field_type == 'ForeignKey':
                    # not necessarily a model
                    if hasattr(old, '_meta') and old._meta.pk.name != 'id':
                        label = old.pk
                    else:
                        label = '-- SEM PK --'
                    fk_field_name = '{}_id'.format(field.name)
                    value = get_fk_related(field, old_value, label)
                    setattr(new, fk_field_name, value)
                else:
                    value = getattr(old, old_field_name)
                    # TODO rever esse DateField após as mudança para datas com
                    # timezone
                    if field_type == 'DateField' and \
                            not field.null and value is None:
                        # TODO REVER ISSO
                        descricao = 'A data 1111-11-11 foi colocada no lugar'
                        problema = 'O valor da data era nulo ou inválido'
                        warn("O valor do campo %s (%s) do model %s "
                             "era inválido => %s" % (
                                 field.name, field_type,
                                 field.model.__name__, descricao))
                        value = date(1111, 11, 11)
                        self.data_mudada['obj'] = new
                        self.data_mudada['descricao'] = descricao
                        self.data_mudada['problema'] = problema
                        self.data_mudada.setdefault('nome_campo', []).\
                            append(field.name)
                    if (field_type in ['CharField', 'TextField']
                            and value in [None, 'None']):
                        value = ''
                    setattr(new, field.name, value)

    def migrate(self, obj=appconfs, interativo=True):
        # warning: model/app migration order is of utmost importance

        uniformiza_banco()

        # excluindo database antigo.
        if interativo:
            info('Todos os dados do banco serão excluidos. '
                 'Recomendamos que faça backup do banco sapl '
                 'antes de continuar.')
            info('Deseja continuar? [s/n]')
            resposta = input()
            if resposta.lower() in ['s', 'sim', 'y', 'yes']:
                pass
            else:
                info('Migração cancelada.')
                return 0
        info('Excluindo entradas antigas do banco.')
        call([PROJECT_DIR.child('manage.py'), 'flush',
              '--database=default', '--no-input'], stdout=PIPE)

        fill_vinculo_norma_juridica()
        fill_dados_basicos()
        info('Começando migração: %s...' % obj)
        self._do_migrate(obj)

        info('Excluindo possíveis duplicações em RegistroVotacao...')
        excluir_registrovotacao_duplicados()

    def _do_migrate(self, obj):
        if isinstance(obj, AppConfig):
            models_to_migrate = (model for model in obj.models.values()
                                 if model in self.field_renames)
            self._do_migrate(models_to_migrate)
        elif isinstance(obj, ModelBase):
            # A migração vai pular TipoProposicao e só vai migrar essa model
            # antes de migrar Proposicao. Isso deve acontecer por causa da
            # GenericRelation existente em TipoProposicao.
            if not obj.__name__ == 'TipoProposicao':
                if obj.__name__ == 'Proposicao':
                    self.migrate_model(TipoProposicao)
                self.migrate_model(obj)
        elif hasattr(obj, '__iter__'):
            for item in obj:
                self._do_migrate(item)
        else:
            raise TypeError(
                'Parameter must be a Model, AppConfig or a sequence of them')

    def migrate_model(self, model):
        print('Migrando %s...' % model.__name__)

        legacy_model_name = self.model_renames.get(model, model.__name__)
        legacy_model = legacy_app.get_model(legacy_model_name)
        legacy_pk_name = legacy_model._meta.pk.name

        # setup migration strategy for tables with or without a pk
        if legacy_pk_name == 'id':
            deve_ajustar_sequence_ao_final = False
            # There is no pk in the legacy table

            def save(new, old):
                with reversion.create_revision():
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')

                # apaga registro do legado
                delete_old(legacy_model, old.__dict__)

            old_records = iter_sql_records(
                'select * from ' + legacy_model._meta.db_table, 'legacy')
        else:
            deve_ajustar_sequence_ao_final = True

            def save(new, old):
                with reversion.create_revision():
                    # salva new com id de old
                    new.id = getattr(old, legacy_pk_name)
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')

                # apaga registro do legado
                delete_old(legacy_model, {legacy_pk_name: new.id})

            old_records = legacy_model.objects.all().order_by(legacy_pk_name)

        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():
            for old in old_records:
                if getattr(old, 'ind_excluido', False):
                    # não migramos registros marcados como excluídos
                    continue
                new = model()
                try:
                    self.populate_renamed_fields(new, old)
                    if ajuste_antes_salvar:
                        ajuste_antes_salvar(new, old)
                except ForeignKeyFaltando:
                    # tentamos preencher uma FK e o ojeto relacionado
                    # não existe
                    # então este é um objeo órfão: simplesmente ignoramos
                    continue
                else:
                    save(new, old)
                    if ajuste_depois_salvar:
                        ajuste_depois_salvar(new, old)

                    if self.data_mudada:
                        with reversion.create_revision():
                            save_relation(**self.data_mudada)
                            self.data_mudada.clear()
                            reversion.set_comment(
                                'Ajuste de data pela migração')
            # reinicia sequence
            if deve_ajustar_sequence_ao_final:
                last_pk = get_last_pk(model)
                reinicia_sequence(model, last_pk + 1)


def migrate(obj=appconfs, interativo=True):
    dm = DataMigrator()
    dm.migrate(obj, interativo)


# MIGRATION_ADJUSTMENTS #####################################################

def adjust_acompanhamentomateria(new, old):
    new.confirmado = True


def adjust_documentoadministrativo(new, old):
    if old.num_protocolo:
        protocolo = Protocolo.objects.filter(
            numero=old.num_protocolo, ano=new.ano)
        if not protocolo:
            protocolo = Protocolo.objects.filter(
                numero=old.num_protocolo, ano=new.ano + 1)
            print('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! '
                  'DocumentoAdministrativo: {}, numero_protocolo: {}, '
                  'ano doc adm: {}'.format(
                      old.cod_documento, old.num_protocolo, new.ano))
        if not protocolo:
            raise ForeignKeyFaltando(
                'Protocolo {} faltando '
                '(referenciado no documento administrativo {}'.format(
                    old.num_protocolo, old.cod_documento))
        assert len(protocolo) == 1
        new.protocolo = protocolo[0]


def adjust_mandato(new, old):
    if old.dat_fim_mandato:
        new.data_fim_mandato = old.dat_fim_mandato
    if not new.data_fim_mandato:
        legislatura = Legislatura.objects.latest('data_fim')
        new.data_fim_mandato = legislatura.data_fim
        new.data_expedicao_diploma = legislatura.data_inicio
    if not new.data_inicio_mandato:
        new.data_inicio_mandato = new.legislatura.data_inicio
        new.data_fim_mandato = new.legislatura.data_fim


def adjust_ordemdia_antes_salvar(new, old):
    new.votacao_aberta = False

    if not old.tip_votacao:
        new.tipo_votacao = 1

    if old.num_ordem is None:
        new.numero_ordem = 999999999


def adjust_ordemdia_depois_salvar(new, old):
    if old.num_ordem is None and new.numero_ordem == 999999999:
        with reversion.create_revision():
            problema = 'OrdemDia de PK %s tinha seu valor de numero ordem'\
                ' nulo.' % old.pk
            descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema,
                          descricao=descricao, eh_stub=False)
            reversion.set_comment('OrdemDia sem número da ordem.')


def adjust_parlamentar(new, old):
    if old.ind_unid_deliberativa:
        value = new.unidade_deliberativa
        # Field is defined as not null in legacy db,
        # but data includes null values
        #  => transform None to False
        if value is None:
            warn('nulo convertido para falso')
            new.unidade_deliberativa = False


def adjust_participacao(new, old):
    composicao = Composicao()
    composicao.comissao_id, composicao.periodo_id = [
        get_fk_related(Composicao._meta.get_field(name), value)
        for name, value in (('comissao', old.cod_comissao),
                            ('periodo', old.cod_periodo_comp))]
    # check if there is already an "equal" one in the db
    already_created = Composicao.objects.filter(
        comissao=composicao.comissao, periodo=composicao.periodo)
    if already_created:
        assert len(already_created) == 1  # we must never have made 2 copies
        [composicao] = already_created
    else:
        with reversion.create_revision():
            composicao.save()
            reversion.set_comment('Objeto criado pela migração')
    new.composicao = composicao


def adjust_proposicao_antes_salvar(new, old):
    if new.data_envio:
        new.ano = new.data_envio.year


def adjust_proposicao_depois_salvar(new, old):
    if not hasattr(old.dat_envio, 'year') or old.dat_envio.year == 1800:
        msg = "O valor do campo data_envio (DateField) da model Proposicao"\
            " era inválido"
        descricao = 'A data 1111-11-11 foi colocada no lugar'
        problema = 'O valor da data era nulo ou inválido'
        warn(msg + ' => ' + descricao)
        new.data_envio = date(1111, 11, 11)
        with reversion.create_revision():
            save_relation(obj=new, problema=problema,
                          descricao=descricao, eh_stub=False)
            reversion.set_comment('Ajuste de data pela migração')


def adjust_normarelacionada(new, old):
    tipo = TipoVinculoNormaJuridica.objects.filter(sigla=old.tip_vinculo)
    assert len(tipo) == 1
    new.tipo_vinculo = tipo[0]


def adjust_protocolo_antes_salvar(new, old):
    if old.num_protocolo is None:
        new.numero = old.cod_protocolo


def adjust_protocolo_depois_salvar(new, old):
    if old.num_protocolo is None:
        with reversion.create_revision():
            problema = 'Número do protocolo de PK %s é nulo' % new.pk
            descricao = 'Número do protocolo alterado para %s!' % new.numero
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema,
                          descricao=descricao, eh_stub=False)
            reversion.set_comment('Número de protocolo teve que ser alterado')


def adjust_registrovotacao_antes_salvar(new, old):
    ordem_dia = OrdemDia.objects.filter(
        pk=old.cod_ordem, materia=old.cod_materia)
    expediente_materia = ExpedienteMateria.objects.filter(
        pk=old.cod_ordem, materia=old.cod_materia)

    if ordem_dia and not expediente_materia:
        new.ordem = ordem_dia[0]
    if not ordem_dia and expediente_materia:
        new.expediente = expediente_materia[0]


def adjust_registrovotacao_depois_salvar(new, old):
    if not new.ordem and not new.expediente:
        with reversion.create_revision():
            problema = 'RegistroVotacao de PK %s não possui nenhuma OrdemDia'\
                ' ou ExpedienteMateria.' % old.pk
            descricao = 'RevistroVotacao deve ter no mínimo uma ordem do dia'\
                ' ou expediente vinculado.'
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema,
                          descricao=descricao, eh_stub=False)
            reversion.set_comment('RegistroVotacao sem ordem ou expediente')


def adjust_tipoafastamento(new, old):
    if old.ind_afastamento == 1:
        new.indicador = 'A'


def adjust_tipoproposicao(new, old):
    if old.ind_mat_ou_doc == 'M':
        tipo_materia = TipoMateriaLegislativa.objects.filter(
            pk=old.tip_mat_ou_doc)
        if tipo_materia:
            new.tipo_conteudo_related = tipo_materia[0]
        else:
            raise ForeignKeyFaltando
    elif old.ind_mat_ou_doc == 'D':
        tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc)
        if tipo_documento:
            new.tipo_conteudo_related = tipo_documento[0]
        else:
            raise ForeignKeyFaltando


def adjust_statustramitacao(new, old):
    if old.ind_fim_tramitacao:
        new.indicador = 'F'
    elif old.ind_retorno_tramitacao:
        new.indicador = 'R'
    else:
        new.indicador = ''


def adjust_statustramitacaoadm(new, old):
    adjust_statustramitacao(new, old)


def adjust_tramitacao(new, old):
    if old.sgl_turno == 'Ú':
        new.turno = 'U'


def adjust_tipo_autor(new, old):
    model_apontado = normalize(new.descricao.lower()).replace(' ', '')
    content_types = ContentType.objects.filter(
        model=model_apontado).exclude(app_label='legacy')
    assert len(content_types) <= 1
    new.content_type = content_types[0] if content_types else None


def adjust_normajuridica_antes_salvar(new, old):
    # Ajusta choice de esfera_federacao
    # O 'S' vem de 'Selecionar'. Na versão antiga do SAPL, quando uma opção do
    # combobox era selecionada, o sistema pegava a primeira letra da seleção,
    # sendo F para Federal, E para Estadual, M para Municipal e o S para
    # Selecionar, que era a primeira opção quando nada era selecionado.
    if old.tip_esfera_federacao == 'S':
        new.esfera_federacao = ''


def adjust_normajuridica_depois_salvar(new, old):
    # Ajusta relação M2M

    if not old.cod_assunto:  # it can be null or empty
        return

    # lista de pks separadas por vírgulas (ignorando strings vazias)
    lista_pks_assunto = [int(pk) for pk in old.cod_assunto.split(',') if pk]

    for pk_assunto in lista_pks_assunto:
        try:
            new.assuntos.add(AssuntoNorma.objects.get(pk=pk_assunto))
        except ObjectDoesNotExist:
            pass  # ignora assuntos inexistentes


def vincula_autor(new, old, model_relacionado, campo_relacionado, campo_nome):
    pk_rel = getattr(old, campo_relacionado)
    if pk_rel:
        try:
            new.autor_related = model_relacionado.objects.get(pk=pk_rel)
        except ObjectDoesNotExist:
            # ignoramos o autor órfão
            raise ForeignKeyFaltando('{} inexiste para autor'.format(
                model_relacionado._meta.verbose_name))
        else:
            new.nome = getattr(new.autor_related, campo_nome)
            return True


def adjust_autor(new, old):
    for args in [
            # essa ordem é importante
            (Parlamentar, 'cod_parlamentar', 'nome_parlamentar'),
            (Comissao, 'cod_comissao', 'nome'),
            (Partido, 'cod_partido', 'nome')]:
        if vincula_autor(new, old, *args):
            break

    if old.col_username:
        user_model = get_user_model()
        if not user_model.objects.filter(username=old.col_username).exists():
            # cria um novo ususaŕio para o autor
            user = user_model(username=old.col_username)
            # gera uma senha inutilizável, que precisará ser trocada
            user.set_password(None)
            with reversion.create_revision():
                user.save()
                reversion.set_comment(
                    'Usuário criado pela migração para o autor {}'.format(
                        old.cod_autor))
            grupo_autor = Group.objects.get(name="Autor")
            user.groups.add(grupo_autor)


def adjust_comissao(new, old):
    if not old.dat_extincao and not old.dat_fim_comissao:
        new.ativa = True
    elif old.dat_extincao and date.today() < new.data_extincao or \
            old.dat_fim_comissao and date.today() < new.data_fim_comissao:
        new.ativa = True
    else:
        new.ativa = False


AJUSTE_ANTES_SALVAR = {
    Autor: adjust_autor,
    TipoAutor: adjust_tipo_autor,
    AcompanhamentoMateria: adjust_acompanhamentomateria,
    Comissao: adjust_comissao,
    DocumentoAdministrativo: adjust_documentoadministrativo,
    Mandato: adjust_mandato,
    NormaJuridica: adjust_normajuridica_antes_salvar,
    NormaRelacionada: adjust_normarelacionada,
    OrdemDia: adjust_ordemdia_antes_salvar,
    Parlamentar: adjust_parlamentar,
    Participacao: adjust_participacao,
    Proposicao: adjust_proposicao_antes_salvar,
    Protocolo: adjust_protocolo_antes_salvar,
    RegistroVotacao: adjust_registrovotacao_antes_salvar,
    TipoAfastamento: adjust_tipoafastamento,
    TipoProposicao: adjust_tipoproposicao,
    StatusTramitacao: adjust_statustramitacao,
    StatusTramitacaoAdministrativo: adjust_statustramitacaoadm,
    Tramitacao: adjust_tramitacao,
}

AJUSTE_DEPOIS_SALVAR = {
    NormaJuridica: adjust_normajuridica_depois_salvar,
    OrdemDia: adjust_ordemdia_depois_salvar,
    Proposicao: adjust_proposicao_depois_salvar,
    Protocolo: adjust_protocolo_depois_salvar,
    RegistroVotacao: adjust_registrovotacao_depois_salvar,
}

# CHECKS ####################################################################


def get_ind_excluido(new):
    legacy_model = legacy_app.get_model(type(new).__name__)
    old = legacy_model.objects.get(**{legacy_model._meta.pk.name: new.id})
    return getattr(old, 'ind_excluido', False)


def check_app_no_ind_excluido(app):
    for model in app.models.values():
        assert not any(get_ind_excluido(new) for new in model.objects.all())
    print('OK!')

5fc1675ce1b626e63a372e38374c29b978ce2514
<mask token>


def warn(msg):
    print('CUIDADO! ' + msg)


class ForeignKeyFaltando(ObjectDoesNotExist):
    """Uma FK aponta para um registro inexistente"""
    pass


<mask token>


def exec_sql(sql, db='default'):
    cursor = connections[db].cursor()
    cursor.execute(sql)
    return cursor


<mask token>


def existe_coluna_no_legado(tabela, coluna):
    sql = SQL_NAO_TEM_COLUNA.format(tabela, coluna)
    return exec_legado(sql).fetchone()[0] > 0


def garante_coluna_no_legado(tabela, spec_coluna):
    coluna = spec_coluna.split()[0]
    if not existe_coluna_no_legado(tabela, coluna):
        exec_legado('ALTER TABLE {} ADD COLUMN {}'.format(tabela, spec_coluna))
    assert existe_coluna_no_legado(tabela, coluna)


def garante_tabela_no_legado(create_table):
    tabela = create_table.strip().splitlines()[0].split()[2]
    if not existe_tabela_no_legado(tabela):
        exec_legado(create_table)
        assert existe_tabela_no_legado(tabela)


<mask token>


def iter_sql_records(sql, db):


    class Record:
        pass
    cursor = exec_sql(sql, db)
    fieldnames = [name[0] for name in cursor.description]
    for row in cursor.fetchall():
        record = Record()
        record.__dict__.update(zip(fieldnames, row))
        yield record


<mask token>


def fill_vinculo_norma_juridica():
    lista = [('A', 'Altera o(a)', 'Alterado(a) pelo(a)'), ('R',
        'Revoga integralmente o(a)', 'Revogado(a) integralmente pelo(a)'),
        ('P', 'Revoga parcialmente o(a)',
        'Revogado(a) parcialmente pelo(a)'), ('T',
        'Revoga integralmente por consolidação',
        'Revogado(a) integralmente por consolidação'), ('C',
        'Norma correlata', 'Norma correlata'), ('S', 'Ressalva o(a)',
        'Ressalvada pelo(a)'), ('E', 'Reedita o(a)', 'Reeditada pelo(a)'),
        ('I', 'Reedita com alteração o(a)',
        'Reeditada com alteração pelo(a)'), ('G', 'Regulamenta o(a)',
        'Regulamentada pelo(a)'), ('K', 'Suspende parcialmente o(a)',
        'Suspenso(a) parcialmente pelo(a)'), ('L',
        'Suspende integralmente o(a)', 'Suspenso(a) integralmente pelo(a)'),
        ('N', 'Julga integralmente inconstitucional',
        'Julgada integralmente inconstitucional'), ('O',
        'Julga parcialmente inconstitucional',
        'Julgada parcialmente inconstitucional')]
    lista_objs = [TipoVinculoNormaJuridica(sigla=item[0], descricao_ativa=
        item[1], descricao_passiva=item[2]) for item in lista]
    TipoVinculoNormaJuridica.objects.bulk_create(lista_objs)


def fill_dados_basicos():
    letra = 'A'
    try:
        tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo')
        if 'POR ANO' in tipo.des_numeracao_protocolo:
            letra = 'A'
        elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo:
            letra = 'L'
        elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo:
            letra = 'U'
    except Exception as e:
        pass
    appconf = AppConf(sequencia_numeracao=letra)
    appconf.save()
    casa = CasaLegislativa()
    casa.save()


<mask token>


def delete_old(legacy_model, cols_values):
    if legacy_model.__name__ == 'ExpedienteSessaoPlenaria':
        cols_values.pop('txt_expediente')

    def eq_clause(col, value):
        if value is None:
            return '{} IS NULL'.format(col)
        else:
            return '{}="{}"'.format(col, value)
    delete_sql = 'delete from {} where {}'.format(legacy_model._meta.
        db_table, ' and '.join([eq_clause(col, value) for col, value in
        cols_values.items()]))
    exec_sql(delete_sql, 'legacy')


def get_last_pk(model):
    last_value = model.objects.all().aggregate(Max('pk'))
    return last_value['pk__max'] or 0


def reinicia_sequence(model, id):
    sequence_name = '%s_id_seq' % model._meta.db_table
    exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % (
        sequence_name, id))


class DataMigrator:

    def __init__(self):
        self.field_renames, self.model_renames = get_renames()
        self.data_mudada = {}
        self.choice_valida = {}

    def populate_renamed_fields(self, new, old):
        renames = self.field_renames[type(new)]
        for field in new._meta.fields:
            old_field_name = renames.get(field.name)
            field_type = field.get_internal_type()
            if old_field_name:
                old_value = getattr(old, old_field_name)
                if field_type == 'ForeignKey':
                    if hasattr(old, '_meta') and old._meta.pk.name != 'id':
                        label = old.pk
                    else:
                        label = '-- SEM PK --'
                    fk_field_name = '{}_id'.format(field.name)
                    value = get_fk_related(field, old_value, label)
                    setattr(new, fk_field_name, value)
                else:
                    value = getattr(old, old_field_name)
                    if (field_type == 'DateField' and not field.null and 
                        value is None):
                        descricao = 'A data 1111-11-11 foi colocada no lugar'
                        problema = 'O valor da data era nulo ou inválido'
                        warn(
                            'O valor do campo %s (%s) do model %s era inválido => %s'
                             % (field.name, field_type, field.model.
                            __name__, descricao))
                        value = date(1111, 11, 11)
                        self.data_mudada['obj'] = new
                        self.data_mudada['descricao'] = descricao
                        self.data_mudada['problema'] = problema
                        self.data_mudada.setdefault('nome_campo', []).append(
                            field.name)
                    if field_type in ['CharField', 'TextField'] and value in [
                        None, 'None']:
                        value = ''
                    setattr(new, field.name, value)

    def migrate(self, obj=appconfs, interativo=True):
        uniformiza_banco()
        if interativo:
            info(
                'Todos os dados do banco serão excluidos. Recomendamos que faça backup do banco sapl antes de continuar.'
                )
            info('Deseja continuar? [s/n]')
            resposta = input()
            if resposta.lower() in ['s', 'sim', 'y', 'yes']:
                pass
            else:
                info('Migração cancelada.')
                return 0
        info('Excluindo entradas antigas do banco.')
        call([PROJECT_DIR.child('manage.py'), 'flush', '--database=default',
            '--no-input'], stdout=PIPE)
        fill_vinculo_norma_juridica()
        fill_dados_basicos()
        info('Começando migração: %s...' % obj)
        self._do_migrate(obj)
        info('Excluindo possíveis duplicações em RegistroVotacao...')
        excluir_registrovotacao_duplicados()

    def _do_migrate(self, obj):
        if isinstance(obj, AppConfig):
            models_to_migrate = (model for model in obj.models.values() if 
                model in self.field_renames)
            self._do_migrate(models_to_migrate)
        elif isinstance(obj, ModelBase):
            if not obj.__name__ == 'TipoProposicao':
                if obj.__name__ == 'Proposicao':
                    self.migrate_model(TipoProposicao)
                self.migrate_model(obj)
        elif hasattr(obj, '__iter__'):
            for item in obj:
                self._do_migrate(item)
        else:
            raise TypeError(
                'Parameter must be a Model, AppConfig or a sequence of them')

    def migrate_model(self, model):
        print('Migrando %s...' % model.__name__)
        legacy_model_name = self.model_renames.get(model, model.__name__)
        legacy_model = legacy_app.get_model(legacy_model_name)
        legacy_pk_name = legacy_model._meta.pk.name
        if legacy_pk_name == 'id':
            deve_ajustar_sequence_ao_final = False

            def save(new, old):
                with reversion.create_revision():
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')
                delete_old(legacy_model, old.__dict__)
            old_records = iter_sql_records('select * from ' + legacy_model.
                _meta.db_table, 'legacy')
        else:
            deve_ajustar_sequence_ao_final = True

            def save(new, old):
                with reversion.create_revision():
                    new.id = getattr(old, legacy_pk_name)
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')
                delete_old(legacy_model, {legacy_pk_name: new.id})
            old_records = legacy_model.objects.all().order_by(legacy_pk_name)
        ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model)
        ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model)
        with transaction.atomic():
            for old in old_records:
                if getattr(old, 'ind_excluido', False):
                    continue
                new = model()
                try:
                    self.populate_renamed_fields(new, old)
                    if ajuste_antes_salvar:
                        ajuste_antes_salvar(new, old)
                except ForeignKeyFaltando:
                    continue
                else:
                    save(new, old)
                    if ajuste_depois_salvar:
                        ajuste_depois_salvar(new, old)
                    if self.data_mudada:
                        with reversion.create_revision():
                            save_relation(**self.data_mudada)
                            self.data_mudada.clear()
                            reversion.set_comment(
                                'Ajuste de data pela migração')
            if deve_ajustar_sequence_ao_final:
                last_pk = get_last_pk(model)
                reinicia_sequence(model, last_pk + 1)


def migrate(obj=appconfs, interativo=True):
    dm = DataMigrator()
    dm.migrate(obj, interativo)


<mask token>


def adjust_mandato(new, old):
    if old.dat_fim_mandato:
        new.data_fim_mandato = old.dat_fim_mandato
    if not new.data_fim_mandato:
        legislatura = Legislatura.objects.latest('data_fim')
        new.data_fim_mandato = legislatura.data_fim
        new.data_expedicao_diploma = legislatura.data_inicio
    if not new.data_inicio_mandato:
        new.data_inicio_mandato = new.legislatura.data_inicio
        new.data_fim_mandato = new.legislatura.data_fim


<mask token>


def adjust_ordemdia_depois_salvar(new, old):
    if old.num_ordem is None and new.numero_ordem == 999999999:
        with reversion.create_revision():
            problema = (
                'OrdemDia de PK %s tinha seu valor de numero ordem nulo.' %
                old.pk)
            descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('OrdemDia sem número da ordem.')


def adjust_parlamentar(new, old):
    if old.ind_unid_deliberativa:
        value = new.unidade_deliberativa
        if value is None:
            warn('nulo convertido para falso')
            new.unidade_deliberativa = False


def adjust_participacao(new, old):
    composicao = Composicao()
    composicao.comissao_id, composicao.periodo_id = [get_fk_related(
        Composicao._meta.get_field(name), value) for name, value in ((
        'comissao', old.cod_comissao), ('periodo', old.cod_periodo_comp))]
    already_created = Composicao.objects.filter(comissao=composicao.
        comissao, periodo=composicao.periodo)
    if already_created:
        assert len(already_created) == 1
        [composicao] = already_created
    else:
        with reversion.create_revision():
            composicao.save()
            reversion.set_comment('Objeto criado pela migração')
    new.composicao = composicao


def adjust_proposicao_antes_salvar(new, old):
    if new.data_envio:
        new.ano = new.data_envio.year


def adjust_proposicao_depois_salvar(new, old):
    if not hasattr(old.dat_envio, 'year') or old.dat_envio.year == 1800:
        msg = (
            'O valor do campo data_envio (DateField) da model Proposicao era inválido'
            )
        descricao = 'A data 1111-11-11 foi colocada no lugar'
        problema = 'O valor da data era nulo ou inválido'
        warn(msg + ' => ' + descricao)
        new.data_envio = date(1111, 11, 11)
        with reversion.create_revision():
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('Ajuste de data pela migração')


def adjust_normarelacionada(new, old):
    tipo = TipoVinculoNormaJuridica.objects.filter(sigla=old.tip_vinculo)
    assert len(tipo) == 1
    new.tipo_vinculo = tipo[0]


def adjust_protocolo_antes_salvar(new, old):
    if old.num_protocolo is None:
        new.numero = old.cod_protocolo


def adjust_protocolo_depois_salvar(new, old):
    if old.num_protocolo is None:
        with reversion.create_revision():
            problema = 'Número do protocolo de PK %s é nulo' % new.pk
            descricao = 'Número do protocolo alterado para %s!' % new.numero
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('Número de protocolo teve que ser alterado')


def adjust_registrovotacao_antes_salvar(new, old):
    ordem_dia = OrdemDia.objects.filter(pk=old.cod_ordem, materia=old.
        cod_materia)
    expediente_materia = ExpedienteMateria.objects.filter(pk=old.cod_ordem,
        materia=old.cod_materia)
    if ordem_dia and not expediente_materia:
        new.ordem = ordem_dia[0]
    if not ordem_dia and expediente_materia:
        new.expediente = expediente_materia[0]


<mask token>


def adjust_tipoproposicao(new, old):
    if old.ind_mat_ou_doc == 'M':
        tipo_materia = TipoMateriaLegislativa.objects.filter(pk=old.
            tip_mat_ou_doc)
        if tipo_materia:
            new.tipo_conteudo_related = tipo_materia[0]
        else:
            raise ForeignKeyFaltando
    elif old.ind_mat_ou_doc == 'D':
        tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc)
        if tipo_documento:
            new.tipo_conteudo_related = tipo_documento[0]
        else:
            raise ForeignKeyFaltando


<mask token>


def adjust_tramitacao(new, old):
    if old.sgl_turno == 'Ú':
        new.turno = 'U'


def adjust_tipo_autor(new, old):
    model_apontado = normalize(new.descricao.lower()).replace(' ', '')
    content_types = ContentType.objects.filter(model=model_apontado).exclude(
        app_label='legacy')
    assert len(content_types) <= 1
    new.content_type = content_types[0] if content_types else None


<mask token>


def adjust_normajuridica_depois_salvar(new, old):
    if not old.cod_assunto:
        return
    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


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:
            raise ForeignKeyFaltando('{} inexiste para autor'.format(
                model_relacionado._meta.verbose_name))
        else:
            new.nome = getattr(new.autor_related, campo_nome)
            return True


<mask token>


def check_app_no_ind_excluido(app):
    for model in app.models.values():
        assert not any(get_ind_excluido(new) for new in model.objects.all())
    print('OK!')

<mask token>


def info(msg):
    print('INFO: ' + msg)


def warn(msg):
    print('CUIDADO! ' + msg)


class ForeignKeyFaltando(ObjectDoesNotExist):
    """Uma FK aponta para um registro inexistente"""
    pass


@lru_cache()
def _get_all_ids_from_model(model):
    return set(model.objects.values_list('id', flat=True))


def get_fk_related(field, value, label=None):
    if value is None and field.null:
        return None
    if value in _get_all_ids_from_model(field.related_model):
        return value
    else:
        msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % (field.
            name, value, field.model.__name__, label or '---')
        warn(msg)
        raise ForeignKeyFaltando(msg)


def exec_sql(sql, db='default'):
    cursor = connections[db].cursor()
    cursor.execute(sql)
    return cursor


<mask token>


def existe_coluna_no_legado(tabela, coluna):
    sql = SQL_NAO_TEM_COLUNA.format(tabela, coluna)
    return exec_legado(sql).fetchone()[0] > 0


def garante_coluna_no_legado(tabela, spec_coluna):
    coluna = spec_coluna.split()[0]
    if not existe_coluna_no_legado(tabela, coluna):
        exec_legado('ALTER TABLE {} ADD COLUMN {}'.format(tabela, spec_coluna))
    assert existe_coluna_no_legado(tabela, coluna)


def garante_tabela_no_legado(create_table):
    tabela = create_table.strip().splitlines()[0].split()[2]
    if not existe_tabela_no_legado(tabela):
        exec_legado(create_table)
        assert existe_tabela_no_legado(tabela)


<mask token>


def iter_sql_records(sql, db):


    class Record:
        pass
    cursor = exec_sql(sql, db)
    fieldnames = [name[0] for name in cursor.description]
    for row in cursor.fetchall():
        record = Record()
        record.__dict__.update(zip(fieldnames, row))
        yield record


def save_relation(obj, nome_campo='', problema='', descricao='', eh_stub=
    False, critico=False):
    link = ProblemaMigracao(content_object=obj, nome_campo=nome_campo,
        problema=problema, descricao=descricao, eh_stub=eh_stub, critico=
        critico)
    link.save()


def fill_vinculo_norma_juridica():
    lista = [('A', 'Altera o(a)', 'Alterado(a) pelo(a)'), ('R',
        'Revoga integralmente o(a)', 'Revogado(a) integralmente pelo(a)'),
        ('P', 'Revoga parcialmente o(a)',
        'Revogado(a) parcialmente pelo(a)'), ('T',
        'Revoga integralmente por consolidação',
        'Revogado(a) integralmente por consolidação'), ('C',
        'Norma correlata', 'Norma correlata'), ('S', 'Ressalva o(a)',
        'Ressalvada pelo(a)'), ('E', 'Reedita o(a)', 'Reeditada pelo(a)'),
        ('I', 'Reedita com alteração o(a)',
        'Reeditada com alteração pelo(a)'), ('G', 'Regulamenta o(a)',
        'Regulamentada pelo(a)'), ('K', 'Suspende parcialmente o(a)',
        'Suspenso(a) parcialmente pelo(a)'), ('L',
        'Suspende integralmente o(a)', 'Suspenso(a) integralmente pelo(a)'),
        ('N', 'Julga integralmente inconstitucional',
        'Julgada integralmente inconstitucional'), ('O',
        'Julga parcialmente inconstitucional',
        'Julgada parcialmente inconstitucional')]
    lista_objs = [TipoVinculoNormaJuridica(sigla=item[0], descricao_ativa=
        item[1], descricao_passiva=item[2]) for item in lista]
    TipoVinculoNormaJuridica.objects.bulk_create(lista_objs)


def fill_dados_basicos():
    letra = 'A'
    try:
        tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo')
        if 'POR ANO' in tipo.des_numeracao_protocolo:
            letra = 'A'
        elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo:
            letra = 'L'
        elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo:
            letra = 'U'
    except Exception as e:
        pass
    appconf = AppConf(sequencia_numeracao=letra)
    appconf.save()
    casa = CasaLegislativa()
    casa.save()


def excluir_registrovotacao_duplicados():
    duplicatas_ids = RegistroVotacao.objects.values('materia', 'ordem',
        'expediente').annotate(Count('id')).order_by().filter(id__count__gt=1)
    duplicatas_queryset = RegistroVotacao.objects.filter(materia__in=[item[
        'materia'] for item in duplicatas_ids])
    for dup in duplicatas_queryset:
        lista_dups = duplicatas_queryset.filter(materia=dup.materia,
            expediente=dup.expediente, ordem=dup.ordem)
        primeiro_registro = lista_dups[0]
        lista_dups = lista_dups.exclude(pk=primeiro_registro.pk)
        for objeto in lista_dups:
            if objeto.pk > primeiro_registro.pk:
                try:
                    objeto.delete()
                except:
                    assert 0
            else:
                try:
                    primeiro_registro.delete()
                    primeiro_registro = objeto
                except:
                    assert 0


def delete_old(legacy_model, cols_values):
    if legacy_model.__name__ == 'ExpedienteSessaoPlenaria':
        cols_values.pop('txt_expediente')

    def eq_clause(col, value):
        if value is None:
            return '{} IS NULL'.format(col)
        else:
            return '{}="{}"'.format(col, value)
    delete_sql = 'delete from {} where {}'.format(legacy_model._meta.
        db_table, ' and '.join([eq_clause(col, value) for col, value in
        cols_values.items()]))
    exec_sql(delete_sql, 'legacy')


def get_last_pk(model):
    last_value = model.objects.all().aggregate(Max('pk'))
    return last_value['pk__max'] or 0


def reinicia_sequence(model, id):
    sequence_name = '%s_id_seq' % model._meta.db_table
    exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % (
        sequence_name, id))


class DataMigrator:

    def __init__(self):
        self.field_renames, self.model_renames = get_renames()
        self.data_mudada = {}
        self.choice_valida = {}

    def populate_renamed_fields(self, new, old):
        renames = self.field_renames[type(new)]
        for field in new._meta.fields:
            old_field_name = renames.get(field.name)
            field_type = field.get_internal_type()
            if old_field_name:
                old_value = getattr(old, old_field_name)
                if field_type == 'ForeignKey':
                    if hasattr(old, '_meta') and old._meta.pk.name != 'id':
                        label = old.pk
                    else:
                        label = '-- SEM PK --'
                    fk_field_name = '{}_id'.format(field.name)
                    value = get_fk_related(field, old_value, label)
                    setattr(new, fk_field_name, value)
                else:
                    value = getattr(old, old_field_name)
                    if (field_type == 'DateField' and not field.null and 
                        value is None):
                        descricao = 'A data 1111-11-11 foi colocada no lugar'
                        problema = 'O valor da data era nulo ou inválido'
                        warn(
                            'O valor do campo %s (%s) do model %s era inválido => %s'
                             % (field.name, field_type, field.model.
                            __name__, descricao))
                        value = date(1111, 11, 11)
                        self.data_mudada['obj'] = new
                        self.data_mudada['descricao'] = descricao
                        self.data_mudada['problema'] = problema
                        self.data_mudada.setdefault('nome_campo', []).append(
                            field.name)
                    if field_type in ['CharField', 'TextField'] and value in [
                        None, 'None']:
                        value = ''
                    setattr(new, field.name, value)

    def migrate(self, obj=appconfs, interativo=True):
        uniformiza_banco()
        if interativo:
            info(
                'Todos os dados do banco serão excluidos. Recomendamos que faça backup do banco sapl antes de continuar.'
                )
            info('Deseja continuar? [s/n]')
            resposta = input()
            if resposta.lower() in ['s', 'sim', 'y', 'yes']:
                pass
            else:
                info('Migração cancelada.')
                return 0
        info('Excluindo entradas antigas do banco.')
        call([PROJECT_DIR.child('manage.py'), 'flush', '--database=default',
            '--no-input'], stdout=PIPE)
        fill_vinculo_norma_juridica()
        fill_dados_basicos()
        info('Começando migração: %s...' % obj)
        self._do_migrate(obj)
        info('Excluindo possíveis duplicações em RegistroVotacao...')
        excluir_registrovotacao_duplicados()

    def _do_migrate(self, obj):
        if isinstance(obj, AppConfig):
            models_to_migrate = (model for model in obj.models.values() if 
                model in self.field_renames)
            self._do_migrate(models_to_migrate)
        elif isinstance(obj, ModelBase):
            if not obj.__name__ == 'TipoProposicao':
                if obj.__name__ == 'Proposicao':
                    self.migrate_model(TipoProposicao)
                self.migrate_model(obj)
        elif hasattr(obj, '__iter__'):
            for item in obj:
                self._do_migrate(item)
        else:
            raise TypeError(
                'Parameter must be a Model, AppConfig or a sequence of them')

    def migrate_model(self, model):
        print('Migrando %s...' % model.__name__)
        legacy_model_name = self.model_renames.get(model, model.__name__)
        legacy_model = legacy_app.get_model(legacy_model_name)
        legacy_pk_name = legacy_model._meta.pk.name
        if legacy_pk_name == 'id':
            deve_ajustar_sequence_ao_final = False

            def save(new, old):
                with reversion.create_revision():
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')
                delete_old(legacy_model, old.__dict__)
            old_records = iter_sql_records('select * from ' + legacy_model.
                _meta.db_table, 'legacy')
        else:
            deve_ajustar_sequence_ao_final = True

            def save(new, old):
                with reversion.create_revision():
                    new.id = getattr(old, legacy_pk_name)
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')
                delete_old(legacy_model, {legacy_pk_name: new.id})
            old_records = legacy_model.objects.all().order_by(legacy_pk_name)
        ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model)
        ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model)
        with transaction.atomic():
            for old in old_records:
                if getattr(old, 'ind_excluido', False):
                    continue
                new = model()
                try:
                    self.populate_renamed_fields(new, old)
                    if ajuste_antes_salvar:
                        ajuste_antes_salvar(new, old)
                except ForeignKeyFaltando:
                    continue
                else:
                    save(new, old)
                    if ajuste_depois_salvar:
                        ajuste_depois_salvar(new, old)
                    if self.data_mudada:
                        with reversion.create_revision():
                            save_relation(**self.data_mudada)
                            self.data_mudada.clear()
                            reversion.set_comment(
                                'Ajuste de data pela migração')
            if deve_ajustar_sequence_ao_final:
                last_pk = get_last_pk(model)
                reinicia_sequence(model, last_pk + 1)


def migrate(obj=appconfs, interativo=True):
    dm = DataMigrator()
    dm.migrate(obj, interativo)


<mask token>


def adjust_mandato(new, old):
    if old.dat_fim_mandato:
        new.data_fim_mandato = old.dat_fim_mandato
    if not new.data_fim_mandato:
        legislatura = Legislatura.objects.latest('data_fim')
        new.data_fim_mandato = legislatura.data_fim
        new.data_expedicao_diploma = legislatura.data_inicio
    if not new.data_inicio_mandato:
        new.data_inicio_mandato = new.legislatura.data_inicio
        new.data_fim_mandato = new.legislatura.data_fim


<mask token>


def adjust_ordemdia_depois_salvar(new, old):
    if old.num_ordem is None and new.numero_ordem == 999999999:
        with reversion.create_revision():
            problema = (
                'OrdemDia de PK %s tinha seu valor de numero ordem nulo.' %
                old.pk)
            descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('OrdemDia sem número da ordem.')


def adjust_parlamentar(new, old):
    if old.ind_unid_deliberativa:
        value = new.unidade_deliberativa
        if value is None:
            warn('nulo convertido para falso')
            new.unidade_deliberativa = False


def adjust_participacao(new, old):
    composicao = Composicao()
    composicao.comissao_id, composicao.periodo_id = [get_fk_related(
        Composicao._meta.get_field(name), value) for name, value in ((
        'comissao', old.cod_comissao), ('periodo', old.cod_periodo_comp))]
    already_created = Composicao.objects.filter(comissao=composicao.
        comissao, periodo=composicao.periodo)
    if already_created:
        assert len(already_created) == 1
        [composicao] = already_created
    else:
        with reversion.create_revision():
            composicao.save()
            reversion.set_comment('Objeto criado pela migração')
    new.composicao = composicao


def adjust_proposicao_antes_salvar(new, old):
    if new.data_envio:
        new.ano = new.data_envio.year


def adjust_proposicao_depois_salvar(new, old):
    if not hasattr(old.dat_envio, 'year') or old.dat_envio.year == 1800:
        msg = (
            'O valor do campo data_envio (DateField) da model Proposicao era inválido'
            )
        descricao = 'A data 1111-11-11 foi colocada no lugar'
        problema = 'O valor da data era nulo ou inválido'
        warn(msg + ' => ' + descricao)
        new.data_envio = date(1111, 11, 11)
        with reversion.create_revision():
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('Ajuste de data pela migração')


def adjust_normarelacionada(new, old):
    tipo = TipoVinculoNormaJuridica.objects.filter(sigla=old.tip_vinculo)
    assert len(tipo) == 1
    new.tipo_vinculo = tipo[0]


def adjust_protocolo_antes_salvar(new, old):
    if old.num_protocolo is None:
        new.numero = old.cod_protocolo


def adjust_protocolo_depois_salvar(new, old):
    if old.num_protocolo is None:
        with reversion.create_revision():
            problema = 'Número do protocolo de PK %s é nulo' % new.pk
            descricao = 'Número do protocolo alterado para %s!' % new.numero
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('Número de protocolo teve que ser alterado')


def adjust_registrovotacao_antes_salvar(new, old):
    ordem_dia = OrdemDia.objects.filter(pk=old.cod_ordem, materia=old.
        cod_materia)
    expediente_materia = ExpedienteMateria.objects.filter(pk=old.cod_ordem,
        materia=old.cod_materia)
    if ordem_dia and not expediente_materia:
        new.ordem = ordem_dia[0]
    if not ordem_dia and expediente_materia:
        new.expediente = expediente_materia[0]


<mask token>


def adjust_tipoproposicao(new, old):
    if old.ind_mat_ou_doc == 'M':
        tipo_materia = TipoMateriaLegislativa.objects.filter(pk=old.
            tip_mat_ou_doc)
        if tipo_materia:
            new.tipo_conteudo_related = tipo_materia[0]
        else:
            raise ForeignKeyFaltando
    elif old.ind_mat_ou_doc == 'D':
        tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc)
        if tipo_documento:
            new.tipo_conteudo_related = tipo_documento[0]
        else:
            raise ForeignKeyFaltando


<mask token>


def adjust_tramitacao(new, old):
    if old.sgl_turno == 'Ú':
        new.turno = 'U'


def adjust_tipo_autor(new, old):
    model_apontado = normalize(new.descricao.lower()).replace(' ', '')
    content_types = ContentType.objects.filter(model=model_apontado).exclude(
        app_label='legacy')
    assert len(content_types) <= 1
    new.content_type = content_types[0] if content_types else None


<mask token>


def adjust_normajuridica_depois_salvar(new, old):
    if not old.cod_assunto:
        return
    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


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:
            raise ForeignKeyFaltando('{} inexiste para autor'.format(
                model_relacionado._meta.verbose_name))
        else:
            new.nome = getattr(new.autor_related, campo_nome)
            return True


<mask token>


def check_app_no_ind_excluido(app):
    for model in app.models.values():
        assert not any(get_ind_excluido(new) for new in model.objects.all())
    print('OK!')

<mask token>


def get_renames():
    field_renames = {}
    model_renames = {}
    for app in appconfs:
        app_rename_data = yaml.load(pkg_resources.resource_string(app.
            module.__name__, 'legacy.yaml'))
        for model_name, renames in app_rename_data.items():
            match = MODEL_RENAME_PATTERN.match(model_name)
            if match:
                model_name, old_name = match.groups()
            else:
                old_name = None
            model = getattr(app.models_module, model_name)
            if old_name:
                model_renames[model] = old_name
            field_renames[model] = renames
    for model, renames in field_renames.items():
        if any(parent in field_renames for parent in model.__mro__[1:]):
            renames = {}
            for parent in reversed(model.__mro__):
                if parent in field_renames:
                    renames.update(field_renames[parent])
            field_renames[model] = renames
    field_renames = {m: r for m, r in field_renames.items() if not m._meta.
        abstract}
    return field_renames, model_renames


def info(msg):
    print('INFO: ' + msg)


def warn(msg):
    print('CUIDADO! ' + msg)


class ForeignKeyFaltando(ObjectDoesNotExist):
    """Uma FK aponta para um registro inexistente"""
    pass


@lru_cache()
def _get_all_ids_from_model(model):
    return set(model.objects.values_list('id', flat=True))


def get_fk_related(field, value, label=None):
    if value is None and field.null:
        return None
    if value in _get_all_ids_from_model(field.related_model):
        return value
    else:
        msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % (field.
            name, value, field.model.__name__, label or '---')
        warn(msg)
        raise ForeignKeyFaltando(msg)


def exec_sql(sql, db='default'):
    cursor = connections[db].cursor()
    cursor.execute(sql)
    return cursor


<mask token>


def existe_coluna_no_legado(tabela, coluna):
    sql = SQL_NAO_TEM_COLUNA.format(tabela, coluna)
    return exec_legado(sql).fetchone()[0] > 0


def garante_coluna_no_legado(tabela, spec_coluna):
    coluna = spec_coluna.split()[0]
    if not existe_coluna_no_legado(tabela, coluna):
        exec_legado('ALTER TABLE {} ADD COLUMN {}'.format(tabela, spec_coluna))
    assert existe_coluna_no_legado(tabela, coluna)


def garante_tabela_no_legado(create_table):
    tabela = create_table.strip().splitlines()[0].split()[2]
    if not existe_tabela_no_legado(tabela):
        exec_legado(create_table)
        assert existe_tabela_no_legado(tabela)


def uniformiza_banco():
    exec_legado(
        """
      SELECT replace(@@sql_mode,"STRICT_TRANS_TABLES,","ALLOW_INVALID_DATES");
    """
        )
    exec_legado(
        """
        UPDATE proposicao SET dat_envio = "1800-01-01" WHERE dat_envio = 0;
        alter table proposicao modify dat_envio datetime;
        UPDATE proposicao SET dat_envio = NULL where dat_envio = "1800-01-01";
        """
        )
    garante_coluna_no_legado('proposicao', 'num_proposicao int(11) NULL')
    garante_coluna_no_legado('tipo_materia_legislativa',
        'ind_num_automatica BOOLEAN NULL DEFAULT FALSE')
    garante_coluna_no_legado('tipo_materia_legislativa',
        'quorum_minimo_votacao int(11) NULL')
    if not existe_coluna_no_legado('sessao_plenaria_presenca',
        'cod_presenca_sessao'):
        exec_legado(
            """
            ALTER TABLE sessao_plenaria_presenca
            DROP PRIMARY KEY,
            ADD cod_presenca_sessao INT auto_increment PRIMARY KEY FIRST;
        """
            )
        assert existe_coluna_no_legado('sessao_plenaria_presenca',
            'cod_presenca_sessao')
    garante_coluna_no_legado('sessao_plenaria_presenca', 'dat_sessao DATE NULL'
        )
    garante_tabela_no_legado(
        """
        CREATE TABLE lexml_registro_publicador (
            cod_publicador INT auto_increment NOT NULL,
            id_publicador INT, nom_publicador varchar(255),
            adm_email varchar(50),
            sigla varchar(255),
            nom_responsavel varchar(255),
            tipo varchar(50),
            id_responsavel INT, PRIMARY KEY (cod_publicador));
    """
        )
    garante_tabela_no_legado(
        """
        CREATE TABLE lexml_registro_provedor (
            cod_provedor INT auto_increment NOT NULL,
            id_provedor INT, nom_provedor varchar(255),
            sgl_provedor varchar(15),
            adm_email varchar(50),
            nom_responsavel varchar(255),
            tipo varchar(50),
            id_responsavel INT, xml_provedor longtext,
            PRIMARY KEY (cod_provedor));
    """
        )
    garante_tabela_no_legado(
        """
        CREATE TABLE tipo_situacao_militar (
            tip_situacao_militar INT auto_increment NOT NULL,
            des_tipo_situacao varchar(50),
            ind_excluido INT, PRIMARY KEY (tip_situacao_militar));
    """
        )
    update_specs = (
        """
vinculo_norma_juridica| ind_excluido = ''           | trim(ind_excluido) = '0'
unidade_tramitacao    | cod_parlamentar = NULL      | cod_parlamentar = 0
parlamentar           | cod_nivel_instrucao = NULL  | cod_nivel_instrucao = 0
parlamentar           | tip_situacao_militar = NULL | tip_situacao_militar = 0
mandato               | tip_afastamento = NULL      | tip_afastamento = 0
relatoria             | tip_fim_relatoria = NULL    | tip_fim_relatoria = 0
    """
        .strip().splitlines())
    for spec in update_specs:
        spec = spec.split('|')
        exec_legado('UPDATE {} SET {} WHERE {}'.format(*spec))


def iter_sql_records(sql, db):


    class Record:
        pass
    cursor = exec_sql(sql, db)
    fieldnames = [name[0] for name in cursor.description]
    for row in cursor.fetchall():
        record = Record()
        record.__dict__.update(zip(fieldnames, row))
        yield record


def save_relation(obj, nome_campo='', problema='', descricao='', eh_stub=
    False, critico=False):
    link = ProblemaMigracao(content_object=obj, nome_campo=nome_campo,
        problema=problema, descricao=descricao, eh_stub=eh_stub, critico=
        critico)
    link.save()


def fill_vinculo_norma_juridica():
    lista = [('A', 'Altera o(a)', 'Alterado(a) pelo(a)'), ('R',
        'Revoga integralmente o(a)', 'Revogado(a) integralmente pelo(a)'),
        ('P', 'Revoga parcialmente o(a)',
        'Revogado(a) parcialmente pelo(a)'), ('T',
        'Revoga integralmente por consolidação',
        'Revogado(a) integralmente por consolidação'), ('C',
        'Norma correlata', 'Norma correlata'), ('S', 'Ressalva o(a)',
        'Ressalvada pelo(a)'), ('E', 'Reedita o(a)', 'Reeditada pelo(a)'),
        ('I', 'Reedita com alteração o(a)',
        'Reeditada com alteração pelo(a)'), ('G', 'Regulamenta o(a)',
        'Regulamentada pelo(a)'), ('K', 'Suspende parcialmente o(a)',
        'Suspenso(a) parcialmente pelo(a)'), ('L',
        'Suspende integralmente o(a)', 'Suspenso(a) integralmente pelo(a)'),
        ('N', 'Julga integralmente inconstitucional',
        'Julgada integralmente inconstitucional'), ('O',
        'Julga parcialmente inconstitucional',
        'Julgada parcialmente inconstitucional')]
    lista_objs = [TipoVinculoNormaJuridica(sigla=item[0], descricao_ativa=
        item[1], descricao_passiva=item[2]) for item in lista]
    TipoVinculoNormaJuridica.objects.bulk_create(lista_objs)


def fill_dados_basicos():
    letra = 'A'
    try:
        tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo')
        if 'POR ANO' in tipo.des_numeracao_protocolo:
            letra = 'A'
        elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo:
            letra = 'L'
        elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo:
            letra = 'U'
    except Exception as e:
        pass
    appconf = AppConf(sequencia_numeracao=letra)
    appconf.save()
    casa = CasaLegislativa()
    casa.save()


def excluir_registrovotacao_duplicados():
    duplicatas_ids = RegistroVotacao.objects.values('materia', 'ordem',
        'expediente').annotate(Count('id')).order_by().filter(id__count__gt=1)
    duplicatas_queryset = RegistroVotacao.objects.filter(materia__in=[item[
        'materia'] for item in duplicatas_ids])
    for dup in duplicatas_queryset:
        lista_dups = duplicatas_queryset.filter(materia=dup.materia,
            expediente=dup.expediente, ordem=dup.ordem)
        primeiro_registro = lista_dups[0]
        lista_dups = lista_dups.exclude(pk=primeiro_registro.pk)
        for objeto in lista_dups:
            if objeto.pk > primeiro_registro.pk:
                try:
                    objeto.delete()
                except:
                    assert 0
            else:
                try:
                    primeiro_registro.delete()
                    primeiro_registro = objeto
                except:
                    assert 0


def delete_old(legacy_model, cols_values):
    if legacy_model.__name__ == 'ExpedienteSessaoPlenaria':
        cols_values.pop('txt_expediente')

    def eq_clause(col, value):
        if value is None:
            return '{} IS NULL'.format(col)
        else:
            return '{}="{}"'.format(col, value)
    delete_sql = 'delete from {} where {}'.format(legacy_model._meta.
        db_table, ' and '.join([eq_clause(col, value) for col, value in
        cols_values.items()]))
    exec_sql(delete_sql, 'legacy')


def get_last_pk(model):
    last_value = model.objects.all().aggregate(Max('pk'))
    return last_value['pk__max'] or 0


def reinicia_sequence(model, id):
    sequence_name = '%s_id_seq' % model._meta.db_table
    exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % (
        sequence_name, id))


class DataMigrator:

    def __init__(self):
        self.field_renames, self.model_renames = get_renames()
        self.data_mudada = {}
        self.choice_valida = {}

    def populate_renamed_fields(self, new, old):
        renames = self.field_renames[type(new)]
        for field in new._meta.fields:
            old_field_name = renames.get(field.name)
            field_type = field.get_internal_type()
            if old_field_name:
                old_value = getattr(old, old_field_name)
                if field_type == 'ForeignKey':
                    if hasattr(old, '_meta') and old._meta.pk.name != 'id':
                        label = old.pk
                    else:
                        label = '-- SEM PK --'
                    fk_field_name = '{}_id'.format(field.name)
                    value = get_fk_related(field, old_value, label)
                    setattr(new, fk_field_name, value)
                else:
                    value = getattr(old, old_field_name)
                    if (field_type == 'DateField' and not field.null and 
                        value is None):
                        descricao = 'A data 1111-11-11 foi colocada no lugar'
                        problema = 'O valor da data era nulo ou inválido'
                        warn(
                            'O valor do campo %s (%s) do model %s era inválido => %s'
                             % (field.name, field_type, field.model.
                            __name__, descricao))
                        value = date(1111, 11, 11)
                        self.data_mudada['obj'] = new
                        self.data_mudada['descricao'] = descricao
                        self.data_mudada['problema'] = problema
                        self.data_mudada.setdefault('nome_campo', []).append(
                            field.name)
                    if field_type in ['CharField', 'TextField'] and value in [
                        None, 'None']:
                        value = ''
                    setattr(new, field.name, value)

    def migrate(self, obj=appconfs, interativo=True):
        uniformiza_banco()
        if interativo:
            info(
                'Todos os dados do banco serão excluidos. Recomendamos que faça backup do banco sapl antes de continuar.'
                )
            info('Deseja continuar? [s/n]')
            resposta = input()
            if resposta.lower() in ['s', 'sim', 'y', 'yes']:
                pass
            else:
                info('Migração cancelada.')
                return 0
        info('Excluindo entradas antigas do banco.')
        call([PROJECT_DIR.child('manage.py'), 'flush', '--database=default',
            '--no-input'], stdout=PIPE)
        fill_vinculo_norma_juridica()
        fill_dados_basicos()
        info('Começando migração: %s...' % obj)
        self._do_migrate(obj)
        info('Excluindo possíveis duplicações em RegistroVotacao...')
        excluir_registrovotacao_duplicados()

    def _do_migrate(self, obj):
        if isinstance(obj, AppConfig):
            models_to_migrate = (model for model in obj.models.values() if 
                model in self.field_renames)
            self._do_migrate(models_to_migrate)
        elif isinstance(obj, ModelBase):
            if not obj.__name__ == 'TipoProposicao':
                if obj.__name__ == 'Proposicao':
                    self.migrate_model(TipoProposicao)
                self.migrate_model(obj)
        elif hasattr(obj, '__iter__'):
            for item in obj:
                self._do_migrate(item)
        else:
            raise TypeError(
                'Parameter must be a Model, AppConfig or a sequence of them')

    def migrate_model(self, model):
        print('Migrando %s...' % model.__name__)
        legacy_model_name = self.model_renames.get(model, model.__name__)
        legacy_model = legacy_app.get_model(legacy_model_name)
        legacy_pk_name = legacy_model._meta.pk.name
        if legacy_pk_name == 'id':
            deve_ajustar_sequence_ao_final = False

            def save(new, old):
                with reversion.create_revision():
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')
                delete_old(legacy_model, old.__dict__)
            old_records = iter_sql_records('select * from ' + legacy_model.
                _meta.db_table, 'legacy')
        else:
            deve_ajustar_sequence_ao_final = True

            def save(new, old):
                with reversion.create_revision():
                    new.id = getattr(old, legacy_pk_name)
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')
                delete_old(legacy_model, {legacy_pk_name: new.id})
            old_records = legacy_model.objects.all().order_by(legacy_pk_name)
        ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model)
        ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model)
        with transaction.atomic():
            for old in old_records:
                if getattr(old, 'ind_excluido', False):
                    continue
                new = model()
                try:
                    self.populate_renamed_fields(new, old)
                    if ajuste_antes_salvar:
                        ajuste_antes_salvar(new, old)
                except ForeignKeyFaltando:
                    continue
                else:
                    save(new, old)
                    if ajuste_depois_salvar:
                        ajuste_depois_salvar(new, old)
                    if self.data_mudada:
                        with reversion.create_revision():
                            save_relation(**self.data_mudada)
                            self.data_mudada.clear()
                            reversion.set_comment(
                                'Ajuste de data pela migração')
            if deve_ajustar_sequence_ao_final:
                last_pk = get_last_pk(model)
                reinicia_sequence(model, last_pk + 1)


def migrate(obj=appconfs, interativo=True):
    dm = DataMigrator()
    dm.migrate(obj, interativo)


def adjust_acompanhamentomateria(new, old):
    new.confirmado = True


def adjust_documentoadministrativo(new, old):
    if old.num_protocolo:
        protocolo = Protocolo.objects.filter(numero=old.num_protocolo, ano=
            new.ano)
        if not protocolo:
            protocolo = Protocolo.objects.filter(numero=old.num_protocolo,
                ano=new.ano + 1)
            print(
                'PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! DocumentoAdministrativo: {}, numero_protocolo: {}, ano doc adm: {}'
                .format(old.cod_documento, old.num_protocolo, new.ano))
        if not protocolo:
            raise ForeignKeyFaltando(
                'Protocolo {} faltando (referenciado no documento administrativo {}'
                .format(old.num_protocolo, old.cod_documento))
        assert len(protocolo) == 1
        new.protocolo = protocolo[0]


def adjust_mandato(new, old):
    if old.dat_fim_mandato:
        new.data_fim_mandato = old.dat_fim_mandato
    if not new.data_fim_mandato:
        legislatura = Legislatura.objects.latest('data_fim')
        new.data_fim_mandato = legislatura.data_fim
        new.data_expedicao_diploma = legislatura.data_inicio
    if not new.data_inicio_mandato:
        new.data_inicio_mandato = new.legislatura.data_inicio
        new.data_fim_mandato = new.legislatura.data_fim


<mask token>


def adjust_ordemdia_depois_salvar(new, old):
    if old.num_ordem is None and new.numero_ordem == 999999999:
        with reversion.create_revision():
            problema = (
                'OrdemDia de PK %s tinha seu valor de numero ordem nulo.' %
                old.pk)
            descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('OrdemDia sem número da ordem.')


def adjust_parlamentar(new, old):
    if old.ind_unid_deliberativa:
        value = new.unidade_deliberativa
        if value is None:
            warn('nulo convertido para falso')
            new.unidade_deliberativa = False


def adjust_participacao(new, old):
    composicao = Composicao()
    composicao.comissao_id, composicao.periodo_id = [get_fk_related(
        Composicao._meta.get_field(name), value) for name, value in ((
        'comissao', old.cod_comissao), ('periodo', old.cod_periodo_comp))]
    already_created = Composicao.objects.filter(comissao=composicao.
        comissao, periodo=composicao.periodo)
    if already_created:
        assert len(already_created) == 1
        [composicao] = already_created
    else:
        with reversion.create_revision():
            composicao.save()
            reversion.set_comment('Objeto criado pela migração')
    new.composicao = composicao


def adjust_proposicao_antes_salvar(new, old):
    if new.data_envio:
        new.ano = new.data_envio.year


def adjust_proposicao_depois_salvar(new, old):
    if not hasattr(old.dat_envio, 'year') or old.dat_envio.year == 1800:
        msg = (
            'O valor do campo data_envio (DateField) da model Proposicao era inválido'
            )
        descricao = 'A data 1111-11-11 foi colocada no lugar'
        problema = 'O valor da data era nulo ou inválido'
        warn(msg + ' => ' + descricao)
        new.data_envio = date(1111, 11, 11)
        with reversion.create_revision():
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('Ajuste de data pela migração')


def adjust_normarelacionada(new, old):
    tipo = TipoVinculoNormaJuridica.objects.filter(sigla=old.tip_vinculo)
    assert len(tipo) == 1
    new.tipo_vinculo = tipo[0]


def adjust_protocolo_antes_salvar(new, old):
    if old.num_protocolo is None:
        new.numero = old.cod_protocolo


def adjust_protocolo_depois_salvar(new, old):
    if old.num_protocolo is None:
        with reversion.create_revision():
            problema = 'Número do protocolo de PK %s é nulo' % new.pk
            descricao = 'Número do protocolo alterado para %s!' % new.numero
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('Número de protocolo teve que ser alterado')


def adjust_registrovotacao_antes_salvar(new, old):
    ordem_dia = OrdemDia.objects.filter(pk=old.cod_ordem, materia=old.
        cod_materia)
    expediente_materia = ExpedienteMateria.objects.filter(pk=old.cod_ordem,
        materia=old.cod_materia)
    if ordem_dia and not expediente_materia:
        new.ordem = ordem_dia[0]
    if not ordem_dia and expediente_materia:
        new.expediente = expediente_materia[0]


<mask token>


def adjust_tipoafastamento(new, old):
    if old.ind_afastamento == 1:
        new.indicador = 'A'


def adjust_tipoproposicao(new, old):
    if old.ind_mat_ou_doc == 'M':
        tipo_materia = TipoMateriaLegislativa.objects.filter(pk=old.
            tip_mat_ou_doc)
        if tipo_materia:
            new.tipo_conteudo_related = tipo_materia[0]
        else:
            raise ForeignKeyFaltando
    elif old.ind_mat_ou_doc == 'D':
        tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc)
        if tipo_documento:
            new.tipo_conteudo_related = tipo_documento[0]
        else:
            raise ForeignKeyFaltando


<mask token>


def adjust_tramitacao(new, old):
    if old.sgl_turno == 'Ú':
        new.turno = 'U'


def adjust_tipo_autor(new, old):
    model_apontado = normalize(new.descricao.lower()).replace(' ', '')
    content_types = ContentType.objects.filter(model=model_apontado).exclude(
        app_label='legacy')
    assert len(content_types) <= 1
    new.content_type = content_types[0] if content_types else None


<mask token>


def adjust_normajuridica_depois_salvar(new, old):
    if not old.cod_assunto:
        return
    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


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:
            raise ForeignKeyFaltando('{} inexiste para autor'.format(
                model_relacionado._meta.verbose_name))
        else:
            new.nome = getattr(new.autor_related, campo_nome)
            return True


def adjust_autor(new, old):
    for args in [(Parlamentar, 'cod_parlamentar', 'nome_parlamentar'), (
        Comissao, 'cod_comissao', 'nome'), (Partido, 'cod_partido', 'nome')]:
        if vincula_autor(new, old, *args):
            break
    if old.col_username:
        user_model = get_user_model()
        if not user_model.objects.filter(username=old.col_username).exists():
            user = user_model(username=old.col_username)
            user.set_password(None)
            with reversion.create_revision():
                user.save()
                reversion.set_comment(
                    'Usuário criado pela migração para o autor {}'.format(
                    old.cod_autor))
            grupo_autor = Group.objects.get(name='Autor')
            user.groups.add(grupo_autor)


<mask token>


def check_app_no_ind_excluido(app):
    for model in app.models.values():
        assert not any(get_ind_excluido(new) for new in model.objects.all())
    print('OK!')

<mask token>


def get_renames():
    field_renames = {}
    model_renames = {}
    for app in appconfs:
        app_rename_data = yaml.load(pkg_resources.resource_string(app.
            module.__name__, 'legacy.yaml'))
        for model_name, renames in app_rename_data.items():
            match = MODEL_RENAME_PATTERN.match(model_name)
            if match:
                model_name, old_name = match.groups()
            else:
                old_name = None
            model = getattr(app.models_module, model_name)
            if old_name:
                model_renames[model] = old_name
            field_renames[model] = renames
    for model, renames in field_renames.items():
        if any(parent in field_renames for parent in model.__mro__[1:]):
            renames = {}
            for parent in reversed(model.__mro__):
                if parent in field_renames:
                    renames.update(field_renames[parent])
            field_renames[model] = renames
    field_renames = {m: r for m, r in field_renames.items() if not m._meta.
        abstract}
    return field_renames, model_renames


def info(msg):
    print('INFO: ' + msg)


def warn(msg):
    print('CUIDADO! ' + msg)


class ForeignKeyFaltando(ObjectDoesNotExist):
    """Uma FK aponta para um registro inexistente"""
    pass


@lru_cache()
def _get_all_ids_from_model(model):
    return set(model.objects.values_list('id', flat=True))


def get_fk_related(field, value, label=None):
    if value is None and field.null:
        return None
    if value in _get_all_ids_from_model(field.related_model):
        return value
    else:
        msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % (field.
            name, value, field.model.__name__, label or '---')
        warn(msg)
        raise ForeignKeyFaltando(msg)


def exec_sql(sql, db='default'):
    cursor = connections[db].cursor()
    cursor.execute(sql)
    return cursor


<mask token>


def existe_tabela_no_legado(tabela):
    sql = SQL_NAO_TEM_TABELA.format(tabela)
    return exec_legado(sql).fetchone()[0]


def existe_coluna_no_legado(tabela, coluna):
    sql = SQL_NAO_TEM_COLUNA.format(tabela, coluna)
    return exec_legado(sql).fetchone()[0] > 0


def garante_coluna_no_legado(tabela, spec_coluna):
    coluna = spec_coluna.split()[0]
    if not existe_coluna_no_legado(tabela, coluna):
        exec_legado('ALTER TABLE {} ADD COLUMN {}'.format(tabela, spec_coluna))
    assert existe_coluna_no_legado(tabela, coluna)


def garante_tabela_no_legado(create_table):
    tabela = create_table.strip().splitlines()[0].split()[2]
    if not existe_tabela_no_legado(tabela):
        exec_legado(create_table)
        assert existe_tabela_no_legado(tabela)


def uniformiza_banco():
    exec_legado(
        """
      SELECT replace(@@sql_mode,"STRICT_TRANS_TABLES,","ALLOW_INVALID_DATES");
    """
        )
    exec_legado(
        """
        UPDATE proposicao SET dat_envio = "1800-01-01" WHERE dat_envio = 0;
        alter table proposicao modify dat_envio datetime;
        UPDATE proposicao SET dat_envio = NULL where dat_envio = "1800-01-01";
        """
        )
    garante_coluna_no_legado('proposicao', 'num_proposicao int(11) NULL')
    garante_coluna_no_legado('tipo_materia_legislativa',
        'ind_num_automatica BOOLEAN NULL DEFAULT FALSE')
    garante_coluna_no_legado('tipo_materia_legislativa',
        'quorum_minimo_votacao int(11) NULL')
    if not existe_coluna_no_legado('sessao_plenaria_presenca',
        'cod_presenca_sessao'):
        exec_legado(
            """
            ALTER TABLE sessao_plenaria_presenca
            DROP PRIMARY KEY,
            ADD cod_presenca_sessao INT auto_increment PRIMARY KEY FIRST;
        """
            )
        assert existe_coluna_no_legado('sessao_plenaria_presenca',
            'cod_presenca_sessao')
    garante_coluna_no_legado('sessao_plenaria_presenca', 'dat_sessao DATE NULL'
        )
    garante_tabela_no_legado(
        """
        CREATE TABLE lexml_registro_publicador (
            cod_publicador INT auto_increment NOT NULL,
            id_publicador INT, nom_publicador varchar(255),
            adm_email varchar(50),
            sigla varchar(255),
            nom_responsavel varchar(255),
            tipo varchar(50),
            id_responsavel INT, PRIMARY KEY (cod_publicador));
    """
        )
    garante_tabela_no_legado(
        """
        CREATE TABLE lexml_registro_provedor (
            cod_provedor INT auto_increment NOT NULL,
            id_provedor INT, nom_provedor varchar(255),
            sgl_provedor varchar(15),
            adm_email varchar(50),
            nom_responsavel varchar(255),
            tipo varchar(50),
            id_responsavel INT, xml_provedor longtext,
            PRIMARY KEY (cod_provedor));
    """
        )
    garante_tabela_no_legado(
        """
        CREATE TABLE tipo_situacao_militar (
            tip_situacao_militar INT auto_increment NOT NULL,
            des_tipo_situacao varchar(50),
            ind_excluido INT, PRIMARY KEY (tip_situacao_militar));
    """
        )
    update_specs = (
        """
vinculo_norma_juridica| ind_excluido = ''           | trim(ind_excluido) = '0'
unidade_tramitacao    | cod_parlamentar = NULL      | cod_parlamentar = 0
parlamentar           | cod_nivel_instrucao = NULL  | cod_nivel_instrucao = 0
parlamentar           | tip_situacao_militar = NULL | tip_situacao_militar = 0
mandato               | tip_afastamento = NULL      | tip_afastamento = 0
relatoria             | tip_fim_relatoria = NULL    | tip_fim_relatoria = 0
    """
        .strip().splitlines())
    for spec in update_specs:
        spec = spec.split('|')
        exec_legado('UPDATE {} SET {} WHERE {}'.format(*spec))


def iter_sql_records(sql, db):


    class Record:
        pass
    cursor = exec_sql(sql, db)
    fieldnames = [name[0] for name in cursor.description]
    for row in cursor.fetchall():
        record = Record()
        record.__dict__.update(zip(fieldnames, row))
        yield record


def save_relation(obj, nome_campo='', problema='', descricao='', eh_stub=
    False, critico=False):
    link = ProblemaMigracao(content_object=obj, nome_campo=nome_campo,
        problema=problema, descricao=descricao, eh_stub=eh_stub, critico=
        critico)
    link.save()


def fill_vinculo_norma_juridica():
    lista = [('A', 'Altera o(a)', 'Alterado(a) pelo(a)'), ('R',
        'Revoga integralmente o(a)', 'Revogado(a) integralmente pelo(a)'),
        ('P', 'Revoga parcialmente o(a)',
        'Revogado(a) parcialmente pelo(a)'), ('T',
        'Revoga integralmente por consolidação',
        'Revogado(a) integralmente por consolidação'), ('C',
        'Norma correlata', 'Norma correlata'), ('S', 'Ressalva o(a)',
        'Ressalvada pelo(a)'), ('E', 'Reedita o(a)', 'Reeditada pelo(a)'),
        ('I', 'Reedita com alteração o(a)',
        'Reeditada com alteração pelo(a)'), ('G', 'Regulamenta o(a)',
        'Regulamentada pelo(a)'), ('K', 'Suspende parcialmente o(a)',
        'Suspenso(a) parcialmente pelo(a)'), ('L',
        'Suspende integralmente o(a)', 'Suspenso(a) integralmente pelo(a)'),
        ('N', 'Julga integralmente inconstitucional',
        'Julgada integralmente inconstitucional'), ('O',
        'Julga parcialmente inconstitucional',
        'Julgada parcialmente inconstitucional')]
    lista_objs = [TipoVinculoNormaJuridica(sigla=item[0], descricao_ativa=
        item[1], descricao_passiva=item[2]) for item in lista]
    TipoVinculoNormaJuridica.objects.bulk_create(lista_objs)


def fill_dados_basicos():
    letra = 'A'
    try:
        tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo')
        if 'POR ANO' in tipo.des_numeracao_protocolo:
            letra = 'A'
        elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo:
            letra = 'L'
        elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo:
            letra = 'U'
    except Exception as e:
        pass
    appconf = AppConf(sequencia_numeracao=letra)
    appconf.save()
    casa = CasaLegislativa()
    casa.save()


def excluir_registrovotacao_duplicados():
    duplicatas_ids = RegistroVotacao.objects.values('materia', 'ordem',
        'expediente').annotate(Count('id')).order_by().filter(id__count__gt=1)
    duplicatas_queryset = RegistroVotacao.objects.filter(materia__in=[item[
        'materia'] for item in duplicatas_ids])
    for dup in duplicatas_queryset:
        lista_dups = duplicatas_queryset.filter(materia=dup.materia,
            expediente=dup.expediente, ordem=dup.ordem)
        primeiro_registro = lista_dups[0]
        lista_dups = lista_dups.exclude(pk=primeiro_registro.pk)
        for objeto in lista_dups:
            if objeto.pk > primeiro_registro.pk:
                try:
                    objeto.delete()
                except:
                    assert 0
            else:
                try:
                    primeiro_registro.delete()
                    primeiro_registro = objeto
                except:
                    assert 0


def delete_old(legacy_model, cols_values):
    if legacy_model.__name__ == 'ExpedienteSessaoPlenaria':
        cols_values.pop('txt_expediente')

    def eq_clause(col, value):
        if value is None:
            return '{} IS NULL'.format(col)
        else:
            return '{}="{}"'.format(col, value)
    delete_sql = 'delete from {} where {}'.format(legacy_model._meta.
        db_table, ' and '.join([eq_clause(col, value) for col, value in
        cols_values.items()]))
    exec_sql(delete_sql, 'legacy')


def get_last_pk(model):
    last_value = model.objects.all().aggregate(Max('pk'))
    return last_value['pk__max'] or 0


def reinicia_sequence(model, id):
    sequence_name = '%s_id_seq' % model._meta.db_table
    exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % (
        sequence_name, id))


class DataMigrator:

    def __init__(self):
        self.field_renames, self.model_renames = get_renames()
        self.data_mudada = {}
        self.choice_valida = {}

    def populate_renamed_fields(self, new, old):
        renames = self.field_renames[type(new)]
        for field in new._meta.fields:
            old_field_name = renames.get(field.name)
            field_type = field.get_internal_type()
            if old_field_name:
                old_value = getattr(old, old_field_name)
                if field_type == 'ForeignKey':
                    if hasattr(old, '_meta') and old._meta.pk.name != 'id':
                        label = old.pk
                    else:
                        label = '-- SEM PK --'
                    fk_field_name = '{}_id'.format(field.name)
                    value = get_fk_related(field, old_value, label)
                    setattr(new, fk_field_name, value)
                else:
                    value = getattr(old, old_field_name)
                    if (field_type == 'DateField' and not field.null and 
                        value is None):
                        descricao = 'A data 1111-11-11 foi colocada no lugar'
                        problema = 'O valor da data era nulo ou inválido'
                        warn(
                            'O valor do campo %s (%s) do model %s era inválido => %s'
                             % (field.name, field_type, field.model.
                            __name__, descricao))
                        value = date(1111, 11, 11)
                        self.data_mudada['obj'] = new
                        self.data_mudada['descricao'] = descricao
                        self.data_mudada['problema'] = problema
                        self.data_mudada.setdefault('nome_campo', []).append(
                            field.name)
                    if field_type in ['CharField', 'TextField'] and value in [
                        None, 'None']:
                        value = ''
                    setattr(new, field.name, value)

    def migrate(self, obj=appconfs, interativo=True):
        uniformiza_banco()
        if interativo:
            info(
                'Todos os dados do banco serão excluidos. Recomendamos que faça backup do banco sapl antes de continuar.'
                )
            info('Deseja continuar? [s/n]')
            resposta = input()
            if resposta.lower() in ['s', 'sim', 'y', 'yes']:
                pass
            else:
                info('Migração cancelada.')
                return 0
        info('Excluindo entradas antigas do banco.')
        call([PROJECT_DIR.child('manage.py'), 'flush', '--database=default',
            '--no-input'], stdout=PIPE)
        fill_vinculo_norma_juridica()
        fill_dados_basicos()
        info('Começando migração: %s...' % obj)
        self._do_migrate(obj)
        info('Excluindo possíveis duplicações em RegistroVotacao...')
        excluir_registrovotacao_duplicados()

    def _do_migrate(self, obj):
        if isinstance(obj, AppConfig):
            models_to_migrate = (model for model in obj.models.values() if 
                model in self.field_renames)
            self._do_migrate(models_to_migrate)
        elif isinstance(obj, ModelBase):
            if not obj.__name__ == 'TipoProposicao':
                if obj.__name__ == 'Proposicao':
                    self.migrate_model(TipoProposicao)
                self.migrate_model(obj)
        elif hasattr(obj, '__iter__'):
            for item in obj:
                self._do_migrate(item)
        else:
            raise TypeError(
                'Parameter must be a Model, AppConfig or a sequence of them')

    def migrate_model(self, model):
        print('Migrando %s...' % model.__name__)
        legacy_model_name = self.model_renames.get(model, model.__name__)
        legacy_model = legacy_app.get_model(legacy_model_name)
        legacy_pk_name = legacy_model._meta.pk.name
        if legacy_pk_name == 'id':
            deve_ajustar_sequence_ao_final = False

            def save(new, old):
                with reversion.create_revision():
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')
                delete_old(legacy_model, old.__dict__)
            old_records = iter_sql_records('select * from ' + legacy_model.
                _meta.db_table, 'legacy')
        else:
            deve_ajustar_sequence_ao_final = True

            def save(new, old):
                with reversion.create_revision():
                    new.id = getattr(old, legacy_pk_name)
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')
                delete_old(legacy_model, {legacy_pk_name: new.id})
            old_records = legacy_model.objects.all().order_by(legacy_pk_name)
        ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model)
        ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model)
        with transaction.atomic():
            for old in old_records:
                if getattr(old, 'ind_excluido', False):
                    continue
                new = model()
                try:
                    self.populate_renamed_fields(new, old)
                    if ajuste_antes_salvar:
                        ajuste_antes_salvar(new, old)
                except ForeignKeyFaltando:
                    continue
                else:
                    save(new, old)
                    if ajuste_depois_salvar:
                        ajuste_depois_salvar(new, old)
                    if self.data_mudada:
                        with reversion.create_revision():
                            save_relation(**self.data_mudada)
                            self.data_mudada.clear()
                            reversion.set_comment(
                                'Ajuste de data pela migração')
            if deve_ajustar_sequence_ao_final:
                last_pk = get_last_pk(model)
                reinicia_sequence(model, last_pk + 1)


def migrate(obj=appconfs, interativo=True):
    dm = DataMigrator()
    dm.migrate(obj, interativo)


def adjust_acompanhamentomateria(new, old):
    new.confirmado = True


def adjust_documentoadministrativo(new, old):
    if old.num_protocolo:
        protocolo = Protocolo.objects.filter(numero=old.num_protocolo, ano=
            new.ano)
        if not protocolo:
            protocolo = Protocolo.objects.filter(numero=old.num_protocolo,
                ano=new.ano + 1)
            print(
                'PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! DocumentoAdministrativo: {}, numero_protocolo: {}, ano doc adm: {}'
                .format(old.cod_documento, old.num_protocolo, new.ano))
        if not protocolo:
            raise ForeignKeyFaltando(
                'Protocolo {} faltando (referenciado no documento administrativo {}'
                .format(old.num_protocolo, old.cod_documento))
        assert len(protocolo) == 1
        new.protocolo = protocolo[0]


def adjust_mandato(new, old):
    if old.dat_fim_mandato:
        new.data_fim_mandato = old.dat_fim_mandato
    if not new.data_fim_mandato:
        legislatura = Legislatura.objects.latest('data_fim')
        new.data_fim_mandato = legislatura.data_fim
        new.data_expedicao_diploma = legislatura.data_inicio
    if not new.data_inicio_mandato:
        new.data_inicio_mandato = new.legislatura.data_inicio
        new.data_fim_mandato = new.legislatura.data_fim


def adjust_ordemdia_antes_salvar(new, old):
    new.votacao_aberta = False
    if not old.tip_votacao:
        new.tipo_votacao = 1
    if old.num_ordem is None:
        new.numero_ordem = 999999999


def adjust_ordemdia_depois_salvar(new, old):
    if old.num_ordem is None and new.numero_ordem == 999999999:
        with reversion.create_revision():
            problema = (
                'OrdemDia de PK %s tinha seu valor de numero ordem nulo.' %
                old.pk)
            descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('OrdemDia sem número da ordem.')


def adjust_parlamentar(new, old):
    if old.ind_unid_deliberativa:
        value = new.unidade_deliberativa
        if value is None:
            warn('nulo convertido para falso')
            new.unidade_deliberativa = False


def adjust_participacao(new, old):
    composicao = Composicao()
    composicao.comissao_id, composicao.periodo_id = [get_fk_related(
        Composicao._meta.get_field(name), value) for name, value in ((
        'comissao', old.cod_comissao), ('periodo', old.cod_periodo_comp))]
    already_created = Composicao.objects.filter(comissao=composicao.
        comissao, periodo=composicao.periodo)
    if already_created:
        assert len(already_created) == 1
        [composicao] = already_created
    else:
        with reversion.create_revision():
            composicao.save()
            reversion.set_comment('Objeto criado pela migração')
    new.composicao = composicao


def adjust_proposicao_antes_salvar(new, old):
    if new.data_envio:
        new.ano = new.data_envio.year


def adjust_proposicao_depois_salvar(new, old):
    if not hasattr(old.dat_envio, 'year') or old.dat_envio.year == 1800:
        msg = (
            'O valor do campo data_envio (DateField) da model Proposicao era inválido'
            )
        descricao = 'A data 1111-11-11 foi colocada no lugar'
        problema = 'O valor da data era nulo ou inválido'
        warn(msg + ' => ' + descricao)
        new.data_envio = date(1111, 11, 11)
        with reversion.create_revision():
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('Ajuste de data pela migração')


def adjust_normarelacionada(new, old):
    tipo = TipoVinculoNormaJuridica.objects.filter(sigla=old.tip_vinculo)
    assert len(tipo) == 1
    new.tipo_vinculo = tipo[0]


def adjust_protocolo_antes_salvar(new, old):
    if old.num_protocolo is None:
        new.numero = old.cod_protocolo


def adjust_protocolo_depois_salvar(new, old):
    if old.num_protocolo is None:
        with reversion.create_revision():
            problema = 'Número do protocolo de PK %s é nulo' % new.pk
            descricao = 'Número do protocolo alterado para %s!' % new.numero
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('Número de protocolo teve que ser alterado')


def adjust_registrovotacao_antes_salvar(new, old):
    ordem_dia = OrdemDia.objects.filter(pk=old.cod_ordem, materia=old.
        cod_materia)
    expediente_materia = ExpedienteMateria.objects.filter(pk=old.cod_ordem,
        materia=old.cod_materia)
    if ordem_dia and not expediente_materia:
        new.ordem = ordem_dia[0]
    if not ordem_dia and expediente_materia:
        new.expediente = expediente_materia[0]


def adjust_registrovotacao_depois_salvar(new, old):
    if not new.ordem and not new.expediente:
        with reversion.create_revision():
            problema = (
                'RegistroVotacao de PK %s não possui nenhuma OrdemDia ou ExpedienteMateria.'
                 % old.pk)
            descricao = (
                'RevistroVotacao deve ter no mínimo uma ordem do dia ou expediente vinculado.'
                )
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema, descricao=descricao,
                eh_stub=False)
            reversion.set_comment('RegistroVotacao sem ordem ou expediente')


def adjust_tipoafastamento(new, old):
    if old.ind_afastamento == 1:
        new.indicador = 'A'


def adjust_tipoproposicao(new, old):
    if old.ind_mat_ou_doc == 'M':
        tipo_materia = TipoMateriaLegislativa.objects.filter(pk=old.
            tip_mat_ou_doc)
        if tipo_materia:
            new.tipo_conteudo_related = tipo_materia[0]
        else:
            raise ForeignKeyFaltando
    elif old.ind_mat_ou_doc == 'D':
        tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc)
        if tipo_documento:
            new.tipo_conteudo_related = tipo_documento[0]
        else:
            raise ForeignKeyFaltando


def adjust_statustramitacao(new, old):
    if old.ind_fim_tramitacao:
        new.indicador = 'F'
    elif old.ind_retorno_tramitacao:
        new.indicador = 'R'
    else:
        new.indicador = ''


def adjust_statustramitacaoadm(new, old):
    adjust_statustramitacao(new, old)


def adjust_tramitacao(new, old):
    if old.sgl_turno == 'Ú':
        new.turno = 'U'


def adjust_tipo_autor(new, old):
    model_apontado = normalize(new.descricao.lower()).replace(' ', '')
    content_types = ContentType.objects.filter(model=model_apontado).exclude(
        app_label='legacy')
    assert len(content_types) <= 1
    new.content_type = content_types[0] if content_types else None


def adjust_normajuridica_antes_salvar(new, old):
    if old.tip_esfera_federacao == 'S':
        new.esfera_federacao = ''


def adjust_normajuridica_depois_salvar(new, old):
    if not old.cod_assunto:
        return
    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


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:
            raise ForeignKeyFaltando('{} inexiste para autor'.format(
                model_relacionado._meta.verbose_name))
        else:
            new.nome = getattr(new.autor_related, campo_nome)
            return True


def adjust_autor(new, old):
    for args in [(Parlamentar, 'cod_parlamentar', 'nome_parlamentar'), (
        Comissao, 'cod_comissao', 'nome'), (Partido, 'cod_partido', 'nome')]:
        if vincula_autor(new, old, *args):
            break
    if old.col_username:
        user_model = get_user_model()
        if not user_model.objects.filter(username=old.col_username).exists():
            user = user_model(username=old.col_username)
            user.set_password(None)
            with reversion.create_revision():
                user.save()
                reversion.set_comment(
                    'Usuário criado pela migração para o autor {}'.format(
                    old.cod_autor))
            grupo_autor = Group.objects.get(name='Autor')
            user.groups.add(grupo_autor)


def adjust_comissao(new, old):
    if not old.dat_extincao and not old.dat_fim_comissao:
        new.ativa = True
    elif old.dat_extincao and date.today(
        ) < new.data_extincao or old.dat_fim_comissao and date.today(
        ) < new.data_fim_comissao:
        new.ativa = True
    else:
        new.ativa = False


<mask token>


def get_ind_excluido(new):
    legacy_model = legacy_app.get_model(type(new).__name__)
    old = legacy_model.objects.get(**{legacy_model._meta.pk.name: new.id})
    return getattr(old, 'ind_excluido', False)


def check_app_no_ind_excluido(app):
    for model in app.models.values():
        assert not any(get_ind_excluido(new) for new in model.objects.all())
    print('OK!')

from datetime import date
from functools import lru_cache, partial
from subprocess import PIPE, call
import re

from django.apps import apps
from django.apps.config import AppConfig
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import connections, transaction
from django.db.models import Count, Max
from django.db.models.base import ModelBase
from sapl.base.models import (Autor, CasaLegislativa, ProblemaMigracao,
                              TipoAutor)
from sapl.base.models import AppConfig as AppConf
from sapl.comissoes.models import Comissao, Composicao, Participacao
from sapl.legacy.models import TipoNumeracaoProtocolo
from sapl.materia.models import (AcompanhamentoMateria, Proposicao,
                                 StatusTramitacao, TipoDocumento,
                                 TipoMateriaLegislativa, TipoProposicao,
                                 Tramitacao)
from sapl.norma.models import (AssuntoNorma, NormaJuridica, NormaRelacionada,
                               TipoVinculoNormaJuridica)
from sapl.parlamentares.models import (Legislatura, Mandato, Parlamentar,
                                       Partido, TipoAfastamento)
from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo,
                                      StatusTramitacaoAdministrativo)
from sapl.sessao.models import ExpedienteMateria, OrdemDia, RegistroVotacao
from sapl.utils import normalize
import pkg_resources
import reversion
import yaml

from cmj.settings import PROJECT_DIR


# BASE ######################################################################
#  apps to be migrated, in app dependency order (very important)
appconfs = [apps.get_app_config(n) for n in [
    'parlamentares',
    'comissoes',
    'base',
    'materia',
    'norma',
    'sessao',
    'lexml',
    'protocoloadm', ]]

unique_constraints = []
one_to_one_constraints = []
primeira_vez = []

name_sets = [set(m.__name__ for m in ac.get_models()) for ac in appconfs]

# apps do not overlap
for s1 in name_sets:
    for s2 in name_sets:
        if s1 is not s2:
            assert not s1.intersection(s2)

# apps include all legacy models
legacy_app = apps.get_app_config('legacy')
legacy_model_names = set(m.__name__ for m in legacy_app.get_models())

model_dict = {m.__name__: m for ac in appconfs for m in ac.get_models()}


# RENAMES ###################################################################

MODEL_RENAME_PATTERN = re.compile('(.+) \((.+)\)')


def get_renames():
    field_renames = {}
    model_renames = {}
    for app in appconfs:
        app_rename_data = yaml.load(
            pkg_resources.resource_string(app.module.__name__, 'legacy.yaml'))
        for model_name, renames in app_rename_data.items():
            match = MODEL_RENAME_PATTERN.match(model_name)
            if match:
                model_name, old_name = match.groups()
            else:
                old_name = None
            model = getattr(app.models_module, model_name)
            if old_name:
                model_renames[model] = old_name
            field_renames[model] = renames

    # collect renames from parent classes
    for model, renames in field_renames.items():
        if any(parent in field_renames for parent in model.__mro__[1:]):
            renames = {}
            for parent in reversed(model.__mro__):
                if parent in field_renames:
                    renames.update(field_renames[parent])
            field_renames[model] = renames

    # remove abstract classes
    field_renames = {m: r for m, r in field_renames.items()
                     if not m._meta.abstract}

    return field_renames, model_renames

# MIGRATION #################################################################


def info(msg):
    print('INFO: ' + msg)


def warn(msg):
    print('CUIDADO! ' + msg)


class ForeignKeyFaltando(ObjectDoesNotExist):
    'Uma FK aponta para um registro inexistente'
    pass


@lru_cache()
def _get_all_ids_from_model(model):
    # esta função para uso apenas em get_fk_related
    return set(model.objects.values_list('id', flat=True))


def get_fk_related(field, value, label=None):
    if value is None and field.null:
        return None

    # if field.related_model.objects.filter(id=value).exists():
    if value in _get_all_ids_from_model(field.related_model):
        return value
    else:
        msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % (
            field.name, value, field.model.__name__, label or '---')
        warn(msg)
        raise ForeignKeyFaltando(msg)


def exec_sql(sql, db='default'):
    cursor = connections[db].cursor()
    cursor.execute(sql)
    return cursor

# UNIFORMIZAÇÃO DO BANCO ANTES DA MIGRAÇÃO ###############################


SQL_NAO_TEM_TABELA = '''
   SELECT count(*)
   FROM information_schema.columns
   WHERE table_schema=database()
     AND TABLE_NAME="{}"
'''
SQL_NAO_TEM_COLUNA = SQL_NAO_TEM_TABELA + ' AND COLUMN_NAME="{}"'

exec_legado = partial(exec_sql, db='legacy')


def existe_tabela_no_legado(tabela):
    sql = SQL_NAO_TEM_TABELA.format(tabela)
    return exec_legado(sql).fetchone()[0]


def existe_coluna_no_legado(tabela, coluna):
    sql = SQL_NAO_TEM_COLUNA.format(tabela, coluna)
    return exec_legado(sql).fetchone()[0] > 0


def garante_coluna_no_legado(tabela, spec_coluna):
    coluna = spec_coluna.split()[0]
    if not existe_coluna_no_legado(tabela, coluna):
        exec_legado('ALTER TABLE {} ADD COLUMN {}'.format(tabela, spec_coluna))
    assert existe_coluna_no_legado(tabela, coluna)


def garante_tabela_no_legado(create_table):
    tabela = create_table.strip().splitlines()[0].split()[2]
    if not existe_tabela_no_legado(tabela):
        exec_legado(create_table)
        assert existe_tabela_no_legado(tabela)


def uniformiza_banco():
    exec_legado('''
      SELECT replace(@@sql_mode,"STRICT_TRANS_TABLES,","ALLOW_INVALID_DATES");
    ''')

    # ajusta data zero em proposicao
    # isso é necessário para poder alterar a tabela a seguir
    exec_legado('''
        UPDATE proposicao SET dat_envio = "1800-01-01" WHERE dat_envio = 0;
        alter table proposicao modify dat_envio datetime;
        UPDATE proposicao SET dat_envio = NULL where dat_envio = "1800-01-01";
        ''')

    garante_coluna_no_legado('proposicao',
                             'num_proposicao int(11) NULL')

    garante_coluna_no_legado('tipo_materia_legislativa',
                             'ind_num_automatica BOOLEAN NULL DEFAULT FALSE')

    garante_coluna_no_legado('tipo_materia_legislativa',
                             'quorum_minimo_votacao int(11) NULL')

    # Cria campos cod_presenca_sessao (sendo a nova PK da tabela)
    # e dat_sessao em sessao_plenaria_presenca
    if not existe_coluna_no_legado('sessao_plenaria_presenca',
                                   'cod_presenca_sessao'):
        exec_legado('''
            ALTER TABLE sessao_plenaria_presenca
            DROP PRIMARY KEY,
            ADD cod_presenca_sessao INT auto_increment PRIMARY KEY FIRST;
        ''')
        assert existe_coluna_no_legado('sessao_plenaria_presenca',
                                       'cod_presenca_sessao')

    garante_coluna_no_legado('sessao_plenaria_presenca',
                             'dat_sessao DATE NULL')

    garante_tabela_no_legado('''
        CREATE TABLE lexml_registro_publicador (
            cod_publicador INT auto_increment NOT NULL,
            id_publicador INT, nom_publicador varchar(255),
            adm_email varchar(50),
            sigla varchar(255),
            nom_responsavel varchar(255),
            tipo varchar(50),
            id_responsavel INT, PRIMARY KEY (cod_publicador));
    ''')

    garante_tabela_no_legado('''
        CREATE TABLE lexml_registro_provedor (
            cod_provedor INT auto_increment NOT NULL,
            id_provedor INT, nom_provedor varchar(255),
            sgl_provedor varchar(15),
            adm_email varchar(50),
            nom_responsavel varchar(255),
            tipo varchar(50),
            id_responsavel INT, xml_provedor longtext,
            PRIMARY KEY (cod_provedor));
    ''')

    garante_tabela_no_legado('''
        CREATE TABLE tipo_situacao_militar (
            tip_situacao_militar INT auto_increment NOT NULL,
            des_tipo_situacao varchar(50),
            ind_excluido INT, PRIMARY KEY (tip_situacao_militar));
    ''')

    update_specs = '''
vinculo_norma_juridica| ind_excluido = ''           | trim(ind_excluido) = '0'
unidade_tramitacao    | cod_parlamentar = NULL      | cod_parlamentar = 0
parlamentar           | cod_nivel_instrucao = NULL  | cod_nivel_instrucao = 0
parlamentar           | tip_situacao_militar = NULL | tip_situacao_militar = 0
mandato               | tip_afastamento = NULL      | tip_afastamento = 0
relatoria             | tip_fim_relatoria = NULL    | tip_fim_relatoria = 0
    '''.strip().splitlines()

    for spec in update_specs:
        spec = spec.split('|')
        exec_legado('UPDATE {} SET {} WHERE {}'.format(*spec))


def iter_sql_records(sql, db):
    class Record:
        pass
    cursor = exec_sql(sql, db)
    fieldnames = [name[0] for name in cursor.description]
    for row in cursor.fetchall():
        record = Record()
        record.__dict__.update(zip(fieldnames, row))
        yield record


def save_relation(obj, nome_campo='', problema='', descricao='',
                  eh_stub=False, critico=False):
    link = ProblemaMigracao(
        content_object=obj, nome_campo=nome_campo, problema=problema,
        descricao=descricao, eh_stub=eh_stub, critico=critico)
    link.save()


def fill_vinculo_norma_juridica():
    lista = [('A', 'Altera o(a)',
              'Alterado(a) pelo(a)'),
             ('R', 'Revoga integralmente o(a)',
              'Revogado(a) integralmente pelo(a)'),
             ('P', 'Revoga parcialmente o(a)',
              'Revogado(a) parcialmente pelo(a)'),
             ('T', 'Revoga integralmente por consolidação',
              'Revogado(a) integralmente por consolidação'),
             ('C', 'Norma correlata',
              'Norma correlata'),
             ('S', 'Ressalva o(a)',
              'Ressalvada pelo(a)'),
             ('E', 'Reedita o(a)',
              'Reeditada pelo(a)'),
             ('I', 'Reedita com alteração o(a)',
              'Reeditada com alteração pelo(a)'),
             ('G', 'Regulamenta o(a)',
              'Regulamentada pelo(a)'),
             ('K', 'Suspende parcialmente o(a)',
              'Suspenso(a) parcialmente pelo(a)'),
             ('L', 'Suspende integralmente o(a)',
              'Suspenso(a) integralmente pelo(a)'),
             ('N', 'Julga integralmente inconstitucional',
              'Julgada integralmente inconstitucional'),
             ('O', 'Julga parcialmente inconstitucional',
              'Julgada parcialmente inconstitucional')]
    lista_objs = [TipoVinculoNormaJuridica(
        sigla=item[0], descricao_ativa=item[1], descricao_passiva=item[2])
        for item in lista]
    TipoVinculoNormaJuridica.objects.bulk_create(lista_objs)


def fill_dados_basicos():
    # Ajusta sequencia numérica e cria base.AppConfig
    letra = 'A'
    try:
        tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo')
        if 'POR ANO' in tipo.des_numeracao_protocolo:
            letra = 'A'
        elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo:
            letra = 'L'
        elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo:
            letra = 'U'
    except Exception as e:
        pass
    appconf = AppConf(sequencia_numeracao=letra)
    appconf.save()

    # Cria instância de CasaLegislativa
    casa = CasaLegislativa()
    casa.save()


# Uma anomalia no sapl 2.5 causa a duplicação de registros de votação.
# Essa duplicação deve ser eliminada para que não haja erro no sapl 3.1
def excluir_registrovotacao_duplicados():
    duplicatas_ids = RegistroVotacao.objects.values(
        'materia', 'ordem', 'expediente').annotate(
            Count('id')).order_by().filter(id__count__gt=1)
    duplicatas_queryset = RegistroVotacao.objects.filter(
        materia__in=[item['materia'] for item in duplicatas_ids])

    for dup in duplicatas_queryset:
        lista_dups = duplicatas_queryset.filter(
            materia=dup.materia, expediente=dup.expediente, ordem=dup.ordem)
        primeiro_registro = lista_dups[0]
        lista_dups = lista_dups.exclude(pk=primeiro_registro.pk)
        for objeto in lista_dups:
            if (objeto.pk > primeiro_registro.pk):
                try:
                    objeto.delete()
                except:
                    assert 0
            else:
                try:
                    primeiro_registro.delete()
                    primeiro_registro = objeto
                except:
                    assert 0


def delete_old(legacy_model, cols_values):
    # ajuste necessário por conta de cósigos html em txt_expediente
    if legacy_model.__name__ == 'ExpedienteSessaoPlenaria':
        cols_values.pop('txt_expediente')

    def eq_clause(col, value):
        if value is None:
            return '{} IS NULL'.format(col)
        else:
            return '{}="{}"'.format(col, value)

    delete_sql = 'delete from {} where {}'.format(
        legacy_model._meta.db_table,
        ' and '.join([eq_clause(col, value)
                      for col, value in cols_values.items()]))
    exec_sql(delete_sql, 'legacy')


def get_last_pk(model):
    last_value = model.objects.all().aggregate(Max('pk'))
    return last_value['pk__max'] or 0


def reinicia_sequence(model, id):
    sequence_name = '%s_id_seq' % model._meta.db_table
    exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % (
        sequence_name, id))


class DataMigrator:

    def __init__(self):
        self.field_renames, self.model_renames = get_renames()
        self.data_mudada = {}
        self.choice_valida = {}

    def populate_renamed_fields(self, new, old):
        renames = self.field_renames[type(new)]

        for field in new._meta.fields:
            old_field_name = renames.get(field.name)
            field_type = field.get_internal_type()
            if old_field_name:
                old_value = getattr(old, old_field_name)

                if field_type == 'ForeignKey':
                    # not necessarily a model
                    if hasattr(old, '_meta') and old._meta.pk.name != 'id':
                        label = old.pk
                    else:
                        label = '-- SEM PK --'
                    fk_field_name = '{}_id'.format(field.name)
                    value = get_fk_related(field, old_value, label)
                    setattr(new, fk_field_name, value)
                else:
                    value = getattr(old, old_field_name)
                    # TODO rever esse DateField após as mudança para datas com
                    # timezone
                    if field_type == 'DateField' and \
                            not field.null and value is None:
                        # TODO REVER ISSO
                        descricao = 'A data 1111-11-11 foi colocada no lugar'
                        problema = 'O valor da data era nulo ou inválido'
                        warn("O valor do campo %s (%s) do model %s "
                             "era inválido => %s" % (
                                 field.name, field_type,
                                 field.model.__name__, descricao))
                        value = date(1111, 11, 11)
                        self.data_mudada['obj'] = new
                        self.data_mudada['descricao'] = descricao
                        self.data_mudada['problema'] = problema
                        self.data_mudada.setdefault('nome_campo', []).\
                            append(field.name)
                    if (field_type in ['CharField', 'TextField']
                            and value in [None, 'None']):
                        value = ''
                    setattr(new, field.name, value)

    def migrate(self, obj=appconfs, interativo=True):
        # warning: model/app migration order is of utmost importance

        uniformiza_banco()

        # excluindo database antigo.
        if interativo:
            info('Todos os dados do banco serão excluidos. '
                 'Recomendamos que faça backup do banco sapl '
                 'antes de continuar.')
            info('Deseja continuar? [s/n]')
            resposta = input()
            if resposta.lower() in ['s', 'sim', 'y', 'yes']:
                pass
            else:
                info('Migração cancelada.')
                return 0
        info('Excluindo entradas antigas do banco.')
        call([PROJECT_DIR.child('manage.py'), 'flush',
              '--database=default', '--no-input'], stdout=PIPE)

        fill_vinculo_norma_juridica()
        fill_dados_basicos()
        info('Começando migração: %s...' % obj)
        self._do_migrate(obj)

        info('Excluindo possíveis duplicações em RegistroVotacao...')
        excluir_registrovotacao_duplicados()

    def _do_migrate(self, obj):
        if isinstance(obj, AppConfig):
            models_to_migrate = (model for model in obj.models.values()
                                 if model in self.field_renames)
            self._do_migrate(models_to_migrate)
        elif isinstance(obj, ModelBase):
            # A migração vai pular TipoProposicao e só vai migrar essa model
            # antes de migrar Proposicao. Isso deve acontecer por causa da
            # GenericRelation existente em TipoProposicao.
            if not obj.__name__ == 'TipoProposicao':
                if obj.__name__ == 'Proposicao':
                    self.migrate_model(TipoProposicao)
                self.migrate_model(obj)
        elif hasattr(obj, '__iter__'):
            for item in obj:
                self._do_migrate(item)
        else:
            raise TypeError(
                'Parameter must be a Model, AppConfig or a sequence of them')

    def migrate_model(self, model):
        print('Migrando %s...' % model.__name__)

        legacy_model_name = self.model_renames.get(model, model.__name__)
        legacy_model = legacy_app.get_model(legacy_model_name)
        legacy_pk_name = legacy_model._meta.pk.name

        # setup migration strategy for tables with or without a pk
        if legacy_pk_name == 'id':
            deve_ajustar_sequence_ao_final = False
            # There is no pk in the legacy table

            def save(new, old):
                with reversion.create_revision():
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')

                # apaga registro do legado
                delete_old(legacy_model, old.__dict__)

            old_records = iter_sql_records(
                'select * from ' + legacy_model._meta.db_table, 'legacy')
        else:
            deve_ajustar_sequence_ao_final = True

            def save(new, old):
                with reversion.create_revision():
                    # salva new com id de old
                    new.id = getattr(old, legacy_pk_name)
                    new.save()
                    reversion.set_comment('Objeto criado pela migração')

                # apaga registro do legado
                delete_old(legacy_model, {legacy_pk_name: new.id})

            old_records = legacy_model.objects.all().order_by(legacy_pk_name)

        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():
            for old in old_records:
                if getattr(old, 'ind_excluido', False):
                    # não migramos registros marcados como excluídos
                    continue
                new = model()
                try:
                    self.populate_renamed_fields(new, old)
                    if ajuste_antes_salvar:
                        ajuste_antes_salvar(new, old)
                except ForeignKeyFaltando:
                    # tentamos preencher uma FK e o ojeto relacionado
                    # não existe
                    # então este é um objeo órfão: simplesmente ignoramos
                    continue
                else:
                    save(new, old)
                    if ajuste_depois_salvar:
                        ajuste_depois_salvar(new, old)

                    if self.data_mudada:
                        with reversion.create_revision():
                            save_relation(**self.data_mudada)
                            self.data_mudada.clear()
                            reversion.set_comment(
                                'Ajuste de data pela migração')
            # reinicia sequence
            if deve_ajustar_sequence_ao_final:
                last_pk = get_last_pk(model)
                reinicia_sequence(model, last_pk + 1)


def migrate(obj=appconfs, interativo=True):
    dm = DataMigrator()
    dm.migrate(obj, interativo)


# MIGRATION_ADJUSTMENTS #####################################################

def adjust_acompanhamentomateria(new, old):
    new.confirmado = True


def adjust_documentoadministrativo(new, old):
    if old.num_protocolo:
        protocolo = Protocolo.objects.filter(
            numero=old.num_protocolo, ano=new.ano)
        if not protocolo:
            protocolo = Protocolo.objects.filter(
                numero=old.num_protocolo, ano=new.ano + 1)
            print('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! '
                  'DocumentoAdministrativo: {}, numero_protocolo: {}, '
                  'ano doc adm: {}'.format(
                      old.cod_documento, old.num_protocolo, new.ano))
        if not protocolo:
            raise ForeignKeyFaltando(
                'Protocolo {} faltando '
                '(referenciado no documento administrativo {}'.format(
                    old.num_protocolo, old.cod_documento))
        assert len(protocolo) == 1
        new.protocolo = protocolo[0]


def adjust_mandato(new, old):
    if old.dat_fim_mandato:
        new.data_fim_mandato = old.dat_fim_mandato
    if not new.data_fim_mandato:
        legislatura = Legislatura.objects.latest('data_fim')
        new.data_fim_mandato = legislatura.data_fim
        new.data_expedicao_diploma = legislatura.data_inicio
    if not new.data_inicio_mandato:
        new.data_inicio_mandato = new.legislatura.data_inicio
        new.data_fim_mandato = new.legislatura.data_fim


def adjust_ordemdia_antes_salvar(new, old):
    new.votacao_aberta = False

    if not old.tip_votacao:
        new.tipo_votacao = 1

    if old.num_ordem is None:
        new.numero_ordem = 999999999


def adjust_ordemdia_depois_salvar(new, old):
    if old.num_ordem is None and new.numero_ordem == 999999999:
        with reversion.create_revision():
            problema = 'OrdemDia de PK %s tinha seu valor de numero ordem'\
                ' nulo.' % old.pk
            descricao = 'O valor %s foi colocado no lugar.' % new.numero_ordem
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema,
                          descricao=descricao, eh_stub=False)
            reversion.set_comment('OrdemDia sem número da ordem.')


def adjust_parlamentar(new, old):
    if old.ind_unid_deliberativa:
        value = new.unidade_deliberativa
        # Field is defined as not null in legacy db,
        # but data includes null values
        #  => transform None to False
        if value is None:
            warn('nulo convertido para falso')
            new.unidade_deliberativa = False


def adjust_participacao(new, old):
    composicao = Composicao()
    composicao.comissao_id, composicao.periodo_id = [
        get_fk_related(Composicao._meta.get_field(name), value)
        for name, value in (('comissao', old.cod_comissao),
                            ('periodo', old.cod_periodo_comp))]
    # check if there is already an "equal" one in the db
    already_created = Composicao.objects.filter(
        comissao=composicao.comissao, periodo=composicao.periodo)
    if already_created:
        assert len(already_created) == 1  # we must never have made 2 copies
        [composicao] = already_created
    else:
        with reversion.create_revision():
            composicao.save()
            reversion.set_comment('Objeto criado pela migração')
    new.composicao = composicao


def adjust_proposicao_antes_salvar(new, old):
    if new.data_envio:
        new.ano = new.data_envio.year


def adjust_proposicao_depois_salvar(new, old):
    if not hasattr(old.dat_envio, 'year') or old.dat_envio.year == 1800:
        msg = "O valor do campo data_envio (DateField) da model Proposicao"\
            " era inválido"
        descricao = 'A data 1111-11-11 foi colocada no lugar'
        problema = 'O valor da data era nulo ou inválido'
        warn(msg + ' => ' + descricao)
        new.data_envio = date(1111, 11, 11)
        with reversion.create_revision():
            save_relation(obj=new, problema=problema,
                          descricao=descricao, eh_stub=False)
            reversion.set_comment('Ajuste de data pela migração')


def adjust_normarelacionada(new, old):
    tipo = TipoVinculoNormaJuridica.objects.filter(sigla=old.tip_vinculo)
    assert len(tipo) == 1
    new.tipo_vinculo = tipo[0]


def adjust_protocolo_antes_salvar(new, old):
    if old.num_protocolo is None:
        new.numero = old.cod_protocolo


def adjust_protocolo_depois_salvar(new, old):
    if old.num_protocolo is None:
        with reversion.create_revision():
            problema = 'Número do protocolo de PK %s é nulo' % new.pk
            descricao = 'Número do protocolo alterado para %s!' % new.numero
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema,
                          descricao=descricao, eh_stub=False)
            reversion.set_comment('Número de protocolo teve que ser alterado')


def adjust_registrovotacao_antes_salvar(new, old):
    ordem_dia = OrdemDia.objects.filter(
        pk=old.cod_ordem, materia=old.cod_materia)
    expediente_materia = ExpedienteMateria.objects.filter(
        pk=old.cod_ordem, materia=old.cod_materia)

    if ordem_dia and not expediente_materia:
        new.ordem = ordem_dia[0]
    if not ordem_dia and expediente_materia:
        new.expediente = expediente_materia[0]


def adjust_registrovotacao_depois_salvar(new, old):
    if not new.ordem and not new.expediente:
        with reversion.create_revision():
            problema = 'RegistroVotacao de PK %s não possui nenhuma OrdemDia'\
                ' ou ExpedienteMateria.' % old.pk
            descricao = 'RevistroVotacao deve ter no mínimo uma ordem do dia'\
                ' ou expediente vinculado.'
            warn(problema + ' => ' + descricao)
            save_relation(obj=new, problema=problema,
                          descricao=descricao, eh_stub=False)
            reversion.set_comment('RegistroVotacao sem ordem ou expediente')


def adjust_tipoafastamento(new, old):
    if old.ind_afastamento == 1:
        new.indicador = 'A'


def adjust_tipoproposicao(new, old):
    if old.ind_mat_ou_doc == 'M':
        tipo_materia = TipoMateriaLegislativa.objects.filter(
            pk=old.tip_mat_ou_doc)
        if tipo_materia:
            new.tipo_conteudo_related = tipo_materia[0]
        else:
            raise ForeignKeyFaltando
    elif old.ind_mat_ou_doc == 'D':
        tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc)
        if tipo_documento:
            new.tipo_conteudo_related = tipo_documento[0]
        else:
            raise ForeignKeyFaltando


def adjust_statustramitacao(new, old):
    if old.ind_fim_tramitacao:
        new.indicador = 'F'
    elif old.ind_retorno_tramitacao:
        new.indicador = 'R'
    else:
        new.indicador = ''


def adjust_statustramitacaoadm(new, old):
    adjust_statustramitacao(new, old)


def adjust_tramitacao(new, old):
    if old.sgl_turno == 'Ú':
        new.turno = 'U'


def adjust_tipo_autor(new, old):
    model_apontado = normalize(new.descricao.lower()).replace(' ', '')
    content_types = ContentType.objects.filter(
        model=model_apontado).exclude(app_label='legacy')
    assert len(content_types) <= 1
    new.content_type = content_types[0] if content_types else None


def adjust_normajuridica_antes_salvar(new, old):
    # Ajusta choice de esfera_federacao
    # O 'S' vem de 'Selecionar'. Na versão antiga do SAPL, quando uma opção do
    # combobox era selecionada, o sistema pegava a primeira letra da seleção,
    # sendo F para Federal, E para Estadual, M para Municipal e o S para
    # Selecionar, que era a primeira opção quando nada era selecionado.
    if old.tip_esfera_federacao == 'S':
        new.esfera_federacao = ''


def adjust_normajuridica_depois_salvar(new, old):
    # Ajusta relação M2M

    if not old.cod_assunto:  # it can be null or empty
        return

    # lista de pks separadas por vírgulas (ignorando strings vazias)
    lista_pks_assunto = [int(pk) for pk in old.cod_assunto.split(',') if pk]

    for pk_assunto in lista_pks_assunto:
        try:
            new.assuntos.add(AssuntoNorma.objects.get(pk=pk_assunto))
        except ObjectDoesNotExist:
            pass  # ignora assuntos inexistentes


def vincula_autor(new, old, model_relacionado, campo_relacionado, campo_nome):
    pk_rel = getattr(old, campo_relacionado)
    if pk_rel:
        try:
            new.autor_related = model_relacionado.objects.get(pk=pk_rel)
        except ObjectDoesNotExist:
            # ignoramos o autor órfão
            raise ForeignKeyFaltando('{} inexiste para autor'.format(
                model_relacionado._meta.verbose_name))
        else:
            new.nome = getattr(new.autor_related, campo_nome)
            return True


def adjust_autor(new, old):
    for args in [
            # essa ordem é importante
            (Parlamentar, 'cod_parlamentar', 'nome_parlamentar'),
            (Comissao, 'cod_comissao', 'nome'),
            (Partido, 'cod_partido', 'nome')]:
        if vincula_autor(new, old, *args):
            break

    if old.col_username:
        user_model = get_user_model()
        if not user_model.objects.filter(username=old.col_username).exists():
            # cria um novo ususaŕio para o autor
            user = user_model(username=old.col_username)
            # gera uma senha inutilizável, que precisará ser trocada
            user.set_password(None)
            with reversion.create_revision():
                user.save()
                reversion.set_comment(
                    'Usuário criado pela migração para o autor {}'.format(
                        old.cod_autor))
            grupo_autor = Group.objects.get(name="Autor")
            user.groups.add(grupo_autor)


def adjust_comissao(new, old):
    if not old.dat_extincao and not old.dat_fim_comissao:
        new.ativa = True
    elif old.dat_extincao and date.today() < new.data_extincao or \
            old.dat_fim_comissao and date.today() < new.data_fim_comissao:
        new.ativa = True
    else:
        new.ativa = False


AJUSTE_ANTES_SALVAR = {
    Autor: adjust_autor,
    TipoAutor: adjust_tipo_autor,
    AcompanhamentoMateria: adjust_acompanhamentomateria,
    Comissao: adjust_comissao,
    DocumentoAdministrativo: adjust_documentoadministrativo,
    Mandato: adjust_mandato,
    NormaJuridica: adjust_normajuridica_antes_salvar,
    NormaRelacionada: adjust_normarelacionada,
    OrdemDia: adjust_ordemdia_antes_salvar,
    Parlamentar: adjust_parlamentar,
    Participacao: adjust_participacao,
    Proposicao: adjust_proposicao_antes_salvar,
    Protocolo: adjust_protocolo_antes_salvar,
    RegistroVotacao: adjust_registrovotacao_antes_salvar,
    TipoAfastamento: adjust_tipoafastamento,
    TipoProposicao: adjust_tipoproposicao,
    StatusTramitacao: adjust_statustramitacao,
    StatusTramitacaoAdministrativo: adjust_statustramitacaoadm,
    Tramitacao: adjust_tramitacao,
}

AJUSTE_DEPOIS_SALVAR = {
    NormaJuridica: adjust_normajuridica_depois_salvar,
    OrdemDia: adjust_ordemdia_depois_salvar,
    Proposicao: adjust_proposicao_depois_salvar,
    Protocolo: adjust_protocolo_depois_salvar,
    RegistroVotacao: adjust_registrovotacao_depois_salvar,
}

# CHECKS ####################################################################


def get_ind_excluido(new):
    legacy_model = legacy_app.get_model(type(new).__name__)
    old = legacy_model.objects.get(**{legacy_model._meta.pk.name: new.id})
    return getattr(old, 'ind_excluido', False)


def check_app_no_ind_excluido(app):
    for model in app.models.values():
        assert not any(get_ind_excluido(new) for new in model.objects.all())
    print('OK!')
