diff --git a/Dockerfile b/Dockerfile index c192ccab4..693230025 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM alpine:3.5 ENV BUILD_PACKAGES postgresql-dev graphviz-dev graphviz build-base git pkgconfig \ python3-dev libxml2-dev jpeg-dev libressl-dev libffi-dev libxslt-dev nodejs py3-lxml \ -py3-magic postgresql-client poppler-utils vim +py3-magic postgresql-client poppler-utils antiword vim RUN apk --update add fontconfig ttf-dejavu && fc-cache -fv diff --git a/docker-compose.yml b/docker-compose.yml index 3afd6dc62..11e5caedf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ sapldb: ports: - "5432:5432" sapl: - image: interlegis/sapl:3.1.52 + image: interlegis/sapl:3.1.54 restart: always environment: ADMIN_PASSWORD: interlegis diff --git a/docs/instalacao31.rst b/docs/instalacao31.rst index f12b92d9c..12828b199 100644 --- a/docs/instalacao31.rst +++ b/docs/instalacao31.rst @@ -28,7 +28,7 @@ Instalar as seguintes dependências do sistema:: pkg-config postgresql postgresql-contrib pgadmin3 python-psycopg2 \ software-properties-common build-essential libxml2-dev libjpeg-dev \ libmysqlclient-dev libssl-dev libffi-dev libxslt1-dev python3-setuptools \ - python3-pip curl poppler-utils default-jre + python3-pip curl poppler-utils antiword default-jre sudo -i curl -sL https://deb.nodesource.com/setup_6.x | bash - @@ -187,9 +187,9 @@ Copie a chave que aparecerá, edite o arquivo .env e altere o valor do parâmetr * Subir o servidor do django:: ./manage.py runserver 0.0.0.0:8001 - + * Compilar os arquivos de estilização:: - + ./manage.py compilescss ./manage.py collectstatic @@ -211,7 +211,7 @@ Instruções para criação do super usuário e de usuários de testes * Os perfis fixos não aceitam customização via admin, porém outros grupos podem ser criados. O SAPL não interferirá no conjunto de permissões definidas em grupos customizados e se comportará diante de usuários segundo seus grupos e suas permissões. * Para criar os usuários de teste, deve-se seguir os seguintes passos:: - + ./manage.py shell_plus from sapl.rules.apps import cria_usuarios_padrao cria_usuarios_padrao() diff --git a/sapl/compilacao/apps.py b/sapl/compilacao/apps.py index 85c8ba349..6b96175b5 100644 --- a/sapl/compilacao/apps.py +++ b/sapl/compilacao/apps.py @@ -1,8 +1,102 @@ +import logging + from django import apps -from django.utils.translation import ugettext_lazy as _ +from django.conf import settings +from django.db import models, connection +from django.db.utils import DEFAULT_DB_ALIAS, IntegrityError +from django.utils.translation import ugettext_lazy as _, string_concat + + +from sapl.settings import BASE_DIR + +logger = logging.getLogger(BASE_DIR.name) class AppConfig(apps.AppConfig): name = 'sapl.compilacao' label = 'compilacao' verbose_name = _('Compilação') + + @staticmethod + def import_pattern(): + + from sapl.compilacao.models import TipoTextoArticulado + from sapl.compilacao.utils import get_integrations_view_names + + from django.contrib.contenttypes.models import ContentType + from unipath import Path + + compilacao_app = Path(__file__).ancestor(1) + # print(compilacao_app) + with open(compilacao_app + '/compilacao_data_tables.sql', 'r') as f: + lines = f.readlines() + lines = [line.rstrip('\n') for line in lines] + + with connection.cursor() as cursor: + for line in lines: + line = line.strip() + if not line or line.startswith('#'): + continue + + try: + cursor.execute(line) + except IntegrityError as e: + if not settings.DEBUG: + logger.error( + string_concat( + _('Ocorreu erro na importação: '), + line, + str(e))) + except Exception as ee: + print(ee) + + integrations_view_names = get_integrations_view_names() + + def cria_sigla(verbose_name): + verbose_name = verbose_name.upper().split() + if len(verbose_name) == 1: + verbose_name = verbose_name[0] + sigla = '' + for letra in verbose_name: + if letra in 'BCDFGHJKLMNPQRSTVWXYZ': + sigla += letra + else: + sigla = ''.join([palavra[0] for palavra in verbose_name]) + return sigla[:3] + + for view in integrations_view_names: + try: + tipo = TipoTextoArticulado() + tipo.sigla = cria_sigla( + view.model._meta.verbose_name + if view.model._meta.verbose_name + else view.model._meta.model_name) + tipo.descricao = view.model._meta.verbose_name + tipo.content_type = ContentType.objects.get_by_natural_key( + view.model._meta.app_label, view.model._meta.model_name) + tipo.save() + except IntegrityError as e: + if not settings.DEBUG: + logger.error( + string_concat( + _('Ocorreu erro na criação tipo de ta: '), + str(e))) + + +def init_compilacao_base(app_config, verbosity=2, interactive=True, + using=DEFAULT_DB_ALIAS, **kwargs): + + if app_config != AppConfig and not isinstance(app_config, AppConfig): + return + from sapl.compilacao.models import TipoDispositivo + if not TipoDispositivo.objects.exists(): + + print('') + print(string_concat('\033[93m\033[1m', + _('Iniciando Textos Articulados...'), + '\033[0m')) + AppConfig.import_pattern() + + +models.signals.post_migrate.connect( + receiver=init_compilacao_base) diff --git a/sapl/compilacao/tests/test_tipo_texto_articulado_form.py b/sapl/compilacao/tests/test_tipo_texto_articulado_form.py index 84593650f..b682307c9 100644 --- a/sapl/compilacao/tests/test_tipo_texto_articulado_form.py +++ b/sapl/compilacao/tests/test_tipo_texto_articulado_form.py @@ -1,6 +1,7 @@ -import pytest from django.utils.translation import ugettext as _ from model_mommy import mommy +import pytest + from sapl.compilacao import forms from sapl.compilacao.models import PerfilEstruturalTextoArticulado, TipoNota from sapl.compilacao.views import choice_models_in_extenal_views @@ -21,25 +22,6 @@ def test_valida_campos_obrigatorios_tipo_texto_articulado_form(): assert len(errors) == 4 -_content_types = choice_models_in_extenal_views() - - -@pytest.mark.parametrize('content_type', _content_types) -@pytest.mark.django_db(transaction=False) -def test_tipo_texto_articulado_form_valid(content_type): - perfil = mommy.make(PerfilEstruturalTextoArticulado) - - form = forms.TipoTaForm(data={'sigla': 'si', - 'descricao': 'teste', - 'content_type': content_type[0], - 'participacao_social': True, - 'publicacao_func': True, - 'perfis': [perfil.pk, ] - }) - - assert form.is_valid(), form.errors - - def test_valida_campos_obrigatorios_nota_form(): form = forms.NotaForm(data={}) diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index 523de19a2..b24bfd677 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -27,6 +27,7 @@ from django.views.generic.edit import (CreateView, DeleteView, FormView, UpdateView) from django.views.generic.list import ListView +from sapl.compilacao.apps import AppConfig from sapl.compilacao.forms import (DispositivoDefinidorVigenciaForm, DispositivoEdicaoAlteracaoForm, DispositivoEdicaoBasicaForm, @@ -107,7 +108,7 @@ class IntegracaoTaView(TemplateView): try: if settings.DEBUG or not TipoDispositivo.objects.exists(): - self.import_pattern() + AppConfig.import_pattern() if hasattr(self, 'map_funcs'): tipo_ta = TipoTextoArticulado.objects.get( @@ -195,66 +196,6 @@ class IntegracaoTaView(TemplateView): return redirect(to=reverse_lazy('sapl.compilacao:ta_text', kwargs={'ta_id': ta.pk})) - def import_pattern(self): - - from unipath import Path - - compilacao_app = Path(__file__).ancestor(1) - # print(compilacao_app) - with open(compilacao_app + '/compilacao_data_tables.sql', 'r') as f: - lines = f.readlines() - lines = [line.rstrip('\n') for line in lines] - - with connection.cursor() as cursor: - for line in lines: - line = line.strip() - if not line or line.startswith('#'): - continue - - try: - cursor.execute(line) - except IntegrityError as e: - if not settings.DEBUG: - logger.error( - string_concat( - _('Ocorreu erro na importação: '), - line, - str(e))) - except Exception as ee: - print(ee) - - integrations_view_names = get_integrations_view_names() - - def cria_sigla(verbose_name): - verbose_name = verbose_name.upper().split() - if len(verbose_name) == 1: - verbose_name = verbose_name[0] - sigla = '' - for letra in verbose_name: - if letra in 'BCDFGHJKLMNPQRSTVWXYZ': - sigla += letra - else: - sigla = ''.join([palavra[0] for palavra in verbose_name]) - return sigla[:3] - - for view in integrations_view_names: - try: - tipo = TipoTextoArticulado() - tipo.sigla = cria_sigla( - view.model._meta.verbose_name - if view.model._meta.verbose_name - else view.model._meta.model_name) - tipo.descricao = view.model._meta.verbose_name - tipo.content_type = ContentType.objects.get_by_natural_key( - view.model._meta.app_label, view.model._meta.model_name) - tipo.save() - except IntegrityError as e: - if not settings.DEBUG: - logger.error( - string_concat( - _('Ocorreu erro na criação tipo de ta: '), - str(e))) - class Meta: abstract = True diff --git a/sapl/hashers.py b/sapl/hashers.py index a514f8f19..e80642def 100644 --- a/sapl/hashers.py +++ b/sapl/hashers.py @@ -44,6 +44,8 @@ ZOPE_SHA1_PREFIX = '{SSHA}' def zope_encoded_password_to_django(encoded): + "Migra um hash de senha do zope para uso com o ZopeSHA1PasswordHasher" + if encoded.startswith(ZOPE_SHA1_PREFIX): data = encoded[len(ZOPE_SHA1_PREFIX):] salt = get_salt_from_zope_sha1(data) diff --git a/sapl/legacy/management/commands/migracao_25_31.py b/sapl/legacy/management/commands/migracao_25_31.py index 9e7f0a32a..1298feeb3 100644 --- a/sapl/legacy/management/commands/migracao_25_31.py +++ b/sapl/legacy/management/commands/migracao_25_31.py @@ -1,6 +1,7 @@ from django.core import management from django.core.management.base import BaseCommand -from sapl.legacy import migration + +from sapl.legacy.migracao import migrar class Command(BaseCommand): @@ -18,4 +19,4 @@ class Command(BaseCommand): def handle(self, *args, **options): management.call_command('migrate') - migration.migrate(interativo=not options['force']) + migrar(interativo=not options['force']) diff --git a/sapl/legacy/migration.py b/sapl/legacy/migracao.py similarity index 81% rename from sapl/legacy/migration.py rename to sapl/legacy/migracao.py index e5d98147b..b228acbc1 100644 --- a/sapl/legacy/migration.py +++ b/sapl/legacy/migracao.py @@ -121,7 +121,9 @@ def warn(msg): class ForeignKeyFaltando(ObjectDoesNotExist): 'Uma FK aponta para um registro inexistente' - pass + + def __init__(self, msg=''): + self.msg = msg @lru_cache() @@ -350,9 +352,94 @@ def anula_tipos_origem_externa_invalidos(): where tip_origem_externa not in {};''', tipos_validos) +def get_ids_registros_votacao_para(tabela): + sql = ''' + select r.cod_votacao from {} o + inner join registro_votacao r on + o.cod_ordem = r.cod_ordem and o.cod_materia = r.cod_materia + where o.ind_excluido != 1 and r.ind_excluido != 1 + order by o.cod_sessao_plen, num_ordem + '''.format(tabela) + return set(primeira_coluna(exec_legado(sql))) + + +def checa_registros_votacao_ambiguos_e_remove_nao_usados(): + """Interrompe a migração caso restem registros de votação + que apontam para uma ordem_dia e um expediente_materia ao mesmo tempo. + + Remove do legado registros de votação que não têm + nem ordem_dia nem expediente_materia associados.""" + + ordem, expediente = [ + get_ids_registros_votacao_para(tabela) + for tabela in ('ordem_dia', 'expediente_materia')] + + # interrompe migração se houver registros ambíguos + ambiguos = ordem.intersection(expediente) + assert not ambiguos, '''Existe(m) RegistroVotacao ambíguo(s): {} + Corrija os dados originais antes de migrar!'''.format( + ambiguos) + + # exclui registros não usados (zumbis) + todos = set(primeira_coluna(exec_legado( + 'select cod_votacao from registro_votacao'))) + nao_usados = todos - ordem.union(expediente) + exec_legado_em_subconjunto(''' + update registro_votacao set ind_excluido = 1 + where cod_votacao in {}''', nao_usados) + + +PROPAGACOES_DE_EXCLUSAO = [ + # sessao_legislativa + ('composicao_mesa', 'sessao_legislativa', 'cod_sessao_leg'), + + # parlamentar + ('dependente', 'parlamentar', 'cod_parlamentar'), + ('filiacao', 'parlamentar', 'cod_parlamentar'), + ('mandato', 'parlamentar', 'cod_parlamentar'), + + # comissao + ('composicao_comissao', 'comissao', 'cod_comissao'), + + # sessao + ('ordem_dia', 'sessao_plenaria', 'cod_sessao_plen'), + ('expediente_materia', 'sessao_plenaria', 'cod_sessao_plen'), + ('expediente_sessao_plenaria', 'sessao_plenaria', 'cod_sessao_plen'), + ('registro_votacao_parlamentar', 'registro_votacao', 'cod_votacao'), + # as consultas no código do sapl 2.5 + # votacao_ordem_dia_obter_zsql e votacao_expediente_materia_obter_zsql + # indicam que os registros de votação de matérias excluídas não são + # exibidos... + ('registro_votacao', 'materia_legislativa', 'cod_materia'), + # as exclusões de registro_votacao sem referência + # nem a ordem_dia nem a expediente_materia são feitas num método à parte + + # materia + ('tramitacao', 'materia_legislativa', 'cod_materia'), + ('autoria', 'materia_legislativa', 'cod_materia'), + ('anexada', 'materia_legislativa', 'cod_materia_principal'), + ('anexada', 'materia_legislativa', 'cod_materia_anexada'), + ('documento_acessorio', 'materia_legislativa', 'cod_materia'), + + # documento administrativo + ('tramitacao_administrativo', 'documento_administrativo', 'cod_documento'), +] + + +def propaga_exclusoes(): + for tabela_filha, tabela_pai, fk in PROPAGACOES_DE_EXCLUSAO: + [pk_pai] = get_pk_legado(tabela_pai) + exec_legado(''' + update {} set ind_excluido = 1 where {} not in ( + select {} from {} where ind_excluido != 1) + '''.format(tabela_filha, fk, pk_pai, tabela_pai)) + + def uniformiza_banco(): - # desliga todas as checagens do mysql - exec_legado('SET SESSION sql_mode = "";') + exec_legado('SET SESSION sql_mode = "";') # desliga checagens do mysql + + checa_registros_votacao_ambiguos_e_remove_nao_usados() + propaga_exclusoes() garante_coluna_no_legado('proposicao', 'num_proposicao int(11) NULL') @@ -437,10 +524,10 @@ relatoria | tip_fim_relatoria = NULL | tip_fim_relatoria = 0 anula_tipos_origem_externa_invalidos() -def iter_sql_records(sql, db): +def iter_sql_records(sql): class Record: pass - cursor = exec_sql(sql, db) + cursor = exec_legado(sql) fieldnames = [name[0] for name in cursor.description] for row in cursor.fetchall(): record = Record() @@ -534,24 +621,6 @@ def excluir_registrovotacao_duplicados(): 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 @@ -563,6 +632,12 @@ def reinicia_sequence(model, id): sequence_name, id)) +def get_pk_legado(tabela): + res = exec_legado( + 'show index from {} WHERE Key_name = "PRIMARY"'.format(tabela)) + return [r[4] for r in res] + + class DataMigrator: def __init__(self): @@ -619,7 +694,7 @@ class DataMigrator: setattr(new, field.name, value) - def migrate(self, obj=appconfs, interativo=True): + def migrar(self, obj=appconfs, interativo=True): # warning: model/app migration order is of utmost importance uniformiza_banco() @@ -648,8 +723,8 @@ class DataMigrator: info('Começando migração: %s...' % obj) self._do_migrate(obj) - info('Excluindo possíveis duplicações em RegistroVotacao...') - excluir_registrovotacao_duplicados() + # info('Excluindo possíveis duplicações em RegistroVotacao...') + # excluir_registrovotacao_duplicados() # recria tipos de autor padrão que não foram criados pela migração cria_models_tipo_autor() @@ -677,45 +752,29 @@ class DataMigrator: 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 + nome_model = self.model_renames.get(model, model.__name__) + model_legado = legacy_app.get_model(nome_model) + tabela_legado = model_legado._meta.db_table + campos_pk = get_pk_legado(tabela_legado) - # 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 + if len(campos_pk) == 1: + # a pk no legado tem um único campo + nome_pk = model_legado._meta.pk.name + old_records = model_legado.objects.all().order_by(nome_pk) - 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') + def get_id_do_legado(old): + return getattr(old, nome_pk) 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) + # a pk no legado tem mais de um campo + old_records = iter_sql_records('select * from ' + tabela_legado) + get_id_do_legado = None ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model) ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model) # convert old records to new ones with transaction.atomic(): + sql_delete_legado = '' for old in old_records: if getattr(old, 'ind_excluido', False): # não migramos registros marcados como excluídos @@ -725,25 +784,46 @@ class DataMigrator: self.populate_renamed_fields(new, old) if ajuste_antes_salvar: ajuste_antes_salvar(new, old) - except ForeignKeyFaltando: + except ForeignKeyFaltando as e: # tentamos preencher uma FK e o ojeto relacionado # não existe # então este é um objeo órfão: simplesmente ignoramos + warn(e.msg) continue else: - save(new, old) + if get_id_do_legado: + new.id = get_id_do_legado(old) + # validação do model + new.clean() + # salva novo registro + with reversion.create_revision(): + new.save() + reversion.set_comment('Objeto criado pela migração') + + # acumula deleção do registro no legado + sql_delete_legado += 'delete from {} where {};\n'.format( + tabela_legado, + ' and '.join( + '{} = "{}"'.format(campo, + getattr(old, campo)) + for campo in campos_pk)) + if ajuste_depois_salvar: ajuste_depois_salvar(new, old) - # reinicia sequence - if deve_ajustar_sequence_ao_final: + # se configuramos ids explicitamente devemos reiniciar a sequence + if get_id_do_legado: last_pk = get_last_pk(model) reinicia_sequence(model, last_pk + 1) + # apaga registros migrados do legado + if sql_delete_legado: + exec_legado(sql_delete_legado) + -def migrate(obj=appconfs, interativo=True): +def migrar(obj=appconfs, interativo=True): dm = DataMigrator() - dm.migrate(obj, interativo) + dm.migrar(obj, interativo) # MIGRATION_ADJUSTMENTS ##################################################### @@ -752,24 +832,51 @@ def adjust_acompanhamentomateria(new, old): new.confirmado = True +NOTA_DOCADM = ''' +## NOTA DE MIGRAÇÃO DE DADOS DO SAPL 2.5 ## +O número de protocolo original deste documento era [{num_protocolo}], ano {ano_original}. +'''.strip() # noqa + + def adjust_documentoadministrativo(new, old): if old.num_protocolo: + nota = None + ano_original = new.ano 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] + # tentamos encontrar o protocolo no ano seguinte + ano_novo = ano_original + 1 + protocolo = Protocolo.objects.filter(numero=old.num_protocolo, + ano=ano_novo) + if protocolo: + nota = NOTA_DOCADM + ''' +O protocolo vinculado é o de mesmo número, porém do ano seguinte ({ano_novo}), +pois não existe protocolo no sistema com este número no ano {ano_original}. +''' + nota = nota.strip().format(num_protocolo=old.num_protocolo, + ano_original=ano_original, + ano_novo=ano_novo) + warn('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! ' + 'DocumentoAdministrativo: {}, numero_protocolo: {}, ' + 'ano doc adm: {}'.format( + old.cod_documento, old.num_protocolo, ano_original)) + else: + nota = NOTA_DOCADM + ''' +Não existe no sistema nenhum protocolo com estes dados +e portanto nenhum protocolo foi vinculado a este documento.''' + nota = nota.format( + num_protocolo=old.num_protocolo, + ano_original=ano_original) + warn('Protocolo {} faltando ' + '(referenciado no documento administrativo {})'.format( + old.num_protocolo, old.cod_documento)) + if protocolo: + assert len(protocolo) == 1, 'mais de um protocolo encontrado' + [new.protocolo] = protocolo + # adiciona nota ao final da observação + if nota: + new.observacao += ('\n\n' if new.observacao else '') + nota def adjust_mandato(new, old): @@ -797,7 +904,7 @@ def adjust_ordemdia_antes_salvar(new, old): 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'\ + 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) @@ -882,19 +989,6 @@ def adjust_registrovotacao_antes_salvar(new, old): 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' @@ -975,8 +1069,9 @@ def vincula_autor(new, old, model_relacionado, campo_relacionado, campo_nome): 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)) + raise ForeignKeyFaltando( + '{} [pk={}] inexistente para autor'.format( + model_relacionado._meta.verbose_name, pk_rel)) else: new.nome = getattr(new.autor_related, campo_nome) return True @@ -1010,8 +1105,8 @@ def adjust_autor(new, old): 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: + 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 @@ -1043,15 +1138,14 @@ AJUSTE_DEPOIS_SALVAR = { NormaJuridica: adjust_normajuridica_depois_salvar, OrdemDia: adjust_ordemdia_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}) + model_legado = legacy_app.get_model(type(new).__name__) + old = model_legado.objects.get(**{model_legado._meta.pk.name: new.id}) return getattr(old, 'ind_excluido', False) diff --git a/sapl/legacy/migracao_documentos.py b/sapl/legacy/migracao_documentos.py index 1f5ca2cd0..9de759348 100644 --- a/sapl/legacy/migracao_documentos.py +++ b/sapl/legacy/migracao_documentos.py @@ -6,7 +6,7 @@ from glob import glob import yaml from sapl.base.models import CasaLegislativa -from sapl.legacy.migration import exec_legado, warn +from sapl.legacy.migracao import exec_legado, warn from sapl.materia.models import (DocumentoAcessorio, MateriaLegislativa, Proposicao) from sapl.norma.models import NormaJuridica diff --git a/sapl/legacy/migracao_usuarios.py b/sapl/legacy/migracao_usuarios.py index bd7d413e5..39d6df4e9 100644 --- a/sapl/legacy/migracao_usuarios.py +++ b/sapl/legacy/migracao_usuarios.py @@ -1,6 +1,7 @@ import yaml from django.contrib.auth.models import Group, User +from sapl.hashers import zope_encoded_password_to_django from sapl.settings import MEDIA_ROOT PERFIL_LEGADO_PARA_NOVO = {legado: Group.objects.get(name=novo) @@ -83,6 +84,7 @@ def migra_usuarios(): for nome, senha, perfis in usuarios: usuario = User.objects.get_or_create(username=nome)[0] + usuario.password = zope_encoded_password_to_django(senha) for perfil in perfis: if perfil in ADMINISTRADORES: # Manager @@ -92,3 +94,4 @@ def migra_usuarios(): usuario.groups.add(PERFIL_LEGADO_PARA_NOVO[perfil]) # apaga arquivo (importante pois contém senhas) ARQUIVO_USUARIOS.remove() + print('Usuários migrados com sucesso.') diff --git a/sapl/legacy/scripts/exporta_zope/exporta_zope.py b/sapl/legacy/scripts/exporta_zope/exporta_zope.py index e31aa4d81..c097c1a1a 100755 --- a/sapl/legacy/scripts/exporta_zope/exporta_zope.py +++ b/sapl/legacy/scripts/exporta_zope/exporta_zope.py @@ -22,6 +22,7 @@ EXTENSOES = { 'application/msword': '.doc', 'application/pdf': '.pdf', 'application/vnd.oasis.opendocument.text': '.odt', + 'application/vnd.ms-excel': '.xls', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', # noqa 'application/xml': '.xml', 'text/xml': '.xml', @@ -219,6 +220,7 @@ DUMP_FUNCTIONS = { 'Folder': partial(dump_folder, enum=enumerate_folder), 'BTreeFolder2': partial(dump_folder, enum=enumerate_btree), 'SDE-Document': partial(dump_sde, tipo='sde.document'), + 'StrDoc': partial(dump_sde, tipo='sde.document'), 'SDE-Template': partial(dump_sde, tipo='sde.template'), # explicitamente ignorados diff --git a/sapl/legacy/scripts/scrap_original_forms.py b/sapl/legacy/scripts/scrap_original_forms.py index fb6927cdc..a3325b32e 100644 --- a/sapl/legacy/scripts/scrap_original_forms.py +++ b/sapl/legacy/scripts/scrap_original_forms.py @@ -10,7 +10,7 @@ from bs4.element import NavigableString, Tag from django.apps.config import AppConfig from sapl.crispy_layout_mixin import heads_and_tails -from sapl.legacy.migration import appconfs, get_renames +from sapl.legacy.migracao import appconfs, get_renames from sapl.legacy.scripts.utils import getsourcelines from sapl.utils import listify diff --git a/sapl/legacy/scripts/study.py b/sapl/legacy/scripts/study.py index a28428c7d..1078af38e 100644 --- a/sapl/legacy/scripts/study.py +++ b/sapl/legacy/scripts/study.py @@ -1,5 +1,5 @@ from django.apps import apps -from sapl.legacy.migration import legacy_app +from sapl.legacy.migracao import legacy_app for model in apps.get_app_config('legacy').get_models(): if 'ind_excluido' in [f.name for f in model._meta.fields]: diff --git a/sapl/legacy/scripts/utils.py b/sapl/legacy/scripts/utils.py index 21f74ce52..c426f1b3b 100644 --- a/sapl/legacy/scripts/utils.py +++ b/sapl/legacy/scripts/utils.py @@ -1,7 +1,7 @@ import inspect from sapl.base.models import Autor -from sapl.legacy.migration import appconfs +from sapl.legacy.migracao import appconfs def getsourcelines(model): diff --git a/sapl/legacy/test_migration.py b/sapl/legacy/test_migration.py index efaa2e6d6..53034987d 100644 --- a/sapl/legacy/test_migration.py +++ b/sapl/legacy/test_migration.py @@ -1,6 +1,6 @@ from random import shuffle -from .migration import (_formatar_lista_para_sql, get_autorias_sem_repeticoes, +from .migracao import (_formatar_lista_para_sql, get_autorias_sem_repeticoes, get_reapontamento_de_autores_repetidos) diff --git a/sapl/legacy/test_renames.py b/sapl/legacy/test_renames.py index 27987aeeb..f275efa2f 100644 --- a/sapl/legacy/test_renames.py +++ b/sapl/legacy/test_renames.py @@ -3,7 +3,7 @@ import sapl.materia import sapl.norma import sapl.sessao -from .migration import appconfs, get_renames, legacy_app +from .migracao import appconfs, get_renames, legacy_app RENAMING_IGNORED_MODELS = [ sapl.comissoes.models.Composicao, diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index df60020ea..6ef99995b 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1,6 +1,5 @@ import os - import django_filters import sapl from crispy_forms.bootstrap import Alert, FormActions, InlineRadios @@ -23,7 +22,7 @@ from django.utils.encoding import force_text from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ -from sapl.base.models import Autor, TipoAutor +from sapl.base.models import Autor, TipoAutor, AppConfig from sapl.comissoes.models import Comissao from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC, STATUS_TA_PRIVATE) @@ -1209,6 +1208,20 @@ class ProposicaoForm(forms.ModelForm): "Arquivo muito grande. ( > {0}MB )".format(max_size)) return texto_original + def gerar_hash(self, inst, receber_recibo): + + inst.save() + if receber_recibo == True: + inst.hash_code = '' + else: + if inst.texto_original: + inst.hash_code = gerar_hash_arquivo( + inst.texto_original.path, str(inst.pk)) + elif inst.texto_articulado.exists(): + ta = inst.texto_articulado.first() + # FIXME hash para textos articulados + inst.hash_code = 'P' + ta.hash() + '/' + str(inst.pk) + def clean(self): super(ProposicaoForm, self).clean() @@ -1235,6 +1248,7 @@ class ProposicaoForm(forms.ModelForm): def save(self, commit=True): cd = self.cleaned_data inst = self.instance + receber_recibo = AppConfig.objects.last().receber_recibo_proposicao if inst.pk: if 'tipo_texto' in cd: @@ -1250,6 +1264,8 @@ class ProposicaoForm(forms.ModelForm): inst.texto_original: inst.texto_original.delete() + self.gerar_hash(inst, receber_recibo) + return super().save(commit) inst.ano = timezone.now().year @@ -1260,13 +1276,7 @@ class ProposicaoForm(forms.ModelForm): inst.numero_proposicao = ( numero__max + 1) if numero__max else 1 - inst.save() - if cd['receber_recibo'] == 'True': - inst.hash_code = '' - else: - _hash = gerar_hash_arquivo(inst.texto_original.path, str(inst.pk)) - - inst.hash_code = _hash + self.gerar_hash(inst, receber_recibo) inst.save() diff --git a/sapl/parlamentares/migrations/0021_clear_thumbnails_cache.py b/sapl/parlamentares/migrations/0021_clear_thumbnails_cache.py new file mode 100644 index 000000000..b3581816f --- /dev/null +++ b/sapl/parlamentares/migrations/0021_clear_thumbnails_cache.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +from django.db import migrations +from sapl.utils import clear_thumbnails_cache + + +def clear_thumbnails_cache_migrate(apps, schema_editor): + Parlamentar = apps.get_model("parlamentares", "Parlamentar") + parlamentares = Parlamentar.objects.all() + clear_thumbnails_cache(parlamentares, 'fotografia') + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0020_fix_inicio_mandato'), + ] + + operations = [ + migrations.RunPython(clear_thumbnails_cache_migrate), + ] diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index db7c50d9b..f4f25cc14 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -1,7 +1,11 @@ +from operator import xor + import reversion +from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils import Choices + from sapl.base.models import Autor from sapl.materia.models import MateriaLegislativa from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar, @@ -429,6 +433,16 @@ class RegistroVotacao(models.Model): 'votacao': self.tipo_resultado_votacao, 'materia': self.materia} + def clean(self): + """Exatamente um dos campos ordem ou expediente deve estar preenchido. + """ + # TODO remover esse método quando OrdemDia e ExpedienteMateria + # forem reestruturados e os campos ordem e expediente forem unificados + if not xor(bool(self.ordem), bool(self.expediente)): + raise ValidationError( + 'RegistroVotacao deve ter exatamente um dos campos ' + 'ordem ou expediente deve estar preenchido') + @reversion.register() class VotoParlamentar(models.Model): # RegistroVotacaoParlamentar diff --git a/sapl/sessao/tests/test_sessao.py b/sapl/sessao/tests/test_sessao.py index 37a17d1ed..d83c56e86 100644 --- a/sapl/sessao/tests/test_sessao.py +++ b/sapl/sessao/tests/test_sessao.py @@ -1,11 +1,13 @@ import pytest +from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from model_mommy import mommy + from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.parlamentares.models import Legislatura, Partido, SessaoLegislativa from sapl.sessao import forms -from sapl.sessao.models import (ExpedienteMateria, SessaoPlenaria, - TipoSessaoPlenaria) +from sapl.sessao.models import (ExpedienteMateria, OrdemDia, RegistroVotacao, + SessaoPlenaria, TipoSessaoPlenaria) def test_valida_campos_obrigatorios_sessao_plenaria_form(): @@ -138,3 +140,25 @@ def test_expediente_materia_form_valido(): }, instance=instance) assert form.is_valid() + + +@pytest.mark.django_db(transaction=False) +def test_registro_votacao_tem_ordem_xor_expediente(): + + def registro_votacao_com(ordem, expediente): + return mommy.make(RegistroVotacao, ordem=ordem, expediente=expediente) + + ordem = mommy.make(OrdemDia) + expediente = mommy.make(ExpedienteMateria) + + # a validação funciona com exatamente um dos campos preenchido + registro_votacao_com(ordem, None).full_clean() + registro_votacao_com(None, expediente).full_clean() + + # a validação NÃO funciona quando nenhum deles é preenchido + with pytest.raises(ValidationError): + registro_votacao_com(None, None).full_clean() + + # a validação NÃO funciona quando ambos são preenchidos + with pytest.raises(ValidationError): + registro_votacao_com(ordem, expediente).full_clean() diff --git a/sapl/settings.py b/sapl/settings.py index 31905aa07..2e6b7c7ff 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -188,6 +188,9 @@ THUMBNAIL_PROCESSORS = ( 'image_cropping.thumbnail_processors.crop_corners', ) + thumbnail_settings.THUMBNAIL_PROCESSORS +THUMBNAIL_SOURCE_GENERATORS = ( + 'sapl.utils.pil_image', +) # troque no caso de reimplementação da classe User conforme # https://docs.djangoproject.com/en/1.9/topics/auth/customizing/#substituting-a-custom-user-model diff --git a/sapl/templates/compilacao/textoarticulado_menu_config.html b/sapl/templates/compilacao/textoarticulado_menu_config.html index 976f18148..0116ba667 100644 --- a/sapl/templates/compilacao/textoarticulado_menu_config.html +++ b/sapl/templates/compilacao/textoarticulado_menu_config.html @@ -1,5 +1,7 @@ {% load i18n %} {% load common_tags %} + +{% if user.is_superuser %} @@ -13,6 +15,7 @@ {% if user.is_superuser %}
  • {%model_verbose_name_plural 'sapl.compilacao.models.TipoDispositivo'%}
  • -
  • TODO: Perfil Estrutural de Textos Articulados
  • +
  • Relacionamento entre Dispositivos
  • {% endif %} +{% endif %} diff --git a/sapl/templates/sistema.html b/sapl/templates/sistema.html index cfc7ed82d..643c38a9b 100644 --- a/sapl/templates/sistema.html +++ b/sapl/templates/sistema.html @@ -74,6 +74,20 @@
    Assunto de Norma Jurídica
    Tipo de Vínculo
    + +
    +

    Módulo Textos Articulados

    +
    +
    Tipos de Textos Articulados
    +
    Tipos de Publicação
    +
    Veículos de Publicação
    +
    Tipos de Notas
    +
    Tipos de Vides
    +
    Tipos de Dispositivos
    +
    Relacionamento entre Dispositivos
    +
    + +

    Módulo Sessão Plenária

    diff --git a/sapl/utils.py b/sapl/utils.py index 01f0f5f2b..488b2d8d6 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -1,13 +1,11 @@ +from functools import wraps +from operator import itemgetter +from unicodedata import normalize as unicodedata_normalize import hashlib import logging import os import re -from functools import wraps -from operator import itemgetter -from unicodedata import normalize as unicodedata_normalize -import django_filters -import magic from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button from django import forms @@ -21,14 +19,41 @@ from django.db.models import Q from django.utils import six, timezone from django.utils.translation import ugettext_lazy as _ from django_filters.filterset import STRICTNESS +from easy_thumbnails import source_generators from floppyforms import ClearableFileInput from reversion.admin import VersionAdmin +from unipath.path import Path +import django_filters +import magic + from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row from sapl.settings import BASE_DIR + sapl_logger = logging.getLogger(BASE_DIR.name) +def pil_image(source, exif_orientation=False, **options): + return source_generators.pil_image(source, exif_orientation, **options) + + +def clear_thumbnails_cache(queryset, field): + + for r in queryset: + assert hasattr(r, field), _( + 'Objeto da listagem não possui o campo informado') + + if not getattr(r, field): + continue + + path = Path(getattr(r, field).path) + cache_files = path.parent.walk() + + for cf in cache_files: + if cf != path: + cf.remove() + + def normalize(txt): return unicodedata_normalize( 'NFKD', txt).encode('ASCII', 'ignore').decode('ASCII') diff --git a/setup.py b/setup.py index 1feba9cef..c98fb482e 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ install_requires = [ ] setup( name='interlegis-sapl', - version='3.1.52', + version='3.1.54', packages=find_packages(), include_package_data=True, license='GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007',