Browse Source

Merge branch 'master' of https://github.com/interlegis/sapl into 1664-fix-numeracao-materia-legislativa

pull/1726/head
tapumar@gmail.com 8 years ago
parent
commit
f3d049f43a
  1. 2
      Dockerfile
  2. 2
      docker-compose.yml
  3. 8
      docs/instalacao31.rst
  4. 96
      sapl/compilacao/apps.py
  5. 22
      sapl/compilacao/tests/test_tipo_texto_articulado_form.py
  6. 63
      sapl/compilacao/views.py
  7. 2
      sapl/hashers.py
  8. 5
      sapl/legacy/management/commands/migracao_25_31.py
  9. 286
      sapl/legacy/migracao.py
  10. 2
      sapl/legacy/migracao_documentos.py
  11. 3
      sapl/legacy/migracao_usuarios.py
  12. 2
      sapl/legacy/scripts/exporta_zope/exporta_zope.py
  13. 2
      sapl/legacy/scripts/scrap_original_forms.py
  14. 2
      sapl/legacy/scripts/study.py
  15. 2
      sapl/legacy/scripts/utils.py
  16. 2
      sapl/legacy/test_migration.py
  17. 2
      sapl/legacy/test_renames.py
  18. 28
      sapl/materia/forms.py
  19. 23
      sapl/parlamentares/migrations/0021_clear_thumbnails_cache.py
  20. 14
      sapl/sessao/models.py
  21. 28
      sapl/sessao/tests/test_sessao.py
  22. 3
      sapl/settings.py
  23. 5
      sapl/templates/compilacao/textoarticulado_menu_config.html
  24. 14
      sapl/templates/sistema.html
  25. 35
      sapl/utils.py
  26. 2
      setup.py

2
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

2
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

8
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()

96
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)

22
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={})

63
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

2
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)

5
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'])

286
sapl/legacy/migration.py → 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)

2
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

3
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.')

2
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

2
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

2
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]:

2
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):

2
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)

2
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,

28
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()

23
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),
]

14
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

28
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()

3
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

5
sapl/templates/compilacao/textoarticulado_menu_config.html

@ -1,5 +1,7 @@
{% load i18n %}
{% load common_tags %}
{% if user.is_superuser %}
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-cog fa-1x fa-fw"></i>
</button>
@ -13,6 +15,7 @@
{% if user.is_superuser %}
<li><a href="{% url 'sapl.compilacao:tipodispositivo_list' %}">{%model_verbose_name_plural 'sapl.compilacao.models.TipoDispositivo'%}</a></li>
<li><a href="#">TODO: Perfil Estrutural de Textos Articulados</a></li>
<li><a href="/admin/compilacao/tipodispositivorelationship/">Relacionamento entre Dispositivos</a></li>
{% endif %}
</ul>
{% endif %}

14
sapl/templates/sistema.html

@ -74,6 +74,20 @@
<div class="col-md-6"><a href="{% url 'sapl.norma:assuntonorma_list' %}" class="btn btn-link">Assunto de Norma Jurídica</a></div>
<div class="col-md-6"><a href="{% url 'sapl.norma:tipovinculonormajuridica_list' %}" class="btn btn-link">Tipo de Vínculo</a></div>
</div>
<hr />
<h2>Módulo Textos Articulados</h2>
<div class="row">
<div class="col-md-6"><a class="btn btn-link" href="{% url 'sapl.compilacao:tipo_ta_list' %}">Tipos de Textos Articulados</a></div>
<div class="col-md-6"><a class="btn btn-link" href="{% url 'sapl.compilacao:tipopublicacao_list' %}">Tipos de Publicação</a></div>
<div class="col-md-6"><a class="btn btn-link" href="{% url 'sapl.compilacao:veiculopublicacao_list' %}">Veículos de Publicação</a></div>
<div class="col-md-6"><a class="btn btn-link" href="{% url 'sapl.compilacao:tiponota_list' %}">Tipos de Notas</a></div>
<div class="col-md-6"><a class="btn btn-link" href="{% url 'sapl.compilacao:tipovide_list' %}">Tipos de Vides</a></div>
<div class="col-md-6"><a class="btn btn-link" href="{% url 'sapl.compilacao:tipodispositivo_list' %}">Tipos de Dispositivos</a></div>
<div class="col-md-6"><a class="btn btn-link" href="/admin/compilacao/tipodispositivorelationship/">Relacionamento entre Dispositivos</a></div>
</div>
<hr />
<h2>Módulo Sessão Plenária</h2>

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

2
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',

Loading…
Cancel
Save