diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt index 63ab21d6c..9d8174f64 100644 --- a/requirements/dev-requirements.txt +++ b/requirements/dev-requirements.txt @@ -1,7 +1,8 @@ -r test-requirements.txt -autopep8==1.2.2 +autopep8==1.2.4 beautifulsoup4==4.4.1 django-debug-toolbar==1.4 -ipdb==0.9.0 -pygraphviz==1.3rc2 -pytest-ipdb==0.1-prerelease2 \ No newline at end of file +ipdb==0.10.1 +pip-review==0.4 +pygraphviz==1.3.1 +pytest-ipdb==0.1-prerelease2 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 2af641724..44ef147c0 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,25 +2,25 @@ dj-database-url==0.4.1 django-admin-bootstrapped==2.5.7 django-bootstrap3==7.0.1 django-bower==5.1.0 -django-braces==1.8.1 +django-braces==1.9.0 django-compressor==2.0 django-crispy-forms==1.6.0 -django-extensions==1.6.1 -django-extra-views==0.7.1 +django-extensions==1.6.7 +django-extra-views==0.8.0 django-filter==0.13.0 -django-floppyforms==1.6.1 -django-model-utils==2.4 -django-sass-processor==0.3.4 -django==1.9.5 +django-floppyforms==1.6.2 +django-model-utils==2.5 +django-sass-processor==0.4.0 +django==1.9.7 djangorestframework easy-thumbnails==2.3 git+git://github.com/interlegis/trml2pdf.git -libsass==0.11.0 -psycopg2==2.6.1 +libsass==0.11.1 +psycopg2==2.6.2 python-decouple==3.0 -pytz==2016.3 +pytz==2016.4 pyyaml==3.11 -rtyaml==0.0.2 +rtyaml==0.0.3 unipath==1.1 -python-magic==0.4.10 -gunicorn==19.4.5 +python-magic==0.4.12 +gunicorn==19.6.0 diff --git a/requirements/test-requirements.txt b/requirements/test-requirements.txt index 03bd937a2..7dd6def90 100644 --- a/requirements/test-requirements.txt +++ b/requirements/test-requirements.txt @@ -1,11 +1,11 @@ -r requirements.txt -coverage==4.0.3 +coverage==4.1 django-webtest -flake8==2.5.4 +flake8==2.6.2 isort==4.2.5 -model_mommy==1.2.6 +model-mommy==1.2.6 pep8==1.7.0 -pytest==2.7.2 -pytest-cov==2.2.1 +pytest==2.9.2 +pytest-cov==2.3.0 pytest-django==2.9.1 -webtest==2.0.21 \ No newline at end of file +webtest==2.0.21 diff --git a/sapl/base/migrations/0016_auto_20160701_0940.py b/sapl/base/migrations/0016_auto_20160701_0940.py new file mode 100644 index 000000000..0347cbd19 --- /dev/null +++ b/sapl/base/migrations/0016_auto_20160701_0940.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-07-01 12:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0015_problemamigracao_nome_campo'), + ] + + operations = [ + migrations.AlterModelOptions( + name='casalegislativa', + options={'verbose_name': 'Casa Legislativa', 'verbose_name_plural': 'Casa Legislativa'}, + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index b2310d566..966f66de3 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -46,7 +46,11 @@ class CasaLegislativa(models.Model): class Meta: verbose_name = _('Casa Legislativa') - verbose_name_plural = _('Casas Legislativas') + verbose_name_plural = _('Casa Legislativa') + + def __str__(self): + return _('Casa Legislativa de %(municipio)s') % { + 'municipio': self.municipio} class ProblemaMigracao(models.Model): diff --git a/sapl/base/templatetags/menus.py b/sapl/base/templatetags/menus.py index 4d1f14b4c..2f121d3c7 100644 --- a/sapl/base/templatetags/menus.py +++ b/sapl/base/templatetags/menus.py @@ -41,7 +41,7 @@ def subnav(context, path=None): if path: yaml_path = path elif 'subnav_template_name' in context: - yaml_path = context['subnav_template_name'] + yaml_path = context['subnav_template_name'] else: yaml_path = '%s/%s' % (app_template, 'subnav.yaml') diff --git a/sapl/base/views.py b/sapl/base/views.py index e0ea3f9e3..4b8c91305 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1,7 +1,10 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect from django.views.generic.base import TemplateView -from sapl.crud.base import Crud, CrudBaseMixin, CrudCreateView, CrudUpdateView +from sapl.crud.base import (Crud, CrudBaseMixin, CrudCreateView, CrudUpdateView, + CrudDetailView) from sapl.utils import permissao_tb_aux from .forms import CasaLegislativaForm @@ -33,8 +36,16 @@ class CasaLegislativaCrud(Crud): permission_required = {'base.change_casalegislativa'} form_class = CasaLegislativaForm + class DetailView(CrudDetailView): + form_class = CasaLegislativaForm + + def get(self, request, *args, **kwargs): + return HttpResponseRedirect( + reverse('sapl.base:casalegislativa_update', + kwargs={'pk': self.kwargs['pk']})) + -class HelpView(PermissionRequiredMixin, TemplateView): +class HelpView(TemplateView): # XXX treat non existing template as a 404!!!! def get_template_names(self): diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index 6c60c9b79..c6c4437a6 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -145,7 +145,6 @@ class CrispyLayoutFormMixin: def read_yaml_from_file(yaml_layout): - # TODO cache this at application level t = template.loader.get_template(yaml_layout) rendered = t.render() diff --git a/sapl/parlamentares/forms.py b/sapl/parlamentares/forms.py index c61e4fb57..9b636e882 100644 --- a/sapl/parlamentares/forms.py +++ b/sapl/parlamentares/forms.py @@ -1,14 +1,13 @@ -from datetime import date +from datetime import date, timedelta from django import forms from django.core.exceptions import ValidationError from django.db import transaction +from django.db.models import Q from django.forms import ModelForm from django.utils.translation import ugettext_lazy as _ from floppyforms.widgets import ClearableFileInput -from sapl.utils import intervalos_tem_intersecao - from .models import (ComposicaoColigacao, Filiacao, Legislatura, Mandato, Parlamentar) @@ -74,59 +73,60 @@ class ParlamentarCreateForm(ParlamentarForm): def validar_datas(data_filiacao, data_desfiliacao, parlamentar, filiacao): - # Verifica se data de desfiliacao é anterior a data de filiacao if data_desfiliacao and data_desfiliacao < data_filiacao: error_msg = _("A data de desfiliação não pode anterior \ à data de filiação") return [False, error_msg] - filiacao_atual_id = filiacao.pk - # recupera filiacoes em ordem crescente de data - todas_filiacoes = parlamentar.filiacao_set.all().order_by('data') - filiacoes_id = [parlamentar.pk for parlamentar in todas_filiacoes] - - # Novo registro inserido com filiacoes ja existentes - if filiacao_atual_id not in filiacoes_id and len(filiacoes_id) > 0: - ultima_filiacao = todas_filiacoes.last() - # Se ultima filiacao aberta e insercao posterior a esta filiacao - if (not ultima_filiacao.data_desfiliacao and - data_filiacao >= ultima_filiacao.data): - error_msg = _("O parlamentar não pode se filiar \ - a novo partido sem antes se \ - desfiliar do partido anterior") - return [False, error_msg] - - # checa intervalos de interseccao + filiacoes = parlamentar.filiacao_set.order_by('data') + if not filiacoes.exists(): + return [True, ''] + + # data ficticia de desfiliacao + df_desfiliacao = data_desfiliacao if data_desfiliacao else date.today() + + # se não puder haver filiação no mesmo dia de desfiliação, basta + # retirar os timedelta abaixo + range_livre_exigido = Q( + data__range=[data_filiacao + timedelta(days=1), + df_desfiliacao - timedelta(days=1)]) | Q( + data_desfiliacao__range=[data_filiacao + timedelta(days=1), + df_desfiliacao - timedelta(days=1)]) + + filiacao_em_edicao_id = filiacao.pk error_msg = None - for filiacoes in todas_filiacoes: - # nao comparar o registro com ele mesmo - if filiacoes.id != filiacao_atual_id: - - # Se a atualizacao eh para remover a data de desfiliacao - if not data_desfiliacao: - # so permite na ultima data (ou a unica) - if filiacao_atual_id != filiacoes_id[-1]: - error_msg = _("Data de desfiliação do parlamentar não \ - pode ser ausente, se existirem datas de \ - filiação posteriores") - return [False, error_msg] - else: - data_inicio = filiacoes.data - data_fim = filiacoes.data_desfiliacao - - # Se filiacao ainda em aberto, preenche uma desfiliacao - # ficticia para fins de checagem de interseccao - if not data_fim: - data_fim = date.today() - - # finalmente verifica intersecao - if intervalos_tem_intersecao(data_inicio, data_fim, - data_filiacao, data_desfiliacao): - error_msg = _("A data de filiação e \ - desfiliação não podem estar no intervalo \ - de outro período de filiação") - break + # filiação em edição não é a última e está sem data de desfiliação + if not data_desfiliacao and filiacao_em_edicao_id and\ + filiacao_em_edicao_id != filiacoes.last().pk: + error_msg = _("Data de desfiliação do parlamentar não pode ser\ + ausente, se existirem datas de filiação posteriores.") + + # a filiação que está sendo inclusa não tem data de desfiliação mas + # já existe outra sem data de desfiliação + elif not data_desfiliacao and not filiacao_em_edicao_id and\ + not filiacoes.last().data_desfiliacao: + error_msg = _("O parlamentar não pode se filiar a novo partido sem\ + antes se desfiliar do partido anterior.") + + if not error_msg: + # se a filiação é uma edição, a exclui das possibilidades + if filiacao_em_edicao_id: + filiacoes = filiacoes.exclude(pk=filiacao_em_edicao_id) + + # testa a intercessão de intervalo com outra filiação + if filiacoes.filter(range_livre_exigido).exists(): + error_msg = _("A data de filiação e desfiliação não podem estar\ + no intervalo de outro período de filiação.") + + if not error_msg: + # passou pelo teste de intervalo mas a data de filiação é maior que + # a ultima que está em aberto + if filiacoes.filter(data_desfiliacao__isnull=True, + data__lte=data_filiacao).exists(): + error_msg = _("Não pode haver um registro de filiação com data de \ + filiação igual ou superior a data de filiação em aberto.") + if error_msg: return [False, error_msg] diff --git a/sapl/templates/parlamentares/layouts.yaml b/sapl/templates/parlamentares/layouts.yaml index 48e11240c..24a0aab86 100644 --- a/sapl/templates/parlamentares/layouts.yaml +++ b/sapl/templates/parlamentares/layouts.yaml @@ -62,7 +62,7 @@ ParlamentarCreate: - biografia Filiacao: - {% trans ''Filiações Partidárias '' %}: + {% trans 'Filiações Partidárias' %}: - partido data data_desfiliacao Mandato: @@ -89,5 +89,5 @@ SituacaoMilitar: - descricao ComposicaoColigacao: - {% trans ''Nome do Partido'' %}: + {% trans 'Nome do Partido' %}: - partido diff --git a/sapl/test_crispy_layout_mixin.py b/sapl/test_crispy_layout_mixin.py index 5ed4a910c..2c72a1b44 100644 --- a/sapl/test_crispy_layout_mixin.py +++ b/sapl/test_crispy_layout_mixin.py @@ -1,9 +1,13 @@ +from unittest import mock + +import rtyaml + from sapl.crispy_layout_mixin import read_layout_from_yaml def test_read_layout_from_yaml(tmpdir): - contents = ''' + stub_content = ''' ModelName: Cool Legend: - name:9 place tiny @@ -12,18 +16,17 @@ ModelName: More data: - equalA equalB equalC - highlander ''' - file = tmpdir.join('zzz.yaml') - file.write(contents) - expected = [ - ['Cool Legend', - [('name', 9), ('place', 2), ('tiny', 1)], - [('field', 10), ('nature', 2)], - [('kind', 1), ('date', 3), ('unit', 5), ('status', 3)], - ], - ['More data', - [('equalA', 4), ('equalB', 4), ('equalC', 4)], - [('highlander', 12)], - ], - ] - assert read_layout_from_yaml(file.strpath, 'ModelName') == expected + with mock.patch('sapl.crispy_layout_mixin.read_yaml_from_file') as ryff: + ryff.return_value = rtyaml.load(stub_content) + assert read_layout_from_yaml('....', 'ModelName') == [ + ['Cool Legend', + [('name', 9), ('place', 2), ('tiny', 1)], + [('field', 10), ('nature', 2)], + [('kind', 1), ('date', 3), ('unit', 5), ('status', 3)], + ], + ['More data', + [('equalA', 4), ('equalB', 4), ('equalC', 4)], + [('highlander', 12)], + ], + ] diff --git a/sapl/test_general.py b/sapl/test_general.py index 53465b193..da2e17ae6 100644 --- a/sapl/test_general.py +++ b/sapl/test_general.py @@ -7,21 +7,19 @@ from .settings import SAPL_APPS pytestmark = pytest.mark.django_db -sapl_appconfs = [apps.get_app_config(n) for n in SAPL_APPS] +sapl_appconfs = [apps.get_app_config(n[5:]) for n in SAPL_APPS] -def test_charfiled_textfield(): +def test_charfield_textfield(): for app in sapl_appconfs: for model in app.get_models(): fields = model._meta.local_fields for field in fields: if isinstance(field, (CharField, TextField)): - msg = 'Model = %s || Field = %s - %s - %s' % ( - model.__name__, - field.attname, - type(field).__name__, - field.null) - assert not field.null, msg + assert not field.null, 'This %s is null: %s.%s' % ( + type(field).__name__, + model.__name__, + field.attname) def test_str_sanity(): diff --git a/scripts/atualizar_requirements.py b/scripts/atualizar_requirements.py new file mode 100755 index 000000000..5b8e22d22 --- /dev/null +++ b/scripts/atualizar_requirements.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +# Este script altera os arquivos requirements/*requirements.txt +# atualizando as versões fixadas neles para coincidirem com as do venv. +# +# Rode esse script após atualizar as dependências do venv usando, p. ex.: +# pip-review +# +# Após usá-lo confira sempre o resultado com `git diff` e teste as mudanças + +import glob +import re +import subprocess + +freeze_output = subprocess.Popen( + 'pip freeze', shell=True, + stdout=subprocess.PIPE).stdout.read().decode('ascii') +freeze = freeze_output.strip().split('\n') +freeze = {name.lower(): version + for name, version in [re.split('==+', s) for s in freeze]} +req_files = glob.glob('requirements/*requirements.txt') +requirements = [(f, open(f).read().strip().split('\n')) + for f in req_files] + + +def novas_linhas(linhas): + for linha in linhas: + split = re.split('==', linha) + if len(split) == 1: + yield split[0] + else: + nome, versao = split + nome = nome.lower() + yield '%s==%s' % (nome, freeze[nome]) + +for arq, linhas in requirements: + with open(arq, 'w') as f: + f.writelines(l + '\n' for l in novas_linhas(linhas))