diff --git a/docker-compose.yml b/docker-compose.yml index e4cd84b21..5ef3b240e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ sapldb: ports: - "5432:5432" sapl: - image: interlegis/sapl:3.1.147 + image: interlegis/sapl:3.1.150 restart: always environment: ADMIN_PASSWORD: interlegis diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 6a101e223..7e6189373 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -27,9 +27,7 @@ WeasyPrint==44 Pillow==5.1.0 gunicorn==19.9.0 -textract==1.5.0 pysolr==3.6.0 -whoosh==2.7.4 pyoai==2.5.0 diff --git a/sapl/api/views.py b/sapl/api/views.py index 769196c76..f2f146e24 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -22,7 +22,7 @@ from sapl.api.forms import SaplFilterSetMixin from sapl.api.permissions import SaplModelPermissions from sapl.api.serializers import ChoiceSerializer from sapl.base.models import Autor, AppConfig, DOC_ADM_OSTENSIVO -from sapl.materia.models import Proposicao +from sapl.materia.models import Proposicao, TipoMateriaLegislativa from sapl.parlamentares.models import Parlamentar from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria @@ -349,6 +349,23 @@ class _ProposicaoViewSet(SaplSetViews['materia']['proposicao']): return qs +class _TipoMateriaLegislativaViewSet(SaplSetViews['materia']['tipomaterialegislativa']): + + @action(detail=True, methods=['POST']) + def change_position(self, request, *args, **kwargs): + result = { + 'status': 200, + 'message': 'OK' + } + d = request.data + if 'pos_ini' in d and 'pos_fim' in d: + if d['pos_ini'] != d['pos_fim']: + pk = kwargs['pk'] + TipoMateriaLegislativa.objects.reposicione(pk, d['pos_fim']) + + return Response(result) + + class _DocumentoAdministrativoViewSet(SaplSetViews['protocoloadm']['documentoadministrativo']): class DocumentoAdministrativoPermission(SaplModelPermissions): @@ -427,6 +444,7 @@ class _SessaoPlenariaViewSet( SaplSetViews['base']['autor'] = _AutorViewSet.build_class_with_actions() SaplSetViews['materia']['proposicao'] = _ProposicaoViewSet +SaplSetViews['materia']['tipomaterialegislativa'] = _TipoMateriaLegislativaViewSet SaplSetViews['parlamentares']['parlamentar'] = _ParlamentarViewSet diff --git a/sapl/base/email_utils.py b/sapl/base/email_utils.py index b41c68402..7c23dd2da 100644 --- a/sapl/base/email_utils.py +++ b/sapl/base/email_utils.py @@ -11,6 +11,7 @@ from sapl.materia.models import AcompanhamentoMateria from sapl.protocoloadm.models import AcompanhamentoDocumento from sapl.settings import EMAIL_SEND_USER from sapl.utils import mail_service_configured +from django.utils.translation import ugettext_lazy as _ def load_email_templates(templates, context={}): @@ -208,8 +209,8 @@ def do_envia_email_tramitacao(base_url, tipo, doc_mat, status, unidade_destino): # Envia email de tramitacao para usuarios cadastrados # + logger = logging.getLogger(__name__) if not mail_service_configured(): - logger = logging.getLogger(__name__) logger.warning(_('Servidor de email não configurado.')) return @@ -220,6 +221,10 @@ def do_envia_email_tramitacao(base_url, tipo, doc_mat, status, unidade_destino): destinatarios = AcompanhamentoDocumento.objects.filter(documento=doc_mat, confirmado=True) + if not destinatarios: + logger.debug(_('Não existem destinatários cadastrados para essa matéria.')) + return + casa = CasaLegislativa.objects.first() sender = EMAIL_SEND_USER diff --git a/sapl/base/search_indexes.py b/sapl/base/search_indexes.py index 0e0283ba8..359fbd44b 100644 --- a/sapl/base/search_indexes.py +++ b/sapl/base/search_indexes.py @@ -1,5 +1,4 @@ import os.path -import textract import logging from django.db.models import F, Q, Value @@ -11,7 +10,6 @@ from haystack.constants import Indexable from haystack.fields import CharField from haystack.indexes import SearchIndex from haystack.utils import get_model_ct_tuple -from textract.exceptions import ExtensionNotSupported from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC, STATUS_TA_PUBLIC, Dispositivo) @@ -49,19 +47,6 @@ class TextExtractField(CharField): data = '' return data - def whoosh_extraction(self, arquivo): - - if arquivo.path.endswith('html') or arquivo.path.endswith('xml'): - with open(arquivo.path, 'r', encoding="utf8", errors='ignore') as f: - content = ' '.join(f.read()) - return RemoveTag(content) - - else: - return textract.process( - arquivo.path, - language='pt-br').decode('utf-8').replace('\n', ' ').replace( - '\t', ' ') - def print_error(self, arquivo, error): msg = 'Erro inesperado processando arquivo %s erro: %s' % ( arquivo.path, error) @@ -80,20 +65,6 @@ class TextExtractField(CharField): except Exception as err: print(str(err)) self.print_error(arquivo, err) - - # Em ambiente de DEV utiliza-se o Whoosh - # Como ele não possui extração, faz-se uso do textract - else: - try: - self.logger.debug("Tentando whoosh_extraction no arquivo {}".format(arquivo.path)) - return self.whoosh_extraction(arquivo) - self.print_error(arquivo) - except ExtensionNotSupported as err: - print(str(err)) - self.logger.error(str(err)) - except Exception as err: - print(str(err)) - self.print_error(arquivo, str(err)) return '' def ta_extractor(self, value): diff --git a/sapl/base/urls.py b/sapl/base/urls.py index 317862f18..5d12b3586 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -8,7 +8,7 @@ from django.contrib.auth.views import (password_reset, password_reset_complete, password_reset_done) from django.views.generic.base import RedirectView, TemplateView -from sapl.base.views import AutorCrud, ConfirmarEmailView, TipoAutorCrud, get_data_ultima_atualizacao +from sapl.base.views import AutorCrud, ConfirmarEmailView, TipoAutorCrud, get_estatistica from sapl.settings import EMAIL_SEND_USER, MEDIA_URL from .apps import AppConfig @@ -176,7 +176,7 @@ urlpatterns = [ ListarLegislaturaInfindavelView.as_view(), name='lista_legislatura_infindavel'), - url(r'^sistema/data_ultima_atualizacao', get_data_ultima_atualizacao), + url(r'^sistema/estatisticas', get_estatistica), # todos os sublinks de sistema devem vir acima deste url(r'^sistema/$', permission_required('base.view_tabelas_auxiliares') diff --git a/sapl/base/views.py b/sapl/base/views.py index c6f982c8c..2312ab721 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -611,12 +611,13 @@ class RelatorioMateriasTramitacaoView(FilterView): qs = filtra_url_materias_em_tramitacao( qr, qs, 'tramitacao__status', 'status') - context['object_list'] = qs + li = [li1 for li1 in qs if li1.tramitacao_set.last() and li1.tramitacao_set.last().status.indicador != 'F'] + context['object_list'] = li qtdes = {} for tipo in TipoMateriaLegislativa.objects.all(): - qs = context['object_list'] - qtde = len(qs.filter(tipo_id=tipo.id)) + li = context['object_list'] + qtde = sum(1 for i in li if i.tipo_id==tipo.id) if qtde > 0: qtdes[tipo] = qtde context['qtdes'] = qtdes @@ -1234,7 +1235,9 @@ def mandato_sem_data_inicio(): return Mandato.objects.filter(data_inicio_mandato__isnull=True).order_by('parlamentar') -def get_data_ultima_atualizacao(request): +def get_estatistica(request): + + json_dict = {} datas = [MateriaLegislativa.objects.all(). order_by('-data_ultima_atualizacao'). @@ -1243,15 +1246,22 @@ def get_data_ultima_atualizacao(request): NormaJuridica.objects.all(). order_by('-data_ultima_atualizacao'). values_list('data_ultima_atualizacao', flat=True). - first()] + first()] # Retorna [None, None] se inexistem registros max_data = '' if datas[0] and datas[1]: max_data = max(datas) else: - max_data = next([i for i in datas if i is not None], '') - return JsonResponse({'data_ultima_atualizacao': max_data}) + max_data = next(iter([i for i in datas if i is not None]), '') + + json_dict["data_ultima_atualizacao"] = max_data + json_dict["num_materias_legislativas"] = MateriaLegislativa.objects.all().count() + json_dict["num_normas_juridicas "] = NormaJuridica.objects.all().count() + json_dict["num_parlamentares"] = Parlamentar.objects.all().count() + json_dict["num_sessoes_plenarias"] = SessaoPlenaria.objects.all().count() + + return JsonResponse(json_dict) class ListarMandatoSemDataInicioView(PermissionRequiredMixin, ListView): @@ -1452,7 +1462,7 @@ class PesquisarUsuarioView(PermissionRequiredMixin, FilterView): data = self.filterset.data url = '' if data: - url = "&" + str(self.request.environ['QUERY_STRING']) + url = "&" + str(self.request.META['QUERY_STRING']) if url.startswith("&page"): ponto_comeco = url.find('username=') - 1 url = url[ponto_comeco:] diff --git a/sapl/comissoes/views.py b/sapl/comissoes/views.py index 743eaa58f..d6d129b11 100644 --- a/sapl/comissoes/views.py +++ b/sapl/comissoes/views.py @@ -186,6 +186,7 @@ class MateriasTramitacaoListView(ListView): context = super( MateriasTramitacaoListView, self).get_context_data(**kwargs) context['object'] = Comissao.objects.get(id=self.kwargs['pk']) + context['qtde'] = self.object_list.count() return context diff --git a/sapl/compilacao/forms.py b/sapl/compilacao/forms.py index 71d6ad7e5..222a86aed 100644 --- a/sapl/compilacao/forms.py +++ b/sapl/compilacao/forms.py @@ -3,7 +3,6 @@ from datetime import timedelta from crispy_forms.bootstrap import (Alert, FieldWithButtons, FormActions, InlineCheckboxes, InlineRadios, StrictButton) -from sapl.crispy_layout_mixin import SaplFormHelper from crispy_forms.layout import (HTML, Button, Column, Div, Field, Fieldset, Layout, Row, Submit) from django import forms @@ -23,10 +22,12 @@ from sapl.compilacao.models import (NOTAS_PUBLICIDADE_CHOICES, TipoTextoArticulado, TipoVide, VeiculoPublicacao, Vide) from sapl.compilacao.utils import DISPOSITIVO_SELECT_RELATED +from sapl.crispy_layout_mixin import SaplFormHelper from sapl.crispy_layout_mixin import SaplFormLayout, to_column, to_row,\ form_actions from sapl.utils import YES_NO_CHOICES + error_messages = { 'required': _('Este campo é obrigatório'), 'invalid': _('URL inválida.') @@ -59,6 +60,13 @@ class TipoTaForm(ModelForm): widget=forms.RadioSelect(), required=True) + rodape_global = forms.CharField( + label=TipoTextoArticulado._meta.get_field( + 'rodape_global').verbose_name, + widget=forms.Textarea(attrs={'id': 'texto-rico'}), + required=False + ) + class Meta: model = TipoTextoArticulado fields = ['sigla', @@ -66,10 +74,12 @@ class TipoTaForm(ModelForm): 'content_type', 'participacao_social', 'publicacao_func', - 'perfis' + 'perfis', + 'rodape_global' ] - widgets = {'perfis': widgets.CheckboxSelectMultiple()} + widgets = {'perfis': widgets.CheckboxSelectMultiple(), + 'rodape_global': forms.Textarea} def __init__(self, *args, **kwargs): @@ -84,12 +94,18 @@ class TipoTaForm(ModelForm): ('perfis', 12), ]) + row3 = to_row([ + ('rodape_global', 12), + ]) + self.helper = SaplFormHelper() self.helper.layout = SaplFormLayout( Fieldset(_('Identificação Básica'), row1, css_class="col-md-12"), Fieldset(_('Funcionalidades'), - row2, css_class="col-md-12")) + row2, css_class="col-md-12"), + Fieldset(_('Nota de Rodapé Global'), + row3, css_class="col-md-12")) super(TipoTaForm, self).__init__(*args, **kwargs) diff --git a/sapl/compilacao/migrations/0011_tipotextoarticulado_rodape_global.py b/sapl/compilacao/migrations/0011_tipotextoarticulado_rodape_global.py new file mode 100644 index 000000000..f3b0e323b --- /dev/null +++ b/sapl/compilacao/migrations/0011_tipotextoarticulado_rodape_global.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-26 18:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0010_auto_20181004_1939'), + ] + + operations = [ + migrations.AddField( + model_name='tipotextoarticulado', + name='rodape_global', + field=models.TextField(default='', help_text='A cada Tipo de Texto Articulado pode ser adicionado uma nota global de rodapé!', verbose_name='Rodapé Global'), + ), + ] diff --git a/sapl/compilacao/models.py b/sapl/compilacao/models.py index f36e406f9..f64285aee 100644 --- a/sapl/compilacao/models.py +++ b/sapl/compilacao/models.py @@ -149,6 +149,13 @@ class TipoTextoArticulado(models.Model): em edição. """)) + rodape_global = models.TextField( + verbose_name=_('Rodapé Global'), + help_text=_('A cada Tipo de Texto Articulado pode ser adicionado ' + 'uma nota global de rodapé!'), + default='' + ) + class Meta: verbose_name = _('Tipo de Texto Articulado') verbose_name_plural = _('Tipos de Texto Articulados') diff --git a/sapl/crud/base.py b/sapl/crud/base.py index 5fa4f70e5..98af2a3bb 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -2,7 +2,6 @@ import logging from braces.views import FormMessagesMixin from crispy_forms.bootstrap import FieldWithButtons, StrictButton -from sapl.crispy_layout_mixin import SaplFormHelper from crispy_forms.layout import Field, Layout from django import forms from django.conf.urls import url @@ -25,6 +24,7 @@ from django.views.generic.base import ContextMixin from django.views.generic.list import MultipleObjectMixin from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display +from sapl.crispy_layout_mixin import SaplFormHelper from sapl.rules.map_rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, RP_LIST) from sapl.settings import BASE_DIR @@ -449,18 +449,28 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView): if not n: s += '
' continue + m = obj n = n.split('__') for f in n[:-1]: m = getattr(m, f) if not m: break + + ss = '' if m: ss = get_field_display(m, n[-1])[1] ss = ( ('
' if '