diff --git a/Dockerfile b/Dockerfile index d35746703..85724ca11 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 vim +py3-magic postgresql-client poppler-utils vim RUN apk add --no-cache python3 nginx && \ python3 -m ensurepip && \ diff --git a/docs/instacao31.rst b/docs/instacao31.rst index 3d6a8b3eb..9a42573ac 100644 --- a/docs/instacao31.rst +++ b/docs/instacao31.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 + python3-pip curl poppler-utils sudo -i curl -sL https://deb.nodesource.com/setup_5.x | bash - diff --git a/sapl/api/forms.py b/sapl/api/forms.py index c220c01b2..afa9190f4 100644 --- a/sapl/api/forms.py +++ b/sapl/api/forms.py @@ -1,5 +1,8 @@ from django.db.models import Q +from django.forms.fields import MultiValueField, CharField +from django.forms.widgets import TextInput, MultiWidget from django_filters.filters import MethodFilter, ModelChoiceFilter +from rest_framework.compat import django_filters from rest_framework.filters import FilterSet from sapl.base.models import Autor, TipoAutor @@ -35,10 +38,11 @@ class SaplGenericRelationSearchFilterSet(FilterSet): item.related_query_name(), field[0]) ) - q_fs = q_fs | Q(**{'%s__%s%s' % ( - item.related_query_name(), - field[0], - field[1]): qtext}) + if len(field) == 3 and field[2](qtext) is not None: + q_fs = q_fs | Q(**{'%s__%s%s' % ( + item.related_query_name(), + field[0], + field[1]): qtext if len(field) == 2 else field[2](qtext)}) q = q & q_fs @@ -48,6 +52,37 @@ class SaplGenericRelationSearchFilterSet(FilterSet): return queryset +class SearchForFieldWidget(MultiWidget): + + def decompress(self, value): + if value is None: + return [None, None] + return value + + def __init__(self, attrs=None): + widgets = (TextInput, TextInput) + MultiWidget.__init__(self, widgets, attrs) + + +class SearchForFieldField(MultiValueField): + widget = SearchForFieldWidget + + def __init__(self, *args, **kwargs): + fields = ( + CharField(), + CharField()) + super(SearchForFieldField, self).__init__(fields, *args, **kwargs) + + def compress(self, parameters): + if parameters: + return parameters + return None + + +class SearchForFieldFilter(django_filters.filters.MethodFilter): + field_class = SearchForFieldField + + class AutorChoiceFilterSet(SaplGenericRelationSearchFilterSet): q = MethodFilter() tipo = ModelChoiceFilter(queryset=TipoAutor.objects.all()) @@ -60,4 +95,23 @@ class AutorChoiceFilterSet(SaplGenericRelationSearchFilterSet): def filter_q(self, queryset, value): return SaplGenericRelationSearchFilterSet.filter_q( - self, queryset, value).order_by('nome') + self, queryset, value).distinct('nome').order_by('nome') + + +class AutorSearchForFieldFilterSet(AutorChoiceFilterSet): + q = SearchForFieldFilter() + + class Meta(AutorChoiceFilterSet.Meta): + pass + + def filter_q(self, queryset, value): + + value[0] = value[0].split(',') + value[1] = value[1].split(',') + + params = {} + for key, v in list(zip(value[0], value[1])): + if v in ['True', 'False']: + v = '1' if v == 'True' else '0' + params[key] = v + return queryset.filter(**params).distinct('nome').order_by('nome') diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index 21a48a848..60ea6692a 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -46,6 +46,7 @@ class AutorSerializer(serializers.ModelSerializer): class Meta: model = Autor + fields = '__all__' class MateriaLegislativaSerializer(serializers.ModelSerializer): diff --git a/sapl/api/views.py b/sapl/api/views.py index 4a8341c52..eb1364bf7 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -10,7 +10,7 @@ from rest_framework.permissions import (IsAuthenticated, AllowAny) from rest_framework.viewsets import GenericViewSet, ModelViewSet -from sapl.api.forms import AutorChoiceFilterSet +from sapl.api.forms import AutorChoiceFilterSet, AutorSearchForFieldFilterSet from sapl.api.serializers import (AutorChoiceSerializer, AutorSerializer, ChoiceSerializer, MateriaLegislativaSerializer, @@ -79,7 +79,57 @@ class AutorListView(ListAPIView): o django-filter é desativado e a busca é feita no model do ContentType associado ao tipo. - Outros campos + - q_0 / q_1 - q_0 faz o código ignorar "q"... + + q_0 -> campos lookup a serem filtrados em qualquer Model + que implemente SaplGenericRelation + q_1 -> o valor que será pesquisado no lookup de q_0 + + q_0 e q_1 podem ser separados por ","... isso dará a + possibilidade de filtrar mais de um campo. + + + http://localhost:8000 + /api/autor?tr=1&q_0=parlamentar_set__ativo&q_1=False + /api/autor?tr=1&q_0=parlamentar_set__ativo&q_1=True + /api/autor?tr=3&q_0=parlamentar_set__ativo&q_1=False + /api/autor?tr=3&q_0=parlamentar_set__ativo&q_1=True + + http://localhost:8000 + /api/autor?tr=1 + &q_0=parlamentar_set__nome_completo__icontains, + parlamentar_set__ativo + &q_1=Carvalho,False + /api/autor?tr=1 + &q_0=parlamentar_set__nome_completo__icontains, + parlamentar_set__ativo + &q_1=Carvalho,True + /api/autor?tr=3 + &q_0=parlamentar_set__nome_completo__icontains, + parlamentar_set__ativo + &q_1=Carvalho,False + /api/autor?tr=3 + &q_0=parlamentar_set__nome_completo__icontains, + parlamentar_set__ativo + &q_1=Carvalho,True + + + não importa o campo que vc passe de qualquer dos Models + ligados... é possível ver que models são esses, + na ocasião do commit deste texto, executando: + In [6]: from sapl.utils import models_with_gr_for_model + + In [7]: models_with_gr_for_model(Autor) + Out[7]: + [sapl.parlamentares.models.Parlamentar, + sapl.parlamentares.models.Frente, + sapl.comissoes.models.Comissao, + sapl.materia.models.Orgao, + sapl.sessao.models.Bancada, + sapl.sessao.models.Bloco] + + qualquer atributo destes models podem ser passados + para busca """ TR_AUTOR_CHOICE_SERIALIZER = 1 @@ -125,6 +175,9 @@ class AutorListView(ListAPIView): self.serializer_class = AutorSerializer self.permission_classes = (IsAuthenticated,) + if self.filter_class and 'q_0' in request.GET: + self.filter_class = AutorSearchForFieldFilterSet + return ListAPIView.get(self, request, *args, **kwargs) def get_queryset(self): @@ -207,6 +260,7 @@ class MateriaLegislativaViewSet(ListModelMixin, filter_backends = (DjangoFilterBackend,) filter_fields = ('numero', 'ano', 'tipo', ) + class SessaoPlenariaViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): diff --git a/sapl/base/search_indexes.py b/sapl/base/search_indexes.py index e41dac3bf..61fe3438a 100644 --- a/sapl/base/search_indexes.py +++ b/sapl/base/search_indexes.py @@ -32,10 +32,7 @@ class DocumentoAcessorioIndex(indexes.SearchIndex, indexes.Indexable): arquivo = getattr(obj, self.filename) if arquivo: - try: - arquivo.open() - arquivo.close() - except OSError: + if not os.path.exists(arquivo.path): return self.prepared_data if not os.path.splitext(arquivo.path)[1][:1]: @@ -43,10 +40,13 @@ class DocumentoAcessorioIndex(indexes.SearchIndex, indexes.Indexable): try: extracted_data = textract.process( - arquivo.path).decode( - 'utf-8').replace('\n', ' ') + arquivo.path, + language='pt-br').decode('utf-8').replace('\n', ' ') except ExtensionNotSupported: return self.prepared_data + except Exception: + print('Erro inesperado processando arquivo: %s' % arquivo.path) + return self.prepared_data extracted_data = extracted_data.replace('\t', ' ') @@ -54,8 +54,8 @@ class DocumentoAcessorioIndex(indexes.SearchIndex, indexes.Indexable): # text field with *all* of our metadata visible for templating: t = loader.select_template(( 'search/indexes/' + self.template_name, )) - data['text'] = t.render(Context({'object': obj, - 'extracted': extracted_data})) + data['text'] = t.render({'object': obj, + 'extracted': extracted_data}) return data diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 7c0b6d8c8..978fce522 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -164,18 +164,6 @@ class DocumentoAcessorioForm(ModelForm): class Meta: model = DocumentoAcessorio fields = ['tipo', 'nome', 'data', 'autor', 'ementa', 'arquivo'] - widgets = {'autor': forms.HiddenInput()} - - def clean_autor(self): - autor_field = self.cleaned_data['autor'] - try: - int(autor_field) - except ValueError: - return autor_field - else: - if autor_field: - return str(Autor.objects.get(id=autor_field)) - class RelatoriaForm(ModelForm): diff --git a/sapl/materia/migrations/0005_auto_20170522_1904.py b/sapl/materia/migrations/0005_auto_20170522_1904.py new file mode 100644 index 000000000..7789aec96 --- /dev/null +++ b/sapl/materia/migrations/0005_auto_20170522_1904.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2017-05-22 19:04 +from __future__ import unicode_literals + +from django.db import migrations, models +import sapl.materia.models +import sapl.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0004_auto_20170504_1751'), + ] + + operations = [ + migrations.AlterField( + model_name='documentoacessorio', + name='arquivo', + field=models.FileField(blank=True, null=True, upload_to=sapl.materia.models.anexo_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Integral'), + ), + migrations.AlterField( + model_name='materialegislativa', + name='texto_original', + field=models.FileField(blank=True, null=True, upload_to=sapl.materia.models.materia_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Original'), + ), + migrations.AlterField( + model_name='proposicao', + name='texto_original', + field=models.FileField(blank=True, null=True, upload_to=sapl.materia.models.materia_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Original'), + ), + ] diff --git a/sapl/materia/views.py b/sapl/materia/views.py index f71bfc373..8690bb17a 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -963,25 +963,21 @@ class DocumentoAcessorioCrud(MasterDetailCrud): form_class = DocumentoAcessorioForm def __init__(self, **kwargs): - montar_helper_documento_acessorio(self) super(MasterDetailCrud.CreateView, self).__init__(**kwargs) def get_context_data(self, **kwargs): context = super( MasterDetailCrud.CreateView, self).get_context_data(**kwargs) - context['helper'] = self.helper return context class UpdateView(MasterDetailCrud.UpdateView): form_class = DocumentoAcessorioForm def __init__(self, **kwargs): - montar_helper_documento_acessorio(self) super(MasterDetailCrud.UpdateView, self).__init__(**kwargs) def get_context_data(self, **kwargs): context = super(UpdateView, self).get_context_data(**kwargs) - context['helper'] = self.helper return context @@ -1617,15 +1613,15 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView): tipo = TipoDocumento.objects.get(descricao=request.POST['tipo']) for materia_id in marcadas: - DocumentoAcessorio.objects.create( - materia_id=materia_id, - tipo=tipo, - arquivo=request.POST['arquivo'], - nome=request.POST['nome'], - data=datetime.strptime(request.POST['data'], "%d/%m/%Y"), - autor=Autor.objects.get(id=request.POST['autor']), - ementa=request.POST['ementa'] - ) + doc = DocumentoAcessorio() + doc.materia_id = materia_id + doc.tipo = tipo + doc.arquivo = request.FILES['arquivo'] + doc.nome = request.POST['nome'] + doc.data = datetime.strptime(request.POST['data'], "%d/%m/%Y") + doc.autor = request.POST['autor'] + doc.ementa = request.POST['ementa'] + doc.save() msg = _('Documento(s) criado(s).') messages.add_message(request, messages.SUCCESS, msg) return self.get(request, self.kwargs) diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py index 4d15ab6fd..2f1284fbf 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -1,9 +1,9 @@ from datetime import datetime -import reversion from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils import Choices +import reversion from sapl.base.models import Autor from sapl.utils import (INDICADOR_AFASTAMENTO, UF, YES_NO_CHOICES, @@ -206,6 +206,15 @@ def foto_upload_path(instance, filename): return texto_upload_path(instance, filename, subpath='') +def true_false_none(x): + if x == 'True': + return True + elif x == 'False': + return False + else: + return None + + @reversion.register() class Parlamentar(models.Model): FEMININO = 'F' @@ -303,6 +312,7 @@ class Parlamentar(models.Model): ('nome_completo', '__icontains'), ('nome_parlamentar', '__icontains'), ('filiacao__partido__sigla', '__icontains'), + ('ativo', '', true_false_none), )) class Meta: diff --git a/sapl/templates/base.html b/sapl/templates/base.html index 51351e7a0..5348ee291 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -157,11 +157,13 @@
- Desenvolvido pelo Interlegis em software livre e aberto. + Desenvolvido pelo Interlegis em software livre e aberto.