diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..8f4c8f147 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +media +collected_static +.git +whoosh diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 8d64c275c..c549f2bd4 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -26,11 +26,11 @@ ## Imagens do Ocorrido - ## Seu Ambiente * Versão usada (_Release_): * Nome e versão do navegador: * Nome e versão do Sistema Operacional (desktop ou mobile): -* Link para o seu projeto (Caso de fork deste projeto): \ No newline at end of file +* Link para o seu projeto (Caso de fork deste projeto): diff --git a/.gitignore b/.gitignore index cb6cce48e..e59540379 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Nodejs node_modules/ +yarn.lock # Distribution / packaging .Python @@ -53,7 +54,7 @@ coverage.xml # Django stuff: *.log - +sapl.log.* *.swp # Sphinx documentation @@ -88,6 +89,7 @@ target/ .ipynb_checkpoints/ *.ipynb .vscode/* +*/.vscode/* # specific to this project whoosh_index diff --git a/.travis.yml b/.travis.yml index f54ee6d20..80532c789 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ install: - pip install -r requirements/test-requirements.txt before_script: - - npm install -g bower - cp sapl/.env_test sapl/.env - psql -c "CREATE USER sapl WITH PASSWORD 'sapl'" -U postgres; - psql -c "CREATE DATABASE sapl OWNER sapl;" -U postgres @@ -18,7 +17,6 @@ before_script: script: - ./manage.py migrate - - ./manage.py bower install - py.test --create-db # - ./scripts/django/test_and_check_qa.sh diff --git a/Dockerfile b/Dockerfile index 3f3adc78e..d970a4e4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ FROM alpine:3.8 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 npm py3-lxml py3-magic postgresql-client poppler-utils antiword vim openssh-client + python3-dev libxml2-dev jpeg-dev libressl-dev libffi-dev libxslt-dev \ + nodejs py3-lxml py3-magic postgresql-client poppler-utils antiword \ + curl jq openssh-client vim openssh-client bash RUN apk update --update-cache && apk upgrade @@ -16,9 +17,7 @@ RUN apk add --no-cache python3 nginx tzdata && \ rm -f /etc/nginx/conf.d/* RUN mkdir -p /var/interlegis/sapl && \ - apk add --update --no-cache $BUILD_PACKAGES && \ - npm install -g bower && \ - npm cache verify + apk add --update --no-cache $BUILD_PACKAGES WORKDIR /var/interlegis/sapl/ @@ -36,13 +35,6 @@ COPY config/env_dockerfile /var/interlegis/sapl/sapl/.env # Configura timezone para BRT # RUN cp /usr/share/zoneinfo/America/Sao_Paulo /etc/localtime && echo "America/Sao_Paulo" > /etc/timezone -# manage.py bower install bug: https://github.com/nvbn/django-bower/issues/51 - -# compilescss - Precompile all occurrences of your SASS/SCSS files for the whole project into css files - -RUN python3 manage.py bower_install --allow-root && \ - python3 manage.py compilescss - RUN python3 manage.py collectstatic --noinput --clear # Remove .env(fake) e sapl.db da imagem @@ -52,7 +44,8 @@ RUN rm -rf /var/interlegis/sapl/sapl/.env && \ RUN chmod +x /var/interlegis/sapl/start.sh && \ ln -sf /dev/stdout /var/log/nginx/access.log && \ ln -sf /dev/stderr /var/log/nginx/error.log && \ - mkdir /var/log/sapl/ + mkdir /var/log/sapl/ && touch /var/interlegis/sapl/sapl.log && \ + ln -s /var/interlegis/sapl/sapl.log /var/log/sapl/sapl.log VOLUME ["/var/interlegis/sapl/data", "/var/interlegis/sapl/media"] diff --git a/MANIFEST.in b/MANIFEST.in index 3fe372d5d..7c730520d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include README.rst LICENSE.txt -recursive-include sapl *.html *.yaml +include sapl/webpack-stats.json +recursive-include sapl *.html *.yaml recursive-include sapl/static * recursive-include sapl/relatorios/templates *.py recursive-include sapl/compilacao *.sql diff --git a/README.rst b/README.rst index dd21aecf0..c2d47d643 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://travis-ci.org/interlegis/sapl.svg?branch=master +.. image:: https://travis-ci.org/interlegis/sapl.svg?branch=3.1.x :target: https://travis-ci.org/interlegis/sapl @@ -17,17 +17,17 @@ atual do sistema (2.5), visite a página do `projeto na Interlegis wiki `_ + `Instalação do Ambiente de Desenvolvimento `_ Instalação do Solr ====================== - `Instalação e configuração do Solr `_ + `Instalação e configuração do Solr `_ Instruções para Deploy ====================== - `Deploy SAPL com Nginx + Gunicorn `_ + `Deploy SAPL com Nginx + Gunicorn `_ Instruções para Importação da base mysql 2.5 @@ -37,19 +37,19 @@ Instruções para Importação da base mysql 2.5 Instruções para Tradução ======================== - `Instruções para Tradução `_ + `Instruções para Tradução `_ Orientações gerais de implementação =================================== - `Instruções para Implementação `_ + `Instruções para Implementação `_ Orientações gerais sobre o GitHub =================================== - `Instruções para GitHub `_ + `Instruções para GitHub `_ diff --git a/check_solr.sh b/check_solr.sh new file mode 100644 index 000000000..b3c4760c4 --- /dev/null +++ b/check_solr.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Pass the base SOLR URL as parameter, i.e., bash check_solr http://localhost:8983 + +SOLR_URL=$1 + +echo "Waiting for solr connection at $SOLR_URL ..." +while true; do + echo "$SOLR_URL/solr/admin/collections?action=LIST" + RESULT=$(curl -s -o /dev/null -I "$SOLR_URL/solr/admin/collections?action=LIST" -w '%{http_code}') + echo $RESULT + if [ "$RESULT" -eq '200' ]; then + echo "Solr server is up!" + break + else + sleep 3 + fi +done diff --git a/config/env-sample b/config/env-sample index bde081a92..aeb8c38c4 100644 --- a/config/env-sample +++ b/config/env-sample @@ -5,4 +5,5 @@ EMAIL_USE_TLS = True EMAIL_PORT = 587 EMAIL_HOST = '' EMAIL_HOST_USER = '' +EMAIL_SEND_USER = '' EMAIL_HOST_PASSWORD = '' diff --git a/config/env_dockerfile b/config/env_dockerfile index c83fc88f1..134beb274 100644 --- a/config/env_dockerfile +++ b/config/env_dockerfile @@ -5,4 +5,5 @@ EMAIL_USE_TLS = True EMAIL_PORT = 587 EMAIL_HOST = '' EMAIL_HOST_USER = '' +EMAIL_SEND_USER = '' EMAIL_HOST_PASSWORD = '' diff --git a/docker-compose.yml b/docker-compose.yml index ac3425c9f..e6d7bd6d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,16 +11,17 @@ sapldb: ports: - "5432:5432" sapl: - image: interlegis/sapl:3.1.137 + image: interlegis/sapl:3.1.145 restart: always environment: ADMIN_PASSWORD: interlegis ADMIN_EMAIL: email@dominio.net DEBUG: 'False' - USE_TLS: 'False' EMAIL_PORT: 587 + EMAIL_USE_TLS: 'False' EMAIL_HOST: smtp.dominio.net EMAIL_HOST_USER: usuariosmtp + EMAIL_SEND_USER: usuariosmtp EMAIL_HOST_PASSWORD: senhasmtp TZ: America/Sao_Paulo volumes: diff --git a/docs/instalacao31.rst b/docs/instalacao31.rst index e904f8d6c..a9752990f 100644 --- a/docs/instalacao31.rst +++ b/docs/instalacao31.rst @@ -28,15 +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 antiword default-jre python3-venv - - sudo -i - curl -sL https://deb.nodesource.com/setup_8.x | bash - - exit - sudo apt-get install nodejs - - sudo npm install npm -g - sudo npm install -g bower + python3-pip poppler-utils antiword default-jre python3-venv Instalar o virtualenv usando python 3 para o projeto. ----------------------------------------------------- @@ -147,11 +139,14 @@ Criação da `SECRET_KEY tmp2 mv tmp2 setup.py - sed -e s/$VERSION/$NEXT_VERSION/g sapl/templates/base.html > tmp3 mv tmp3 sapl/templates/base.html + + sed -e s/$VERSION/$NEXT_VERSION/g sapl/settings.py > tmp4 + mv tmp4 sapl/settings.py } function commit_and_push { echo "committing..." - git add docker-compose.yml setup.py sapl/templates/base.html + git add docker-compose.yml setup.py sapl/settings.py sapl/templates/base.html git commit -m "Release: $NEXT_VERSION" git tag $NEXT_VERSION diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 556c5d112..ad26b229a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,36 +1,36 @@ -dj-database-url==0.4.1 -django-haystack==2.6.0 -django>=1.10,<1.11 -git+git://github.com/rubgombar1/django-admin-bootstrapped.git -django-bootstrap3==7.0.1 -django-bower==5.2.0 +django>=1.11.19,<2.0 +django-haystack==2.8.1 +django-filter==2.0.0 +djangorestframework==3.9.0 +dj-database-url==0.5.0 django-braces==1.9.0 -django-compressor==2.0 -django-crispy-forms==1.6.1 -django-extensions==1.9.8 -django-extra-views==0.11.0 -django-filter==0.15.3 -django-floppyforms==1.6.2 -django-model-utils==3.1.1 -django-sass-processor==0.5.8 -djangorestframework==3.4.0 -git+git://github.com/jasperlittle/django-rest-framework-docs -easy-thumbnails==2.5 +django-crispy-forms==1.7.2 +django-floppyforms==1.7.0 +django-extra-views==0.12.0 +django-model-utils==3.1.2 +django-reversion==3.0.2 +django-reversion-compare==0.8.6 +django-speedinfo==1.4.0 +django-extensions==2.1.4 django-image-cropping==1.2 -git+git://github.com/interlegis/trml2pdf.git -libsass==0.11.1 -psycopg2-binary==2.7.4 -python-decouple==3.0 -pytz==2016.4 -pyyaml==3.11 -rtyaml==0.0.3 -textract==1.5.0 +django-webpack-loader==0.6.0 +drf-yasg==1.13.0 +easy-thumbnails==2.5 +python-decouple==3.1 +psycopg2-binary==2.7.6.1 +pyyaml==4.2b1 +pytz==2018.9 +rtyaml==0.0.5 +python-magic==0.4.15 unipath==1.1 +WeasyPrint==44 +gunicorn==19.9.0 + +textract==1.5.0 pysolr==3.6.0 -python-magic==0.4.12 -gunicorn==19.6.0 -django-reversion==2.0.8 -WeasyPrint==0.42 whoosh==2.7.4 -django-speedinfo==1.3.5 -django-reversion-compare==0.8.4 + +pyoai==2.5.0 + +git+git://github.com/interlegis/trml2pdf.git +git+git://github.com/interlegis/django-admin-bootstrapped diff --git a/sapl/.env_test b/sapl/.env_test index 9416d402d..616909348 100644 --- a/sapl/.env_test +++ b/sapl/.env_test @@ -5,4 +5,5 @@ EMAIL_USE_TLS = True EMAIL_PORT = 587 EMAIL_HOST = '' EMAIL_HOST_USER = '' +EMAIL_SEND_USER = '' EMAIL_HOST_PASSWORD = '' diff --git a/sapl/api/deprecated.py b/sapl/api/deprecated.py new file mode 100644 index 000000000..1b5fb84e5 --- /dev/null +++ b/sapl/api/deprecated.py @@ -0,0 +1,670 @@ + +import logging +import logging + +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from django.db.models import Q +from django.forms.fields import CharField, MultiValueField +from django.forms.widgets import MultiWidget, TextInput +from django.http import Http404 +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _ +from django_filters.filters import CharFilter, ModelChoiceFilter, DateFilter +from django_filters.rest_framework.backends import DjangoFilterBackend +from django_filters.rest_framework.filterset import FilterSet +from rest_framework import serializers +from rest_framework import serializers +from rest_framework.generics import ListAPIView +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.permissions import (IsAuthenticated, + IsAuthenticatedOrReadOnly, AllowAny) +from rest_framework.viewsets import GenericViewSet + +from sapl.api.serializers import ModelChoiceSerializer, AutorSerializer,\ + ChoiceSerializer +from sapl.base.models import TipoAutor, Autor, CasaLegislativa +from sapl.materia.models import MateriaLegislativa +from sapl.parlamentares.models import Legislatura +from sapl.sessao.models import SessaoPlenaria, OrdemDia +from sapl.utils import SaplGenericRelation +from sapl.utils import generic_relations_for_model + + +class SaplGenericRelationSearchFilterSet(FilterSet): + q = CharFilter(method='filter_q') + + def filter_q(self, queryset, name, value): + + query = value.split(' ') + if query: + q = Q() + for qtext in query: + if not qtext: + continue + q_fs = Q(nome__icontains=qtext) + + order_by = [] + + for gr in generic_relations_for_model(self._meta.model): + sgr = gr[1] + for item in sgr: + if item.related_model != self._meta.model: + + continue + flag_order_by = True + for field in item.fields_search: + if flag_order_by: + flag_order_by = False + order_by.append('%s__%s' % ( + item.related_query_name(), + field[0]) + ) + # 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 + + if q: + queryset = queryset.filter(q).order_by(*order_by) + + 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(CharFilter): + field_class = SearchForFieldField + + +class AutorChoiceFilterSet(SaplGenericRelationSearchFilterSet): + q = CharFilter(method='filter_q') + tipo = ModelChoiceFilter(queryset=TipoAutor.objects.all()) + + class Meta: + model = Autor + fields = ['q', + 'tipo', + 'nome', ] + + def filter_q(self, queryset, name, value): + return super().filter_q( + queryset, name, value).distinct('nome').order_by('nome') + + +class AutorSearchForFieldFilterSet(AutorChoiceFilterSet): + q = SearchForFieldFilter(method='filter_q') + + class Meta(AutorChoiceFilterSet.Meta): + pass + + def filter_q(self, queryset, name, 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') + + +class AutoresPossiveisFilterSet(FilterSet): + logger = logging.getLogger(__name__) + data_relativa = DateFilter(method='filter_data_relativa') + tipo = CharFilter(method='filter_tipo') + + class Meta: + model = Autor + fields = ['data_relativa', 'tipo', ] + + def filter_data_relativa(self, queryset, name, value): + return queryset + + def filter_tipo(self, queryset, name, value): + + try: + self.logger.debug( + "Tentando obter TipoAutor correspondente à pk {}.".format(value)) + tipo = TipoAutor.objects.get(pk=value) + except: + self.logger.error("TipoAutor(pk={}) inexistente.".format(value)) + raise serializers.ValidationError(_('Tipo de Autor inexistente.')) + + qs = queryset.filter(tipo=tipo) + + return qs + + @property + def qs(self): + qs = super().qs + + data_relativa = self.form.cleaned_data['data_relativa'] \ + if 'data_relativa' in self.form.cleaned_data else None + + tipo = self.form.cleaned_data['tipo'] \ + if 'tipo' in self.form.cleaned_data else None + + if not tipo: + return qs + + tipo = TipoAutor.objects.get(pk=tipo) + if not tipo.content_type: + return qs + + filter_for_model = 'filter_%s' % tipo.content_type.model + + if not hasattr(self, filter_for_model): + return qs + + if not data_relativa: + data_relativa = timezone.now() + + return getattr(self, filter_for_model)(qs, data_relativa).distinct() + + def filter_parlamentar(self, queryset, data_relativa): + # não leva em conta afastamentos + legislatura_relativa = Legislatura.objects.filter( + data_inicio__lte=data_relativa, + data_fim__gte=data_relativa).first() + + q = Q( + parlamentar_set__mandato__data_inicio_mandato__lte=data_relativa, + parlamentar_set__mandato__data_fim_mandato__isnull=True) | Q( + parlamentar_set__mandato__data_inicio_mandato__lte=data_relativa, + parlamentar_set__mandato__data_fim_mandato__gte=data_relativa) + + if legislatura_relativa.atual(): + q = q & Q(parlamentar_set__ativo=True) + + return queryset.filter(q) + + def filter_comissao(self, queryset, data_relativa): + return queryset.filter( + Q(comissao_set__data_extincao__isnull=True, + comissao_set__data_fim_comissao__isnull=True) | + Q(comissao_set__data_extincao__gte=data_relativa, + comissao_set__data_fim_comissao__isnull=True) | + Q(comissao_set__data_extincao__gte=data_relativa, + comissao_set__data_fim_comissao__isnull=True) | + Q(comissao_set__data_extincao__isnull=True, + comissao_set__data_fim_comissao__gte=data_relativa) | + Q(comissao_set__data_extincao__gte=data_relativa, + comissao_set__data_fim_comissao__gte=data_relativa), + comissao_set__data_criacao__lte=data_relativa) + + def filter_frente(self, queryset, data_relativa): + return queryset.filter( + Q(frente_set__data_extincao__isnull=True) | + Q(frente_set__data_extincao__gte=data_relativa), + frente_set__data_criacao__lte=data_relativa) + + def filter_bancada(self, queryset, data_relativa): + return queryset.filter( + Q(bancada_set__data_extincao__isnull=True) | + Q(bancada_set__data_extincao__gte=data_relativa), + bancada_set__data_criacao__lte=data_relativa) + + def filter_bloco(self, queryset, data_relativa): + return queryset.filter( + Q(bloco_set__data_extincao__isnull=True) | + Q(bloco_set__data_extincao__gte=data_relativa), + bloco_set__data_criacao__lte=data_relativa) + + def filter_orgao(self, queryset, data_relativa): + # na implementação, não havia regras a implementar para orgao + return queryset + + +class AutorChoiceSerializer(ModelChoiceSerializer): + + def get_text(self, obj): + return obj.nome + + class Meta: + model = Autor + fields = ['id', 'nome'] + + +class MateriaLegislativaOldSerializer(serializers.ModelSerializer): + + class Meta: + model = MateriaLegislativa + fields = '__all__' + + +class SessaoPlenariaOldSerializer(serializers.ModelSerializer): + + codReuniao = serializers.SerializerMethodField('get_pk_sessao') + codReuniaoPrincipal = serializers.SerializerMethodField('get_pk_sessao') + txtTituloReuniao = serializers.SerializerMethodField('get_name') + txtSiglaOrgao = serializers.SerializerMethodField('get_sigla_orgao') + txtApelido = serializers.SerializerMethodField('get_name') + txtNomeOrgao = serializers.SerializerMethodField('get_nome_orgao') + codEstadoReuniao = serializers.SerializerMethodField( + 'get_estadoSessaoPlenaria') + txtTipoReuniao = serializers.SerializerMethodField('get_tipo_sessao') + txtObjeto = serializers.SerializerMethodField('get_assunto_sessao') + txtLocal = serializers.SerializerMethodField('get_endereco_orgao') + bolReuniaoConjunta = serializers.SerializerMethodField( + 'get_reuniao_conjunta') + bolHabilitarEventoInterativo = serializers.SerializerMethodField( + 'get_iterativo') + idYoutube = serializers.SerializerMethodField('get_url') + codEstadoTransmissaoYoutube = serializers.SerializerMethodField( + 'get_estadoTransmissaoYoutube') + datReuniaoString = serializers.SerializerMethodField('get_date') + + # Constantes SessaoPlenaria (de 1-9) (apenas 3 serão usados) + SESSAO_FINALIZADA = 4 + SESSAO_EM_ANDAMENTO = 3 + SESSAO_CONVOCADA = 2 + + # Constantes EstadoTranmissaoYoutube (de 0 a 2) + TRANSMISSAO_ENCERRADA = 2 + TRANSMISSAO_EM_ANDAMENTO = 1 + SEM_TRANSMISSAO = 0 + + class Meta: + model = SessaoPlenaria + fields = ( + 'codReuniao', + 'codReuniaoPrincipal', + 'txtTituloReuniao', + 'txtSiglaOrgao', + 'txtApelido', + 'txtNomeOrgao', + 'codEstadoReuniao', + 'txtTipoReuniao', + 'txtObjeto', + 'txtLocal', + 'bolReuniaoConjunta', + 'bolHabilitarEventoInterativo', + 'idYoutube', + 'codEstadoTransmissaoYoutube', + 'datReuniaoString' + ) + + def __init__(self, *args, **kwargs): + super(SessaoPlenariaOldSerializer, self).__init__(args, kwargs) + + def get_pk_sessao(self, obj): + return obj.pk + + def get_name(self, obj): + return obj.__str__() + + def get_estadoSessaoPlenaria(self, obj): + if obj.finalizada: + return self.SESSAO_FINALIZADA + elif obj.iniciada: + return self.SESSAO_EM_ANDAMENTO + else: + return self.SESSAO_CONVOCADA + + def get_tipo_sessao(self, obj): + return obj.tipo.__str__() + + def get_url(self, obj): + return obj.url_video if obj.url_video else None + + def get_iterativo(self, obj): + return obj.interativa if obj.interativa else False + + def get_date(self, obj): + return "{} {}{}".format( + obj.data_inicio.strftime("%d/%m/%Y"), + obj.hora_inicio, + ":00" + ) + + def get_estadoTransmissaoYoutube(self, obj): + if obj.url_video: + if obj.finalizada: + return self.TRANSMISSAO_ENCERRADA + else: + return self.TRANSMISSAO_EM_ANDAMENTO + else: + return self.SEM_TRANSMISSAO + + def get_assunto_sessao(self, obj): + pauta_sessao = '' + ordem_dia = OrdemDia.objects.filter(sessao_plenaria=obj.pk) + pauta_sessao = ', '.join([i.materia.__str__() for i in ordem_dia]) + + return str(pauta_sessao) + + def get_endereco_orgao(self, obj): + return self.casa().endereco + + def get_reuniao_conjunta(self, obj): + return False + + def get_sigla_orgao(self, obj): + return self.casa().sigla + + def get_nome_orgao(self, obj): + return self.casa().nome + + def casa(self): + casa = CasaLegislativa.objects.first() + return casa + + +class ModelChoiceView(ListAPIView): + """ + Deprecated + + TODO Migrar para customização na api automática + + """ + + # FIXME aplicar permissão correta de usuário + permission_classes = (IsAuthenticated,) + serializer_class = ModelChoiceSerializer + + def get(self, request, *args, **kwargs): + self.model = ContentType.objects.get_for_id( + self.kwargs['content_type']).model_class() + + pagination = request.GET.get('pagination', '') + + if pagination == 'False': + self.pagination_class = None + + return ListAPIView.get(self, request, *args, **kwargs) + + def get_queryset(self): + return self.model.objects.all() + + +class AutorListView(ListAPIView): + """ + Deprecated + + TODO Migrar para customização na api automática + + Listagem de Autores com filtro para autores já cadastrados + e/ou possíveis autores. + + - tr - tipo do resultado + Prepera Lista de Autores para 2 cenários distintos + + - default = 1 + + = 1 -> para (value, text) usados geralmente + em combobox, radiobox, checkbox, etc com pesquisa básica + de Autores feita pelo django-filter + -> processo usado nas pesquisas, o mais usado. + + + = 3 -> Devolve instancias da classe Autor filtradas pelo + django-filter + + - tipo - chave primária do Tipo de Autor a ser filtrado + + - q - busca textual no nome do Autor ou em fields_search + declarados no field SaplGenericRelation das GenericFks + A busca textual acontece via django-filter com a + variável `tr` igual 1 ou 3. Em caso contrário, + o django-filter é desativado e a busca é feita + no model do ContentType associado ao tipo. + + - q_0 / q_1 - q_0 é opcional e quando usado, 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_parlamentar__icontains, + parlamentar_set__ativo + &q_1=Carvalho,False + /api/autor?tr=1 + &q_0=parlamentar_set__nome_parlamentar__icontains, + parlamentar_set__ativo + &q_1=Carvalho,True + /api/autor?tr=3 + &q_0=parlamentar_set__nome_parlamentar__icontains, + parlamentar_set__ativo + &q_1=Carvalho,False + /api/autor?tr=3 + &q_0=parlamentar_set__nome_parlamentar__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 + """ + logger = logging.getLogger(__name__) + + TR_AUTOR_CHOICE_SERIALIZER = 1 + TR_AUTOR_SERIALIZER = 3 + + permission_classes = (IsAuthenticatedOrReadOnly,) + queryset = Autor.objects.all() + model = Autor + + filter_class = AutorChoiceFilterSet + filter_backends = (DjangoFilterBackend, ) + serializer_class = AutorChoiceSerializer + + @property + def tr(self): + username = self.request.user.username + try: + tr = int(self.request.GET.get + ('tr', AutorListView.TR_AUTOR_CHOICE_SERIALIZER)) + + if tr not in (AutorListView.TR_AUTOR_CHOICE_SERIALIZER, + AutorListView.TR_AUTOR_SERIALIZER): + return AutorListView.TR_AUTOR_CHOICE_SERIALIZER + except Exception as e: + self.logger.error('user=' + username + '. ' + str(e)) + return AutorListView.TR_AUTOR_CHOICE_SERIALIZER + return tr + + def get(self, request, *args, **kwargs): + if self.tr == AutorListView.TR_AUTOR_SERIALIZER: + 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) + + +class AutoresProvaveisListView(ListAPIView): + """ + Deprecated + + TODO Migrar para customização na api automática + """ + + logger = logging.getLogger(__name__) + + permission_classes = (IsAuthenticatedOrReadOnly,) + queryset = Autor.objects.all() + model = Autor + + filter_class = None + filter_backends = [] + serializer_class = ChoiceSerializer + + def get_queryset(self): + + params = {'content_type__isnull': False} + username = self.request.user.username + tipo = '' + try: + tipo = int(self.request.GET.get('tipo', '')) + if tipo: + params['id'] = tipo + except Exception as e: + self.logger.error('user= ' + username + '. ' + str(e)) + pass + + tipos = TipoAutor.objects.filter(**params) + + if not tipos.exists() and tipo: + raise Http404() + + r = [] + for tipo in tipos: + q = self.request.GET.get('q', '').strip() + + model_class = tipo.content_type.model_class() + + fields = list(filter( + lambda field: isinstance(field, SaplGenericRelation) and + field.related_model == Autor, + model_class._meta.get_fields(include_hidden=True))) + + """ + fields - é um array de SaplGenericRelation que deve possuir o + atributo fields_search. Verifique na documentação da classe + a estrutura de fields_search. + """ + + assert len(fields) >= 1, (_( + 'Não foi encontrado em %(model)s um atributo do tipo ' + 'SaplGenericRelation que use o model %(model_autor)s') % { + 'model': model_class._meta.verbose_name, + 'model_autor': Autor._meta.verbose_name}) + + qs = model_class.objects.all() + + q_filter = Q() + if q: + for item in fields: + if item.related_model != Autor: + continue + q_fs = Q() + for field in item.fields_search: + q_fs = q_fs | Q(**{'%s%s' % ( + field[0], + field[1]): q}) + q_filter = q_filter & q_fs + + qs = qs.filter(q_filter).distinct( + fields[0].fields_search[0][0]).order_by( + fields[0].fields_search[0][0]) + else: + qs = qs.order_by(fields[0].fields_search[0][0]) + + qs = qs.values_list( + 'id', fields[0].fields_search[0][0]) + r += list(qs) + + if tipos.count() > 1: + r.sort(key=lambda x: x[1].upper()) + return r + + +class AutoresPossiveisListView(ListAPIView): + """ + Deprecated + + TODO Migrar para customização na api automática + """ + + permission_classes = (IsAuthenticatedOrReadOnly,) + queryset = Autor.objects.all() + model = Autor + + pagination_class = None + + filter_class = AutoresPossiveisFilterSet + serializer_class = AutorChoiceSerializer + + +class MateriaLegislativaViewSet(ListModelMixin, + RetrieveModelMixin, + GenericViewSet): + """ + Deprecated + + TODO Migrar para customização na api automática + """ + + permission_classes = (IsAuthenticated,) + serializer_class = MateriaLegislativaOldSerializer + queryset = MateriaLegislativa.objects.all() + filter_backends = (DjangoFilterBackend,) + filter_fields = ('numero', 'ano', 'tipo', ) + + +class SessaoPlenariaViewSet(ListModelMixin, + RetrieveModelMixin, + GenericViewSet): + """ + Deprecated + + TODO Migrar para customização na api automática + """ + + permission_classes = (AllowAny,) + serializer_class = SessaoPlenariaOldSerializer + queryset = SessaoPlenaria.objects.all() + filter_backends = (DjangoFilterBackend,) + filter_fields = ('data_inicio', 'data_fim', 'interativa') diff --git a/sapl/api/forms.py b/sapl/api/forms.py index c36a0c11f..7cb249ff3 100644 --- a/sapl/api/forms.py +++ b/sapl/api/forms.py @@ -1,233 +1,65 @@ -import logging +from django.db.models.fields.files import FileField +from django.template.defaultfilters import capfirst +import django_filters +from django_filters.filters import CharFilter, NumberFilter +from django_filters.rest_framework.filterset import FilterSet +from django_filters.utils import resolve_field +from sapl.sessao.models import SessaoPlenaria -from django.db.models import Q -from django.forms.fields import CharField, MultiValueField -from django.forms.widgets import MultiWidget, TextInput -from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ -from django_filters.filters import DateFilter, MethodFilter, ModelChoiceFilter -from rest_framework import serializers -from rest_framework.compat import django_filters -from rest_framework.filters import FilterSet -from sapl.base.models import Autor, TipoAutor -from sapl.parlamentares.models import Legislatura -from sapl.utils import generic_relations_for_model +class SaplFilterSetMixin(FilterSet): - -class SaplGenericRelationSearchFilterSet(FilterSet): - q = MethodFilter() - - def filter_q(self, queryset, value): - - query = value.split(' ') - if query: - q = Q() - for qtext in query: - if not qtext: - continue - q_fs = Q(nome__icontains=qtext) - - order_by = [] - - for gr in generic_relations_for_model(self._meta.model): - sgr = gr[1] - for item in sgr: - if item.related_model != self._meta.model: - - continue - flag_order_by = True - for field in item.fields_search: - if flag_order_by: - flag_order_by = False - order_by.append('%s__%s' % ( - item.related_query_name(), - field[0]) - ) - # 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 - - if q: - queryset = queryset.filter(q).order_by(*order_by) - - 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()) - - class Meta: - model = Autor - fields = ['q', - 'tipo', - 'nome', ] - - def filter_q(self, queryset, value): - return SaplGenericRelationSearchFilterSet.filter_q( - 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') - - -class AutoresPossiveisFilterSet(FilterSet): - logger = logging.getLogger(__name__) - data_relativa = DateFilter(method='filter_data_relativa') - tipo = MethodFilter() + o = CharFilter(method='filter_o') class Meta: - model = Autor - fields = ['data_relativa', 'tipo', ] - - def filter_data_relativa(self, queryset, name, value): - return queryset - - def filter_tipo(self, queryset, value): - + fields = '__all__' + filter_overrides = { + FileField: { + 'filter_class': django_filters.CharFilter, + 'extra': lambda f: { + 'lookup_expr': 'exact', + }, + }, + } + + def filter_o(self, queryset, name, value): try: - self.logger.debug("Tentando obter TipoAutor correspondente à pk {}.".format(value)) - tipo = TipoAutor.objects.get(pk=value) + return queryset.order_by( + *map(str.strip, value.split(','))) except: - self.logger.error("TipoAutor(pk={}) inexistente.".format(value)) - raise serializers.ValidationError(_('Tipo de Autor inexistente.')) - - qs = queryset.filter(tipo=tipo) - - return qs - - @property - def qs(self): - qs = super().qs - - data_relativa = self.form.cleaned_data['data_relativa'] \ - if 'data_relativa' in self.form.cleaned_data else None - - tipo = self.form.cleaned_data['tipo'] \ - if 'tipo' in self.form.cleaned_data else None - - if not tipo and not data_relativa: - return qs - - if tipo: - # não precisa de try except, já foi validado em filter_tipo - tipo = TipoAutor.objects.get(pk=tipo) - if not tipo.content_type: - return qs - - filter_for_model = 'filter_%s' % tipo.content_type.model - - if not hasattr(self, filter_for_model): - return qs - - if not data_relativa: - data_relativa = timezone.now() - - return getattr(self, filter_for_model)(qs, data_relativa).distinct() - - def filter_parlamentar(self, queryset, data_relativa): - # não leva em conta afastamentos - legislatura_relativa = Legislatura.objects.filter( - data_inicio__lte=data_relativa, - data_fim__gte=data_relativa).first() - - q = Q( - parlamentar_set__mandato__data_inicio_mandato__lte=data_relativa, - parlamentar_set__mandato__data_fim_mandato__isnull=True) | Q( - parlamentar_set__mandato__data_inicio_mandato__lte=data_relativa, - parlamentar_set__mandato__data_fim_mandato__gte=data_relativa) - - if legislatura_relativa.atual(): - q = q & Q(parlamentar_set__ativo=True) - - return queryset.filter(q) + return queryset + + @classmethod + def filter_for_field(cls, f, name, lookup_expr='exact'): + # Redefine método estático para ignorar filtro para + # fields que não possuam lookup_expr informado + f, lookup_type = resolve_field(f, lookup_expr) + + default = { + 'field_name': name, + 'label': capfirst(f.verbose_name), + 'lookup_expr': lookup_expr + } + + filter_class, params = cls.filter_for_lookup( + f, lookup_type) + default.update(params) + if filter_class is not None: + return filter_class(**default) + return None - def filter_comissao(self, queryset, data_relativa): - return queryset.filter( - Q(comissao_set__data_extincao__isnull=True, - comissao_set__data_fim_comissao__isnull=True) | - Q(comissao_set__data_extincao__gte=data_relativa, - comissao_set__data_fim_comissao__isnull=True) | - Q(comissao_set__data_extincao__gte=data_relativa, - comissao_set__data_fim_comissao__isnull=True) | - Q(comissao_set__data_extincao__isnull=True, - comissao_set__data_fim_comissao__gte=data_relativa) | - Q(comissao_set__data_extincao__gte=data_relativa, - comissao_set__data_fim_comissao__gte=data_relativa), - comissao_set__data_criacao__lte=data_relativa) - def filter_frente(self, queryset, data_relativa): - return queryset.filter( - Q(frente_set__data_extincao__isnull=True) | - Q(frente_set__data_extincao__gte=data_relativa), - frente_set__data_criacao__lte=data_relativa) +class SessaoPlenariaFilterSet(SaplFilterSetMixin): + year = NumberFilter(method='filter_year') + month = NumberFilter(method='filter_month') - def filter_bancada(self, queryset, data_relativa): - return queryset.filter( - Q(bancada_set__data_extincao__isnull=True) | - Q(bancada_set__data_extincao__gte=data_relativa), - bancada_set__data_criacao__lte=data_relativa) + class Meta(SaplFilterSetMixin.Meta): + model = SessaoPlenaria - def filter_bloco(self, queryset, data_relativa): - return queryset.filter( - Q(bloco_set__data_extincao__isnull=True) | - Q(bloco_set__data_extincao__gte=data_relativa), - bloco_set__data_criacao__lte=data_relativa) + def filter_year(self, queryset, name, value): + qs = queryset.filter(data_inicio__year=value) + return qs - def filter_orgao(self, queryset, data_relativa): - # na implementação, não havia regras a implementar para orgao - return queryset + def filter_month(self, queryset, name, value): + qs = queryset.filter(data_inicio__month=value) + return qs diff --git a/sapl/api/permissions.py b/sapl/api/permissions.py index 5e17d1fe1..b7df6c63a 100644 --- a/sapl/api/permissions.py +++ b/sapl/api/permissions.py @@ -1,7 +1,8 @@ from rest_framework.permissions import DjangoModelPermissions +from sapl.rules.map_rules import rules_patterns_public -class DjangoModelPermissions(DjangoModelPermissions): +class SaplModelPermissions(DjangoModelPermissions): perms_map = { 'GET': ['%(app_label)s.list_%(model_name)s', @@ -10,9 +11,43 @@ class DjangoModelPermissions(DjangoModelPermissions): '%(app_label)s.detail_%(model_name)s'], 'HEAD': ['%(app_label)s.list_%(model_name)s', '%(app_label)s.detail_%(model_name)s'], - 'POST': ['%(app_label)s.list_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], 'PUT': ['%(app_label)s.change_%(model_name)s'], 'PATCH': ['%(app_label)s.change_%(model_name)s'], 'DELETE': ['%(app_label)s.delete_%(model_name)s'], } + + def has_permission(self, request, view): + if getattr(view, '_ignore_model_permissions', False): + return True + + if hasattr(view, 'get_queryset'): + queryset = view.get_queryset() + else: + queryset = getattr(view, 'queryset', None) + + assert queryset is not None, ( + 'Cannot apply DjangoModelPermissions on a view that ' + 'does not set `.queryset` or have a `.get_queryset()` method.' + ) + + perms = self.get_required_permissions(request.method, queryset.model) + + key = '{}:{}'.format( + queryset.model._meta.app_label, + queryset.model._meta.model_name) + + if key in rules_patterns_public: + perms = set(perms) + perms_publicas = rules_patterns_public[key] + + private_perms = perms - perms_publicas + if not private_perms: + return True + + return ( + request.user and + (request.user.is_authenticated() or not self.authenticated_users_only) and + request.user.has_perms(perms) + ) diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index 9219bf0e4..a9ce737c1 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -1,8 +1,13 @@ +from django.conf import settings from rest_framework import serializers +from rest_framework.relations import StringRelatedField from sapl.base.models import Autor, CasaLegislativa -from sapl.materia.models import MateriaLegislativa -from sapl.sessao.models import OrdemDia, SessaoPlenaria + + +class IntRelatedField(StringRelatedField): + def to_representation(self, value): + return int(value) class ChoiceSerializer(serializers.Serializer): @@ -31,17 +36,10 @@ class ModelChoiceObjectRelatedField(serializers.RelatedField): return ModelChoiceSerializer(value).data -class AutorChoiceSerializer(ModelChoiceSerializer): - - def get_text(self, obj): - return obj.nome - - class Meta: - model = Autor - fields = ['id', 'nome'] - - class AutorSerializer(serializers.ModelSerializer): + # AutorSerializer sendo utilizado pelo gerador automático da api devidos aos + # critérios anotados em views.py + autor_related = ModelChoiceObjectRelatedField(read_only=True) class Meta: @@ -49,126 +47,12 @@ class AutorSerializer(serializers.ModelSerializer): fields = '__all__' -class MateriaLegislativaSerializer(serializers.ModelSerializer): - - class Meta: - model = MateriaLegislativa - fields = '__all__' +class CasaLegislativaSerializer(serializers.ModelSerializer): + version = serializers.SerializerMethodField() - -class SessaoPlenariaSerializer(serializers.ModelSerializer): - - codReuniao = serializers.SerializerMethodField('get_pk_sessao') - codReuniaoPrincipal = serializers.SerializerMethodField('get_pk_sessao') - txtTituloReuniao = serializers.SerializerMethodField('get_name') - txtSiglaOrgao = serializers.SerializerMethodField('get_sigla_orgao') - txtApelido = serializers.SerializerMethodField('get_name') - txtNomeOrgao = serializers.SerializerMethodField('get_nome_orgao') - codEstadoReuniao = serializers.SerializerMethodField( - 'get_estadoSessaoPlenaria') - txtTipoReuniao = serializers.SerializerMethodField('get_tipo_sessao') - txtObjeto = serializers.SerializerMethodField('get_assunto_sessao') - txtLocal = serializers.SerializerMethodField('get_endereco_orgao') - bolReuniaoConjunta = serializers.SerializerMethodField( - 'get_reuniao_conjunta') - bolHabilitarEventoInterativo = serializers.SerializerMethodField( - 'get_iterativo') - idYoutube = serializers.SerializerMethodField('get_url') - codEstadoTransmissaoYoutube = serializers.SerializerMethodField( - 'get_estadoTransmissaoYoutube') - datReuniaoString = serializers.SerializerMethodField('get_date') - - # Constantes SessaoPlenaria (de 1-9) (apenas 3 serão usados) - SESSAO_FINALIZADA = 4 - SESSAO_EM_ANDAMENTO = 3 - SESSAO_CONVOCADA = 2 - - # Constantes EstadoTranmissaoYoutube (de 0 a 2) - TRANSMISSAO_ENCERRADA = 2 - TRANSMISSAO_EM_ANDAMENTO = 1 - SEM_TRANSMISSAO = 0 + def get_version(self, obj): + return settings.SAPL_VERSION class Meta: - model = SessaoPlenaria - fields = ( - 'codReuniao', - 'codReuniaoPrincipal', - 'txtTituloReuniao', - 'txtSiglaOrgao', - 'txtApelido', - 'txtNomeOrgao', - 'codEstadoReuniao', - 'txtTipoReuniao', - 'txtObjeto', - 'txtLocal', - 'bolReuniaoConjunta', - 'bolHabilitarEventoInterativo', - 'idYoutube', - 'codEstadoTransmissaoYoutube', - 'datReuniaoString' - ) - - def __init__(self, *args, **kwargs): - super(SessaoPlenariaSerializer, self).__init__(args, kwargs) - - def get_pk_sessao(self, obj): - return obj.pk - - def get_name(self, obj): - return obj.__str__() - - def get_estadoSessaoPlenaria(self, obj): - if obj.finalizada: - return self.SESSAO_FINALIZADA - elif obj.iniciada: - return self.SESSAO_EM_ANDAMENTO - else: - return self.SESSAO_CONVOCADA - - def get_tipo_sessao(self, obj): - return obj.tipo.__str__() - - def get_url(self, obj): - return obj.url_video if obj.url_video else None - - def get_iterativo(self, obj): - return obj.interativa if obj.interativa else False - - def get_date(self, obj): - return "{} {}{}".format( - obj.data_inicio.strftime("%d/%m/%Y"), - obj.hora_inicio, - ":00" - ) - - def get_estadoTransmissaoYoutube(self, obj): - if obj.url_video: - if obj.finalizada: - return self.TRANSMISSAO_ENCERRADA - else: - return self.TRANSMISSAO_EM_ANDAMENTO - else: - return self.SEM_TRANSMISSAO - - def get_assunto_sessao(self, obj): - pauta_sessao = '' - ordem_dia = OrdemDia.objects.filter(sessao_plenaria=obj.pk) - pauta_sessao = ', '.join([i.materia.__str__() for i in ordem_dia]) - - return str(pauta_sessao) - - def get_endereco_orgao(self, obj): - return self.casa().endereco - - def get_reuniao_conjunta(self, obj): - return False - - def get_sigla_orgao(self, obj): - return self.casa().sigla - - def get_nome_orgao(self, obj): - return self.casa().nome - - def casa(self): - casa = CasaLegislativa.objects.first() - return casa + model = CasaLegislativa + fields = '__all__' diff --git a/sapl/api/urls.py b/sapl/api/urls.py index 1f19e9d1f..3e874add2 100644 --- a/sapl/api/urls.py +++ b/sapl/api/urls.py @@ -1,23 +1,56 @@ from django.conf import settings from django.conf.urls import include, url +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import permissions from rest_framework.routers import DefaultRouter -from sapl.api.views import (AutoresPossiveisListView, AutoresProvaveisListView, - AutorListView, MateriaLegislativaViewSet, - ModelChoiceView, SessaoPlenariaViewSet) +from sapl.api.deprecated import MateriaLegislativaViewSet, SessaoPlenariaViewSet,\ + AutoresProvaveisListView, AutoresPossiveisListView, AutorListView,\ + ModelChoiceView +from sapl.api.views import SaplSetViews from .apps import AppConfig + app_name = AppConfig.name router = DefaultRouter() -router.register(r'materia', MateriaLegislativaViewSet) +router.register(r'materia$', MateriaLegislativaViewSet) router.register(r'sessao-plenaria', SessaoPlenariaViewSet) + + +for app, built_sets in SaplSetViews.items(): + for view_prefix, viewset in built_sets.items(): + router.register(app + '/' + view_prefix, viewset) + + urlpatterns_router = router.urls -urlpatterns_api = [ +schema_view = get_schema_view( + openapi.Info( + title="Sapl API - docs", + default_version='v1', + description="Sapl API - Docs - Configuração Básica", + ), + url=settings.SITE_URL, + public=True, + permission_classes=(permissions.AllowAny,), +) + +urlpatterns_api_doc = [ + url(r'^docs/swagger(?P\.json|\.yaml)$', + schema_view.without_ui(cache_timeout=0), name='schema-json'), + url(r'^docs/swagger/$', + schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + url(r'^docs/redoc/$', + schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), +] + +# TODO: refatorar para customização da api automática +deprecated_urlpatterns_api = [ url(r'^autor/provaveis', AutoresProvaveisListView.as_view(), name='autores_provaveis_list'), url(r'^autor/possiveis', @@ -28,13 +61,16 @@ urlpatterns_api = [ url(r'^model/(?P\d+)/(?P\d*)$', ModelChoiceView.as_view(), name='model_list'), -] -if settings.DEBUG: - urlpatterns_api += [ - url(r'^docs', include('rest_framework_docs.urls')), ] +] urlpatterns = [ - url(r'^api/', include(urlpatterns_api)), - url(r'^api/', include(urlpatterns_router)) + url(r'^api/', include(deprecated_urlpatterns_api)), + url(r'^api/', include(urlpatterns_api_doc)), + url(r'^api/', include(urlpatterns_router)), + + + # implementar caminho para autenticação + # https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/ + # url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework')), ] diff --git a/sapl/api/views.py b/sapl/api/views.py index b8cafc1dd..769196c76 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -1,278 +1,437 @@ import logging + +from django import apps +from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.db.models import Q -from django.http import Http404 +from django.db.models.fields.files import FileField +from django.utils.decorators import classonlymethod +from django.utils.text import capfirst from django.utils.translation import ugettext_lazy as _ -from rest_framework.filters import DjangoFilterBackend -from rest_framework.generics import ListAPIView -from rest_framework.mixins import ListModelMixin, RetrieveModelMixin -from rest_framework.permissions import (AllowAny, IsAuthenticated, - IsAuthenticatedOrReadOnly) -from rest_framework.viewsets import GenericViewSet +import django_filters +from django_filters.filters import CharFilter +from django_filters.rest_framework.backends import DjangoFilterBackend +from django_filters.rest_framework.filterset import FilterSet +from django_filters.utils import resolve_field +from rest_framework import serializers as rest_serializers +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet -from sapl.api.forms import (AutorChoiceFilterSet, AutoresPossiveisFilterSet, - AutorSearchForFieldFilterSet) -from sapl.api.serializers import (AutorChoiceSerializer, AutorSerializer, - ChoiceSerializer, - MateriaLegislativaSerializer, - ModelChoiceSerializer, - SessaoPlenariaSerializer) -from sapl.base.models import Autor, TipoAutor -from sapl.materia.models import MateriaLegislativa -from sapl.sessao.models import SessaoPlenaria -from sapl.utils import SaplGenericRelation +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.parlamentares.models import Parlamentar +from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria -class ModelChoiceView(ListAPIView): +class BusinessRulesNotImplementedMixin: + def create(self, request, *args, **kwargs): + raise Exception(_("POST Create não implementado")) - # FIXME aplicar permissão correta de usuário - permission_classes = (IsAuthenticated,) - serializer_class = ModelChoiceSerializer + def put(self, request, *args, **kwargs): + raise Exception(_("PUT Update não implementado")) - def get(self, request, *args, **kwargs): - self.model = ContentType.objects.get_for_id( - self.kwargs['content_type']).model_class() + def patch(self, request, *args, **kwargs): + raise Exception(_("PATCH Partial Update não implementado")) - pagination = request.GET.get('pagination', '') + def delete(self, request, *args, **kwargs): + raise Exception(_("DELETE Delete não implementado")) - if pagination == 'False': - self.pagination_class = None - return ListAPIView.get(self, request, *args, **kwargs) +class SaplApiViewSetConstrutor(ModelViewSet): - def get_queryset(self): - return self.model.objects.all() + filter_backends = (DjangoFilterBackend,) + + @classonlymethod + def build_class(cls): + import inspect + from sapl.api import serializers + + # Carrega todas as classes de sapl.api.serializers que possuam + # "Serializer" como Sufixo. + serializers_classes = inspect.getmembers(serializers) + serializers_classes = {i[0]: i[1] for i in filter( + lambda x: x[0].endswith('Serializer'), + serializers_classes + )} + + # Carrega todas as classes de sapl.api.forms que possuam + # "FilterSet" como Sufixo. + from sapl.api import forms + filters_classes = inspect.getmembers(forms) + filters_classes = {i[0]: i[1] for i in filter( + lambda x: x[0].endswith('FilterSet'), + filters_classes + )} + + built_sets = {} + + def build(_model): + object_name = _model._meta.object_name + + # Caso Exista, pega a classe sapl.api.serializers.{model}Serializer + serializer_name = '{model}Serializer'.format(model=object_name) + _serializer_class = serializers_classes.get(serializer_name, None) + + # Caso Exista, pega a classe sapl.api.forms.{model}FilterSet + filter_name = '{model}FilterSet'.format(model=object_name) + _filter_class = filters_classes.get(filter_name, None) + + def create_class(): + # Define uma classe padrão para serializer caso não tenha sido + # criada a classe sapl.api.serializers.{model}Serializer + class SaplSerializer(rest_serializers.ModelSerializer): + class Meta: + model = _model + fields = '__all__' + + # Define uma classe padrão para filtro caso não tenha sido + # criada a classe sapl.api.forms.{model}FilterSet + class SaplFilterSet(SaplFilterSetMixin): + class Meta(SaplFilterSetMixin.Meta): + model = _model + + # Define uma classe padrão ModelViewSet de DRF + class ModelSaplViewSet(cls): + queryset = _model.objects.all() + + # Utiliza o filtro customizado pela classe + # sapl.api.forms.{model}FilterSet + # ou utiliza o trivial SaplFilterSet definido acima + filter_class = _filter_class \ + if _filter_class else SaplFilterSet + + # Utiliza o serializer customizado pela classe + # sapl.api.serializers.{model}Serializer + # ou utiliza o trivial SaplSerializer definido acima + serializer_class = _serializer_class \ + if _serializer_class else SaplSerializer + + return ModelSaplViewSet + + viewset = create_class() + viewset.__name__ = '%sModelSaplViewSet' % _model.__name__ + return viewset + + apps_sapl = [apps.apps.get_app_config( + n[5:]) for n in settings.SAPL_APPS] + for app in apps_sapl: + built_sets[app.label] = {} + for model in app.get_models(): + built_sets[app.label][model._meta.model_name] = build(model) + + return built_sets + + +""" +1. Constroi uma rest_framework.viewsets.ModelViewSet para + todos os models de todas as apps do sapl +2. Define DjangoFilterBackend como ferramenta de filtro dos campos +3. Define Serializer como a seguir: + 3.1 - Define um Serializer genérico para cada módel + 3.2 - Recupera Serializer customizado em sapl.api.serializers + 3.3 - Para todo model é opcional a existência de + sapl.api.serializers.{model}Serializer. + Caso não seja definido um Serializer customizado, utiliza-se o trivial +4. Define um FilterSet como a seguir: + 4.1 - Define um FilterSet genérico para cada módel + 4.2 - Recupera FilterSet customizado em sapl.api.forms + 4.3 - Para todo model é opcional a existência de + sapl.api.forms.{model}FilterSet. + Caso não seja definido um FilterSet customizado, utiliza-se o trivial + 4.4 - todos os campos que aceitam lookup 'exact' + podem ser filtrados por default + +5. SaplApiViewSetConstrutor não cria padrões e/ou exige conhecimento alem dos + exigidos pela DRF. + +6. As rotas são criadas seguindo nome da app e nome do model + http://localhost:9000/api/{applabel}/{model_name}/ + e seguem as variações definidas em: + https://www.django-rest-framework.org/api-guide/routers/#defaultrouter + +7. Todas as viewsets construídas por SaplApiViewSetConstrutor e suas rotas + (paginate list, detail, edit, create, delete) + bem como testes em ambiente de desenvolvimento podem ser conferidas em: + http://localhost:9000/api/ + desde que settings.DEBUG=True + +**SaplSetViews** é um dict de dicts de models conforme: + { + ... + + 'audiencia': { + 'tipoaudienciapublica': TipoAudienciaPublicaViewSet, + 'audienciapublica': AudienciaPublicaViewSet, + 'anexoaudienciapublica': AnexoAudienciaPublicaViewSet + + ... + + }, + + ... + + 'base': { + 'casalegislativa': CasaLegislativaViewSet, + 'appconfig': AppConfigViewSet, + + ... + + } + + ... + + } +""" +SaplSetViews = SaplApiViewSetConstrutor.build_class() -class AutorListView(ListAPIView): +# Toda Classe construida acima, pode ser redefinida e aplicado quaisquer +# das possibilidades para uma classe normal criada a partir de +# rest_framework.viewsets.ModelViewSet conforme exemplo para a classe autor + + +# Customização para AutorViewSet com implementação de actions específicas +class _AutorViewSet(SaplSetViews['base']['autor']): """ - Listagem de Autores com filtro para autores já cadastrados - e/ou possíveis autores. - - - tr - tipo do resultado - Prepera Lista de Autores para 3 cenários distintos - - - default = 1 - - = 1 -> para (value, text) usados geralmente - em combobox, radiobox, checkbox, etc com pesquisa básica - de Autores feita pelo django-filter - -> processo usado nas pesquisas, o mais usado. - - - = 3 -> Devolve instancias da classe Autor filtradas pelo - django-filter - - - tipo - chave primária do Tipo de Autor a ser filtrado - - - q - busca textual no nome do Autor ou em fields_search - declarados no field SaplGenericRelation das GenericFks - A busca textual acontece via django-filter com a - variável `tr` igual 1 ou 3. Em caso contrário, - o django-filter é desativado e a busca é feita - no model do ContentType associado ao tipo. - - - q_0 / q_1 - q_0 é opcional e quando usado, 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_parlamentar__icontains, - parlamentar_set__ativo - &q_1=Carvalho,False - /api/autor?tr=1 - &q_0=parlamentar_set__nome_parlamentar__icontains, - parlamentar_set__ativo - &q_1=Carvalho,True - /api/autor?tr=3 - &q_0=parlamentar_set__nome_parlamentar__icontains, - parlamentar_set__ativo - &q_1=Carvalho,False - /api/autor?tr=3 - &q_0=parlamentar_set__nome_parlamentar__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 + Neste exemplo de customização do que foi criado em + SaplApiViewSetConstrutor além do ofertado por + rest_framework.viewsets.ModelViewSet, dentre outras customizações + possíveis, foi adicionado as rotas referentes aos relacionamentos genéricos + + * padrão de ModelViewSet + /api/base/autor/ POST - create + /api/base/autor/ GET - list + /api/base/autor/{pk}/ GET - detail + /api/base/autor/{pk}/ PUT - update + /api/base/autor/{pk}/ PATCH - partial_update + /api/base/autor/{pk}/ DELETE - destroy + + * rotas desta classe local: + /api/base/autor/parlamentar + devolve apenas autores que são parlamentares + /api/base/autor/comissao + devolve apenas autores que são comissões + /api/base/autor/bloco + devolve apenas autores que são blocos parlamentares + /api/base/autor/bancada + devolve apenas autores que são bancadas parlamentares + /api/base/autor/frente + devolve apenas autores que são Frene parlamentares + /api/base/autor/orgao + devolve apenas autores que são Órgãos + + """ - logger = logging.getLogger(__name__) - TR_AUTOR_CHOICE_SERIALIZER = 1 - TR_AUTOR_SERIALIZER = 3 + def list_for_content_type(self, content_type): + qs = self.get_queryset() + qs = qs.filter(content_type=content_type) - permission_classes = (IsAuthenticatedOrReadOnly,) - queryset = Autor.objects.all() - model = Autor + page = self.paginate_queryset(qs) + if page is not None: + serializer = self.serializer_class(page, many=True) + return self.get_paginated_response(serializer.data) - filter_class = AutorChoiceFilterSet - filter_backends = (DjangoFilterBackend, ) - serializer_class = AutorChoiceSerializer + serializer = self.get_serializer(page, many=True) + return Response(serializer.data) - @property - def tr(self): - username = self.request.user.username - try: - tr = int(self.request.GET.get - ('tr', AutorListView.TR_AUTOR_CHOICE_SERIALIZER)) + @classonlymethod + def build_class_with_actions(cls): - if tr not in (AutorListView.TR_AUTOR_CHOICE_SERIALIZER, - AutorListView.TR_AUTOR_SERIALIZER): - return AutorListView.TR_AUTOR_CHOICE_SERIALIZER - except Exception as e: - self.logger.error('user=' + username + '. ' + str(e)) - return AutorListView.TR_AUTOR_CHOICE_SERIALIZER - return tr + models_with_gr_for_autor = models_with_gr_for_model(Autor) - def get(self, request, *args, **kwargs): - if self.tr == AutorListView.TR_AUTOR_SERIALIZER: - self.serializer_class = AutorSerializer - self.permission_classes = (IsAuthenticated,) + for _model in models_with_gr_for_autor: - if self.filter_class and 'q_0' in request.GET: - self.filter_class = AutorSearchForFieldFilterSet + @action(detail=False, name=_model._meta.model_name) + def actionclass(self, request, *args, **kwargs): + model = getattr(self, self.action)._AutorViewSet__model - return ListAPIView.get(self, request, *args, **kwargs) + content_type = ContentType.objects.get_for_model(model) + return self.list_for_content_type(content_type) + func = actionclass + func.mapping['get'] = func.kwargs['name'] + func.url_name = func.kwargs['name'] + func.url_path = func.kwargs['name'] + func.__model = _model -class AutoresProvaveisListView(ListAPIView): - logger = logging.getLogger(__name__) + setattr(cls, _model._meta.model_name, func) + return cls - permission_classes = (IsAuthenticatedOrReadOnly,) - queryset = Autor.objects.all() - model = Autor - filter_class = None - filter_backends = [] - serializer_class = ChoiceSerializer +class _ParlamentarViewSet(SaplSetViews['parlamentares']['parlamentar']): + @action(detail=True) + def proposicoes(self, request, *args, **kwargs): + """ + Lista de proposições públicas de parlamentar específico - def get_queryset(self): - - params = {'content_type__isnull': False} - username = self.request.user.username - tipo = '' - try: - tipo = int(self.request.GET.get('tipo', '')) - if tipo: - params['id'] = tipo - except Exception as e: - self.logger.error('user= ' + username + '. ' + str(e)) - pass - - tipos = TipoAutor.objects.filter(**params) - - if not tipos.exists() and tipo: - raise Http404() - - r = [] - for tipo in tipos: - q = self.request.GET.get('q', '').strip() - - model_class = tipo.content_type.model_class() - - fields = list(filter( - lambda field: isinstance(field, SaplGenericRelation) and - field.related_model == Autor, - model_class._meta.get_fields(include_hidden=True))) - - """ - fields - é um array de SaplGenericRelation que deve possuir o - atributo fields_search. Verifique na documentação da classe - a estrutura de fields_search. - """ - - assert len(fields) >= 1, (_( - 'Não foi encontrado em %(model)s um atributo do tipo ' - 'SaplGenericRelation que use o model %(model_autor)s') % { - 'model': model_class._meta.verbose_name, - 'model_autor': Autor._meta.verbose_name}) - - qs = model_class.objects.all() - - q_filter = Q() - if q: - for item in fields: - if item.related_model != Autor: - continue - q_fs = Q() - for field in item.fields_search: - q_fs = q_fs | Q(**{'%s%s' % ( - field[0], - field[1]): q}) - q_filter = q_filter & q_fs - - qs = qs.filter(q_filter).distinct( - fields[0].fields_search[0][0]).order_by( - fields[0].fields_search[0][0]) + :param int id: - Identificador do parlamentar que se quer recuperar as proposições + :return: uma lista de proposições + """ + # /api/parlamentares/parlamentar/{id}/proposicoes/ + # recupera proposições enviadas e incorporadas do parlamentar + # deve coincidir com + # /parlamentar/{pk}/proposicao + content_type = ContentType.objects.get_for_model(Parlamentar) + + qs = Proposicao.objects.filter( + data_envio__isnull=False, + data_recebimento__isnull=False, + cancelado=False, + autor__object_id=kwargs['pk'], + autor__content_type=content_type + ) + + page = self.paginate_queryset(qs) + if page is not None: + serializer = SaplSetViews[ + 'materia']['proposicao'].serializer_class(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(page, many=True) + return Response(serializer.data) + + +class _ProposicaoViewSet(SaplSetViews['materia']['proposicao']): + """ + list: + Retorna lista de Proposições + + * Permissões: + + * Usuário Dono: + * Pode listar todas suas Proposições + + * Usuário Conectado ou Anônimo: + * Pode listar todas as Proposições incorporadas + + retrieve: + Retorna uma proposição passada pelo 'id' + + * Permissões: + + * Usuário Dono: + * Pode recuperar qualquer de suas Proposições + + * Usuário Conectado ou Anônimo: + * Pode recuperar qualquer das proposições incorporadas + + """ + class ProposicaoPermission(SaplModelPermissions): + def has_permission(self, request, view): + if request.method == 'GET': + return True + # se a solicitação é list ou detail, libera o teste de permissão + # e deixa o get_queryset filtrar de acordo com a regra de + # visibilidade das proposições, ou seja: + # 1. proposição incorporada é proposição pública + # 2. não incorporada só o autor pode ver else: - qs = qs.order_by(fields[0].fields_search[0][0]) + perm = super().has_permission(request, view) + return perm + # não é list ou detail, então passa pelas regras de permissão e, + # depois disso ainda passa pelo filtro de get_queryset - qs = qs.values_list( - 'id', fields[0].fields_search[0][0]) - r += list(qs) + permission_classes = (ProposicaoPermission, ) - if tipos.count() > 1: - r.sort(key=lambda x: x[1].upper()) - return r + def get_queryset(self): + qs = super().get_queryset() + q = Q(data_recebimento__isnull=False, object_id__isnull=False) + if not self.request.user.is_anonymous(): + q |= Q(autor__user=self.request.user) -class AutoresPossiveisListView(ListAPIView): + qs = qs.filter(q) + return qs - permission_classes = (IsAuthenticatedOrReadOnly,) - queryset = Autor.objects.all() - model = Autor - pagination_class = None +class _DocumentoAdministrativoViewSet(SaplSetViews['protocoloadm']['documentoadministrativo']): - filter_class = AutoresPossiveisFilterSet - serializer_class = AutorChoiceSerializer + class DocumentoAdministrativoPermission(SaplModelPermissions): + def has_permission(self, request, view): + if request.method == 'GET': + comportamento = AppConfig.attr('documentos_administrativos') + if comportamento == DOC_ADM_OSTENSIVO: + return True + """ + Diante da lógica implementada na manutenção de documentos + administrativos: + - Se o comportamento é doc adm ostensivo, deve passar pelo + teste de permissões sem avaliá-las + - se o comportamento é doc adm restritivo, deve passar pelo + teste de permissões avaliando-as + """ + return super().has_permission(request, view) + permission_classes = (DocumentoAdministrativoPermission, ) -class MateriaLegislativaViewSet(ListModelMixin, - RetrieveModelMixin, - GenericViewSet): + def get_queryset(self): + """ + mesmo tendo passado pelo teste de permissões, deve ser filtrado, + pelo campo restrito. Sendo este igual a True, disponibilizar apenas + a um usuário conectado. Apenas isso, sem critérios outros de permissão, + conforme implementado em DocumentoAdministrativoCrud + """ + qs = super().get_queryset() - permission_classes = (IsAuthenticated,) - serializer_class = MateriaLegislativaSerializer - queryset = MateriaLegislativa.objects.all() - filter_backends = (DjangoFilterBackend,) - filter_fields = ('numero', 'ano', 'tipo', ) + if self.request.user.is_anonymous(): + qs = qs.exclude(restrito=True) + return qs -class SessaoPlenariaViewSet(ListModelMixin, - RetrieveModelMixin, - GenericViewSet): +class _DocumentoAcessorioAdministrativoViewSet( + SaplSetViews['protocoloadm']['documentoacessorioadministrativo']): - permission_classes = (AllowAny,) - serializer_class = SessaoPlenariaSerializer - queryset = SessaoPlenaria.objects.all() - filter_backends = (DjangoFilterBackend,) - filter_fields = ('data_inicio', 'data_fim', 'interativa') + permission_classes = ( + _DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, ) + + def get_queryset(self): + qs = super().get_queryset() + + if self.request.user.is_anonymous(): + qs = qs.exclude(documento__restrito=True) + return qs + + +class _TramitacaoAdministrativoViewSet( + SaplSetViews['protocoloadm']['tramitacaoadministrativo'], + BusinessRulesNotImplementedMixin): + # TODO: Implementar regras de manutenção das tramitações de docs adms + + permission_classes = ( + _DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, ) + + def get_queryset(self): + qs = super().get_queryset() + + if self.request.user.is_anonymous(): + qs = qs.exclude(documento__restrito=True) + return qs + + +class _SessaoPlenariaViewSet( + SaplSetViews['sessao']['sessaoplenaria']): + + @action(detail=False) + def years(self, request, *args, **kwargs): + years = choice_anos_com_sessaoplenaria() + + serializer = ChoiceSerializer(years, many=True) + return Response(serializer.data) + + +SaplSetViews['base']['autor'] = _AutorViewSet.build_class_with_actions() + +SaplSetViews['materia']['proposicao'] = _ProposicaoViewSet + +SaplSetViews['parlamentares']['parlamentar'] = _ParlamentarViewSet + +SaplSetViews['protocoloadm']['documentoadministrativo'] = _DocumentoAdministrativoViewSet +SaplSetViews['protocoloadm']['documentoacessorioadministrativo'] = _DocumentoAcessorioAdministrativoViewSet +SaplSetViews['protocoloadm']['tramitacaoadministrativo'] = _TramitacaoAdministrativoViewSet + +SaplSetViews['sessao']['sessaoplenaria'] = _SessaoPlenariaViewSet diff --git a/sapl/audiencia/forms.py b/sapl/audiencia/forms.py index ab2f2a6e7..56f4d1285 100755 --- a/sapl/audiencia/forms.py +++ b/sapl/audiencia/forms.py @@ -7,12 +7,12 @@ from django.utils.translation import ugettext_lazy as _ from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica, AnexoAudienciaPublica from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout -from crispy_forms.helper import FormHelper +from sapl.crispy_layout_mixin import SaplFormHelper from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa -from sapl.utils import timezone +from sapl.utils import timezone, FileFieldCheckMixin -class AudienciaForm(forms.ModelForm): +class AudienciaForm(FileFieldCheckMixin, forms.ModelForm): logger = logging.getLogger(__name__) data_atual = timezone.now() @@ -134,7 +134,7 @@ class AnexoAudienciaPublicaForm(forms.ModelForm): row2 = to_row( [('assunto', 12)]) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = SaplFormLayout( Fieldset(_('Identificação Básica'), row1, row2)) diff --git a/sapl/audiencia/migrations/0010_auto_20190219_1511.py b/sapl/audiencia/migrations/0010_auto_20190219_1511.py new file mode 100644 index 000000000..e3f9a046b --- /dev/null +++ b/sapl/audiencia/migrations/0010_auto_20190219_1511.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-02-19 18:11 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +import sapl.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('audiencia', '0009_remove_anexoaudienciapublica_indexacao'), + ] + + operations = [ + migrations.AlterField( + model_name='anexoaudienciapublica', + name='arquivo', + field=models.FileField(default='Assunto não existente.', upload_to=sapl.utils.texto_upload_path, verbose_name='Arquivo'), + preserve_default=False, + ), + migrations.AlterField( + model_name='anexoaudienciapublica', + name='assunto', + field=models.TextField(verbose_name='Assunto'), + ), + migrations.AlterField( + model_name='anexoaudienciapublica', + name='data', + field=models.DateField(auto_now=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/sapl/audiencia/models.py b/sapl/audiencia/models.py index 684f27625..76c36c6c0 100755 --- a/sapl/audiencia/models.py +++ b/sapl/audiencia/models.py @@ -155,13 +155,12 @@ class AnexoAudienciaPublica(models.Model): audiencia = models.ForeignKey(AudienciaPublica, on_delete=models.PROTECT) arquivo = models.FileField( - blank=True, - null=True, upload_to=texto_upload_path, verbose_name=_('Arquivo')) - data = models.DateField(auto_now=timezone.now,blank=True, null=True) + data = models.DateField( + auto_now=timezone.now) assunto = models.TextField( - blank=True, verbose_name=_('Assunto')) + verbose_name=_('Assunto')) class Meta: verbose_name = _('Anexo de Documento Acessório') @@ -174,22 +173,19 @@ class AnexoAudienciaPublica(models.Model): if self.arquivo: self.arquivo.delete() - return models.Model.delete( - self, using=using, keep_parents=keep_parents) - - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): + return models.Model.delete(self, using=using, keep_parents=keep_parents) + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): if not self.pk and self.arquivo: arquivo = self.arquivo self.arquivo = None - models.Model.save(self, force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields) + models.Model.save( + self, + force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) self.arquivo = arquivo - return models.Model.save(self, force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields) \ No newline at end of file + return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, + update_fields=update_fields) diff --git a/sapl/audiencia/urls.py b/sapl/audiencia/urls.py index bdf02beca..b268fd56f 100755 --- a/sapl/audiencia/urls.py +++ b/sapl/audiencia/urls.py @@ -1,11 +1,10 @@ from django.conf.urls import include, url -from sapl.audiencia.views import (index, AudienciaCrud,AnexoAudienciaPublicaCrud) +from sapl.audiencia.views import (index, AudienciaCrud, AnexoAudienciaPublicaCrud) from .apps import AppConfig app_name = AppConfig.name urlpatterns = [ - url(r'^audiencia/', include(AudienciaCrud.get_urls() + - AnexoAudienciaPublicaCrud.get_urls())), + url(r'^audiencia/', include(AudienciaCrud.get_urls() + AnexoAudienciaPublicaCrud.get_urls())), ] \ No newline at end of file diff --git a/sapl/audiencia/views.py b/sapl/audiencia/views.py index 93d214683..82e921469 100755 --- a/sapl/audiencia/views.py +++ b/sapl/audiencia/views.py @@ -86,6 +86,7 @@ class AnexoAudienciaPublicaCrud(MasterDetailCrud): model = AnexoAudienciaPublica parent_field = 'audiencia' help_topic = 'numeracao_docsacess' + public = [RP_LIST, RP_DETAIL, ] class BaseMixin(MasterDetailCrud.BaseMixin): list_field_names = ['assunto'] @@ -104,7 +105,5 @@ class AnexoAudienciaPublicaCrud(MasterDetailCrud): kwargs = {self.crud.parent_field: self.kwargs['pk']} return qs.filter(**kwargs).order_by('-data', '-id') - class DetailView(AudienciaPublicaMixin, - MasterDetailCrud.DetailView): + class DetailView(AudienciaPublicaMixin, MasterDetailCrud.DetailView): pass - \ No newline at end of file diff --git a/sapl/base/email_utils.py b/sapl/base/email_utils.py index 024045edd..b41c68402 100644 --- a/sapl/base/email_utils.py +++ b/sapl/base/email_utils.py @@ -18,7 +18,7 @@ def load_email_templates(templates, context={}): emails = [] for t in templates: tpl = loader.get_template(t) - email = tpl.render(Context(context)) + email = tpl.render(context) if t.endswith(".html"): email = email.replace('\n', '').replace('\r', '') emails.append(email) diff --git a/sapl/base/forms.py b/sapl/base/forms.py index ccd2c132b..9a8dd811e 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -1,8 +1,8 @@ -import django_filters import logging +import os from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton -from crispy_forms.helper import FormHelper +from sapl.crispy_layout_mixin import SaplFormHelper from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row from django import forms from django.conf import settings @@ -14,25 +14,33 @@ from django.core.exceptions import ValidationError from django.db import models, transaction from django.db.models import Q from django.forms import Form, ModelForm -from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ +import django_filters +from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica +from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica from sapl.base.models import Autor, TipoAutor +from sapl.comissoes.models import Reuniao, Comissao +from sapl.comissoes.models import Reuniao, Comissao from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column, to_row) -from sapl.audiencia.models import AudienciaPublica,TipoAudienciaPublica -from sapl.comissoes.models import Reuniao, Comissao -from sapl.materia.models import (MateriaLegislativa, UnidadeTramitacao, StatusTramitacao) -from sapl.parlamentares.models import SessaoLegislativa +from sapl.materia.models import ( + MateriaLegislativa, UnidadeTramitacao, StatusTramitacao) +from sapl.norma.models import (NormaJuridica, NormaEstatisticas) +from sapl.parlamentares.models import SessaoLegislativa, Partido from sapl.sessao.models import SessaoPlenaria from sapl.settings import MAX_IMAGE_UPLOAD_SIZE from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, ChoiceWithoutValidationField, ImageThumbnailFileInput, RangeWidgetOverride, autor_label, autor_modal, - models_with_gr_for_model, qs_override_django_filter) - + models_with_gr_for_model, qs_override_django_filter, + choice_anos_com_normas, choice_anos_com_materias, + FilterOverridesMetaMixin, FileFieldCheckMixin) from .models import AppConfig, CasaLegislativa + ACTION_CREATE_USERS_AUTOR_CHOICE = [ ('A', _('Associar um usuário existente')), ('N', _('Autor sem Usuário de Acesso ao Sapl')), @@ -83,7 +91,8 @@ class UsuarioCreateForm(ModelForm): data = self.cleaned_data if data['password1'] != data['password2']: - self.logger.error('Erro de validação. Senhas informadas ({}, {}) são diferentes.'.format(data['password1'], data['password2'])) + self.logger.error('Erro de validação. Senhas informadas ({}, {}) são diferentes.'.format( + data['password1'], data['password2'])) raise ValidationError('Senhas informadas são diferentes') return data @@ -103,16 +112,38 @@ class UsuarioCreateForm(ModelForm): [('password1', 6), ('password2', 6)]) - row4 = to_row([(form_actions(label='Confirmar'), 6)]) - - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout( row0, row1, row3, row2, 'roles', - row4) + form_actions(label='Confirmar')) + + +class UsuarioFilterSet(django_filters.FilterSet): + + username = django_filters.CharFilter( + label=_('Nome de Usuário'), + lookup_expr='icontains') + + class Meta: + model = User + fields = ['username'] + + def __init__(self, *args, **kwargs): + super(UsuarioFilterSet, self).__init__(*args, **kwargs) + + row0 = to_row([('username', 12)]) + + self.form.helper = SaplFormHelper() + self.form.helper.form_method = 'GET' + self.form.helper.layout = Layout( + Fieldset(_('Pesquisa de Usuário'), + row0, + form_actions(label='Pesquisar')) + ) class UsuarioEditForm(ModelForm): @@ -120,8 +151,10 @@ class UsuarioEditForm(ModelForm): # ROLES = [(g.id, g.name) for g in Group.objects.all().order_by('name')] ROLES = [] - password1 = forms.CharField(required=False, widget=forms.PasswordInput, label='Senha') - password2 = forms.CharField(required=False, widget=forms.PasswordInput, label='Confirmar senha') + password1 = forms.CharField( + required=False, widget=forms.PasswordInput, label='Senha') + password2 = forms.CharField( + required=False, widget=forms.PasswordInput, label='Confirmar senha') user_active = forms.ChoiceField(choices=YES_NO_CHOICES, required=True, label="Usuário ativo?", initial='True') roles = forms.MultipleChoiceField( @@ -143,12 +176,12 @@ class UsuarioEditForm(ModelForm): row3 = to_row([(form_actions(label='Salvar Alterações'), 6)]) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout( row1, row2, 'roles', - row3) + form_actions(label='Salvar Alterações')) def clean(self): super(UsuarioEditForm, self).clean() @@ -158,19 +191,22 @@ class UsuarioEditForm(ModelForm): data = self.cleaned_data if data['password1'] and data['password1'] != data['password2']: - self.logger.error('Erro de validação. Senhas informadas ({}, {}) são diferentes.'.format(data['password1'], data['password2'])) + self.logger.error('Erro de validação. Senhas informadas ({}, {}) são diferentes.'.format( + data['password1'], data['password2'])) raise ValidationError('Senhas informadas são diferentes') return data -class SessaoLegislativaForm(ModelForm): + +class SessaoLegislativaForm(FileFieldCheckMixin, ModelForm): logger = logging.getLogger(__name__) + class Meta: model = SessaoLegislativa exclude = [] def clean(self): - + cleaned_data = super(SessaoLegislativaForm, self).clean() if not self.is_valid(): @@ -185,13 +221,18 @@ class SessaoLegislativaForm(ModelForm): data_fim_leg = legislatura.data_fim pk = self.initial['id'] if self.initial else None # Queries para verificar se existem Sessões Legislativas no período selecionado no form - # Caso onde a data_inicio e data_fim são iguais a de alguma sessão já criada + # Caso onde a data_inicio e data_fim são iguais a de alguma sessão já + # criada primeiro_caso = Q(data_inicio=data_inicio, data_fim=data_fim) - # Caso onde a data_inicio está entre o início e o fim de uma Sessão já existente - segundo_caso = Q(data_inicio__lt=data_inicio, data_fim__range=(data_inicio, data_fim)) - # Caso onde a data_fim está entre o início e o fim de uma Sessão já existente - terceiro_caso = Q(data_inicio__range=(data_inicio, data_fim), data_fim__gt=data_fim) - sessoes_existentes = SessaoLegislativa.objects.filter(primeiro_caso|segundo_caso|terceiro_caso).\ + # Caso onde a data_inicio está entre o início e o fim de uma Sessão já + # existente + segundo_caso = Q(data_inicio__lt=data_inicio, + data_fim__range=(data_inicio, data_fim)) + # Caso onde a data_fim está entre o início e o fim de uma Sessão já + # existente + terceiro_caso = Q(data_inicio__range=( + data_inicio, data_fim), data_fim__gt=data_fim) + sessoes_existentes = SessaoLegislativa.objects.filter(primeiro_caso | segundo_caso | terceiro_caso).\ exclude(pk=pk) if sessoes_existentes: @@ -215,39 +256,41 @@ class SessaoLegislativaForm(ModelForm): if numero <= ult and flag_edit: self.logger.error('O número da SessaoLegislativa ({}) é menor ou igual ' - 'que o de Sessões Legislativas passadas ({})'.format(numero, ult)) + 'que o de Sessões Legislativas passadas ({})'.format(numero, ult)) raise ValidationError('O número da Sessão Legislativa não pode ser menor ou igual ' 'que o de Sessões Legislativas passadas') if data_inicio < data_inicio_leg or \ - data_inicio > data_fim_leg: + data_inicio > data_fim_leg: self.logger.error('A data de início ({}) da SessaoLegislativa está compreendida ' - 'fora da data início ({}) e fim ({}) da Legislatura ' - 'selecionada'.format(data_inicio, data_inicio_leg, data_fim_leg)) + 'fora da data início ({}) e fim ({}) da Legislatura ' + 'selecionada'.format(data_inicio, data_inicio_leg, data_fim_leg)) raise ValidationError('A data de início da Sessão Legislativa deve estar compreendida ' 'entre a data início e fim da Legislatura selecionada') if data_fim > data_fim_leg or \ - data_fim < data_inicio_leg: + data_fim < data_inicio_leg: self.logger.error('A data de fim ({}) da SessaoLegislativa está compreendida ' - 'fora da data início ({}) e fim ({}) da Legislatura ' - 'selecionada.'.format(data_fim, data_inicio_leg, data_fim_leg)) + 'fora da data início ({}) e fim ({}) da Legislatura ' + 'selecionada.'.format(data_fim, data_inicio_leg, data_fim_leg)) raise ValidationError('A data de fim da Sessão Legislativa deve estar compreendida ' 'entre a data início e fim da Legislatura selecionada') if data_inicio > data_fim: - self.logger.error('Data início ({}) superior à data fim ({}).'.format(data_inicio, data_fim)) - raise ValidationError('Data início não pode ser superior à data fim') + self.logger.error( + 'Data início ({}) superior à data fim ({}).'.format(data_inicio, data_fim)) + raise ValidationError( + 'Data início não pode ser superior à data fim') data_inicio_intervalo = cleaned_data['data_inicio_intervalo'] data_fim_intervalo = cleaned_data['data_fim_intervalo'] if data_inicio_intervalo and data_fim_intervalo and \ - data_inicio_intervalo > data_fim_intervalo: - self.logger.error('Data início de intervalo ({}) superior à ' - 'data fim de intervalo ({}).'.format(data_inicio_intervalo, data_fim_intervalo)) - raise ValidationError('Data início de intervalo não pode ser ' - 'superior à data fim de intervalo') + data_inicio_intervalo > data_fim_intervalo: + self.logger.error('Data início de intervalo ({}) superior à ' + 'data fim de intervalo ({}).'.format(data_inicio_intervalo, data_fim_intervalo)) + raise ValidationError('Data início de intervalo não pode ser ' + 'superior à data fim de intervalo') if data_inicio_intervalo: if data_inicio_intervalo < data_inicio or \ @@ -255,9 +298,9 @@ class SessaoLegislativaForm(ModelForm): data_inicio_intervalo > data_fim or \ data_inicio_intervalo > data_fim_leg: self.logger.error('A data de início do intervalo ({}) não está compreendida entre ' - 'as datas de início ({}) e fim ({}) tanto da Legislatura quanto da ' - 'própria Sessão Legislativa ({} e {}).' - .format(data_inicio_intervalo, data_inicio_leg, data_fim_leg, data_inicio, data_fim)) + 'as datas de início ({}) e fim ({}) tanto da Legislatura quanto da ' + 'própria Sessão Legislativa ({} e {}).' + .format(data_inicio_intervalo, data_inicio_leg, data_fim_leg, data_inicio, data_fim)) raise ValidationError('A data de início do intervalo deve estar compreendida entre ' 'as datas de início e fim tanto da Legislatura quanto da ' 'própria Sessão Legislativa') @@ -267,9 +310,9 @@ class SessaoLegislativaForm(ModelForm): data_fim_intervalo < data_inicio or \ data_fim_intervalo < data_inicio_leg: self.logger.error('A data de fim do intervalo ({}) não está compreendida entre ' - 'as datas de início ({}) e fim ({}) tanto da Legislatura quanto da ' - 'própria Sessão Legislativa ({} e {}).' - .format(data_fim_intervalo, data_inicio_leg, data_fim_leg, data_inicio, data_fim)) + 'as datas de início ({}) e fim ({}) tanto da Legislatura quanto da ' + 'própria Sessão Legislativa ({} e {}).' + .format(data_fim_intervalo, data_inicio_leg, data_fim_leg, data_inicio, data_fim)) raise ValidationError('A data de fim do intervalo deve estar compreendida entre ' 'as datas de início e fim tanto da Legislatura quanto da ' 'própria Sessão Legislativa') @@ -373,7 +416,7 @@ class AutorForm(ModelForm): placeholder=_('Pesquisar por possíveis autores para ' 'o Tipo de Autor selecionado.')), StrictButton( - _('Filtrar'), css_class='btn-default btn-filtrar-autor', + _('Filtrar'), css_class='btn-outline-primary btn-filtrar-autor', type='button')), css_class='hidden', data_action='create', @@ -381,9 +424,9 @@ class AutorForm(ModelForm): data_field='autor_related') autor_select = Row(to_column(('tipo', 3)), - Div(to_column(('nome', 5)), - to_column(('cargo', 4)), - css_class="div_nome_cargo"), + Div(to_column(('nome', 7)), + to_column(('cargo', 5)), + css_class="div_nome_cargo row col"), to_column((autor_related, 9)), to_column((Div( Field('autor_related'), @@ -411,7 +454,7 @@ class AutorForm(ModelForm): controle_acesso = Fieldset(_('Controle de Acesso do Autor'), *controle_acesso) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = SaplFormLayout(autor_select, controle_acesso) super(AutorForm, self).__init__(*args, **kwargs) @@ -463,7 +506,8 @@ class AutorForm(ModelForm): def valida_igualdade(self, texto1, texto2, msg): if texto1 != texto2: - self.logger.error('Textos diferentes. ("{}" e "{}")'.format(texto1, texto2)) + self.logger.error( + 'Textos diferentes. ("{}" e "{}")'.format(texto1, texto2)) raise ValidationError(msg) return True @@ -478,7 +522,7 @@ class AutorForm(ModelForm): if 'action_user' not in cd or not cd['action_user']: self.logger.error('Não Informado se o Autor terá usuário ' - 'vinculado para acesso ao Sistema.') + 'vinculado para acesso ao Sistema.') raise ValidationError(_('Informe se o Autor terá usuário ' 'vinculado para acesso ao Sistema.')) @@ -489,9 +533,9 @@ class AutorForm(ModelForm): get_user_model().USERNAME_FIELD) != cd['username']: if 'status_user' not in cd or not cd['status_user']: self.logger.error('Foi trocado ou removido o usuário deste Autor ({}), ' - 'mas não foi informado como se deve proceder ' - 'com o usuário que está sendo desvinculado? ({})' - .format(cd['username'], get_user_model().USERNAME_FIELD)) + 'mas não foi informado como se deve proceder ' + 'com o usuário que está sendo desvinculado? ({})' + .format(cd['username'], get_user_model().USERNAME_FIELD)) raise ValidationError( _('Foi trocado ou removido o usuário deste Autor, ' 'mas não foi informado como se deve proceder ' @@ -508,7 +552,8 @@ class AutorForm(ModelForm): if cd['action_user'] == 'A': param_username = {get_user_model().USERNAME_FIELD: cd['username']} if not User.objects.filter(**param_username).exists(): - self.logger.error('Não existe usuário com username "%s". ' % cd['username']) + self.logger.error( + 'Não existe usuário com username "%s". ' % cd['username']) raise ValidationError( _('Não existe usuário com username "%s". ' 'Para utilizar esse username você deve selecionar ' @@ -523,7 +568,8 @@ class AutorForm(ModelForm): param_username = { 'user__' + get_user_model().USERNAME_FIELD: cd['username']} if qs_autor.filter(**param_username).exists(): - self.logger.error('Já existe um Autor para este usuário ({}).'.format(cd['username'])) + self.logger.error( + 'Já existe um Autor para este usuário ({}).'.format(cd['username'])) raise ValidationError( _('Já existe um Autor para este usuário.')) @@ -547,7 +593,7 @@ class AutorForm(ModelForm): else: if 'autor_related' not in cd or not cd['autor_related']: self.logger.error('Registro de %s não escolhido para ser ' - 'vinculado ao cadastro de Autor' % tipo.descricao) + 'vinculado ao cadastro de Autor' % tipo.descricao) raise ValidationError( _('Um registro de %s deve ser escolhido para ser ' 'vinculado ao cadastro de Autor') % tipo.descricao) @@ -555,7 +601,7 @@ class AutorForm(ModelForm): if not tipo.content_type.model_class().objects.filter( pk=cd['autor_related']).exists(): self.logger.error('O Registro definido (%s-%s) não está na base ' - 'de %s.' % (cd['autor_related'], cd['q'], tipo.descricao)) + 'de %s.' % (cd['autor_related'], cd['q'], tipo.descricao)) raise ValidationError( _('O Registro definido (%s-%s) não está na base de %s.' ) % (cd['autor_related'], cd['q'], tipo.descricao)) @@ -566,7 +612,7 @@ class AutorForm(ModelForm): if qs_autor_selected.exists(): autor = qs_autor_selected.first() self.logger.error('Já existe um autor Cadastrado para ' - '%s' % autor.autor_related) + '%s' % autor.autor_related) raise ValidationError( _('Já existe um autor Cadastrado para %s' ) % autor.autor_related) @@ -654,14 +700,7 @@ class AutorFormForAdmin(AutorForm): class RelatorioAtasFilterSet(django_filters.FilterSet): - filter_overrides = {models.DateField: { - 'filter_class': django_filters.DateFromToRangeFilter, - 'extra': lambda f: { - 'label': '%s (%s)' % (f.verbose_name, _('Inicial - Final')), - 'widget': RangeWidgetOverride} - }} - - class Meta: + class Meta(FilterOverridesMetaMixin): model = SessaoPlenaria fields = ['data_inicio'] @@ -680,7 +719,7 @@ class RelatorioAtasFilterSet(django_filters.FilterSet): row1 = to_row([('data_inicio', 12)]) - self.form.helper = FormHelper() + self.form.helper = SaplFormHelper() self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Atas das Sessões Plenárias'), @@ -688,16 +727,117 @@ class RelatorioAtasFilterSet(django_filters.FilterSet): ) -class RelatorioPresencaSessaoFilterSet(django_filters.FilterSet): +def ultimo_ano_com_norma(): + anos_normas = choice_anos_com_normas() + + if anos_normas: + return anos_normas[0] + return '' + - filter_overrides = {models.DateField: { - 'filter_class': django_filters.DateFromToRangeFilter, - 'extra': lambda f: { - 'label': '%s (%s)' % (f.verbose_name, _('Inicial - Final')), - 'widget': RangeWidgetOverride} - }} +class RelatorioNormasMesFilterSet(django_filters.FilterSet): + + ano = django_filters.ChoiceFilter(required=True, + label='Ano da Norma', + choices=choice_anos_com_normas, + initial=ultimo_ano_com_norma) + + class Meta: + model = NormaJuridica + fields = ['ano'] + + def __init__(self, *args, **kwargs): + super(RelatorioNormasMesFilterSet, self).__init__( + *args, **kwargs) + + self.filters['ano'].label = 'Ano' + self.form.fields['ano'].required = True + + row1 = to_row([('ano', 12)]) + + self.form.helper = SaplFormHelper() + self.form.helper.form_method = 'GET' + self.form.helper.layout = Layout( + Fieldset(_('Normas por mês do ano.'), + row1, form_actions(label='Pesquisar')) + ) + + @property + def qs(self): + parent = super(RelatorioNormasMesFilterSet, self).qs + return parent.distinct().order_by('data') + + +class EstatisticasAcessoNormasForm(Form): + + ano = forms.ChoiceField(required=True, + label='Ano de acesso', + choices=RANGE_ANOS, + initial=timezone.now().year) class Meta: + fields = ['ano'] + + def __init__(self, *args, **kwargs): + super(EstatisticasAcessoNormasForm, self).__init__( + *args, **kwargs) + + row1 = to_row([('ano', 12)]) + + self.helper = SaplFormHelper() + self.helper.form_method = 'GET' + self.helper.layout = Layout( + Fieldset(_('Normas por acessos nos meses do ano.'), + row1, form_actions(label='Pesquisar')) + ) + + def clean(self): + super(EstatisticasAcessoNormasForm, self).clean() + + return self.cleaned_data + + +class RelatorioNormasVigenciaFilterSet(django_filters.FilterSet): + + ano = django_filters.ChoiceFilter(required=True, + label='Ano da Norma', + choices=choice_anos_com_normas, + initial=ultimo_ano_com_norma) + + vigencia = forms.ChoiceField( + label=_('Vigência'), + choices=[(True, "Vigente"), (False, "Não vigente")], + widget=forms.RadioSelect(), + required=True, + initial=True) + + def __init__(self, *args, **kwargs): + super(RelatorioNormasVigenciaFilterSet, self).__init__( + *args, **kwargs) + + self.filters['ano'].label = 'Ano' + self.form.fields['ano'].required = True + self.form.fields['vigencia'] = self.vigencia + + row1 = to_row([('ano', 12)]) + row2 = to_row([('vigencia', 12)]) + + self.form.helper = SaplFormHelper() + self.form.helper.form_method = 'GET' + self.form.helper.layout = Layout( + Fieldset(_('Normas por vigência.'), + row1, row2, + form_actions(label='Pesquisar')) + ) + + @property + def qs(self): + return qs_override_django_filter(self) + + +class RelatorioPresencaSessaoFilterSet(django_filters.FilterSet): + + class Meta(FilterOverridesMetaMixin): model = SessaoPlenaria fields = ['data_inicio'] @@ -710,7 +850,7 @@ class RelatorioPresencaSessaoFilterSet(django_filters.FilterSet): row1 = to_row([('data_inicio', 12)]) - self.form.helper = FormHelper() + self.form.helper = SaplFormHelper() self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Presença dos parlamentares nas sessões plenárias'), @@ -724,19 +864,12 @@ class RelatorioPresencaSessaoFilterSet(django_filters.FilterSet): class RelatorioHistoricoTramitacaoFilterSet(django_filters.FilterSet): - filter_overrides = {models.DateField: { - 'filter_class': django_filters.DateFromToRangeFilter, - 'extra': lambda f: { - 'label': '%s (%s)' % (f.verbose_name, _('Inicial - Final')), - 'widget': RangeWidgetOverride} - }} - @property def qs(self): parent = super(RelatorioHistoricoTramitacaoFilterSet, self).qs return parent.distinct().prefetch_related('tipo').order_by('-ano', 'tipo', 'numero') - class Meta: + class Meta(FilterOverridesMetaMixin): model = MateriaLegislativa fields = ['tipo', 'tramitacao__unidade_tramitacao_local', 'tramitacao__status', 'tramitacao__data_tramitacao'] @@ -747,13 +880,17 @@ class RelatorioHistoricoTramitacaoFilterSet(django_filters.FilterSet): self.filters['tipo'].label = 'Tipo de Matéria' + self.filters['tramitacao__unidade_tramitacao_local' + ].label = _('Unidade Local') + self.filters['tramitacao__status'].label = _('Status') row1 = to_row([('tramitacao__data_tramitacao', 12)]) + row2 = to_row( [('tipo', 4), ('tramitacao__unidade_tramitacao_local', 4), ('tramitacao__status', 4)]) - self.form.helper = FormHelper() + self.form.helper = SaplFormHelper() self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Histórico de Tramitação'), @@ -764,19 +901,12 @@ class RelatorioHistoricoTramitacaoFilterSet(django_filters.FilterSet): class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet): - filter_overrides = {models.DateField: { - 'filter_class': django_filters.DateFromToRangeFilter, - 'extra': lambda f: { - 'label': '%s (%s)' % (f.verbose_name, _('Inicial - Final')), - 'widget': RangeWidgetOverride} - }} - @property def qs(self): parent = super(RelatorioDataFimPrazoTramitacaoFilterSet, self).qs return parent.distinct().prefetch_related('tipo').order_by('-ano', 'tipo', 'numero') - class Meta: + class Meta(FilterOverridesMetaMixin): model = MateriaLegislativa fields = ['tipo', 'tramitacao__unidade_tramitacao_local', 'tramitacao__status', 'tramitacao__data_fim_prazo'] @@ -793,7 +923,7 @@ class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet): ('tramitacao__unidade_tramitacao_local', 4), ('tramitacao__status', 4)]) - self.form.helper = FormHelper() + self.form.helper = SaplFormHelper() self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Tramitações por fim de prazo'), @@ -812,7 +942,7 @@ class RelatorioReuniaoFilterSet(django_filters.FilterSet): class Meta: model = Reuniao fields = ['comissao', 'data', - 'nome','tema'] + 'nome', 'tema'] def __init__(self, *args, **kwargs): super(RelatorioReuniaoFilterSet, self).__init__( @@ -824,7 +954,7 @@ class RelatorioReuniaoFilterSet(django_filters.FilterSet): ('nome', 4), ('tema', 4)]) - self.form.helper = FormHelper() + self.form.helper = SaplFormHelper() self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Reunião de Comissão'), @@ -832,6 +962,7 @@ class RelatorioReuniaoFilterSet(django_filters.FilterSet): form_actions(label='Pesquisar')) ) + class RelatorioAudienciaFilterSet(django_filters.FilterSet): @property @@ -853,7 +984,7 @@ class RelatorioAudienciaFilterSet(django_filters.FilterSet): [('tipo', 4), ('nome', 4)]) - self.form.helper = FormHelper() + self.form.helper = SaplFormHelper() self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Audiência Pública'), @@ -862,12 +993,11 @@ class RelatorioAudienciaFilterSet(django_filters.FilterSet): ) - class RelatorioMateriasTramitacaoilterSet(django_filters.FilterSet): ano = django_filters.ChoiceFilter(required=True, label='Ano da Matéria', - choices=RANGE_ANOS) + choices=choice_anos_com_materias) tramitacao__unidade_tramitacao_destino = django_filters.ModelChoiceFilter( queryset=UnidadeTramitacao.objects.all(), @@ -882,7 +1012,6 @@ class RelatorioMateriasTramitacaoilterSet(django_filters.FilterSet): parent = super(RelatorioMateriasTramitacaoilterSet, self).qs return parent.distinct().order_by('-ano', 'tipo', '-numero') - class Meta: model = MateriaLegislativa fields = ['ano', 'tipo', 'tramitacao__unidade_tramitacao_destino', @@ -899,7 +1028,7 @@ class RelatorioMateriasTramitacaoilterSet(django_filters.FilterSet): row3 = to_row([('tramitacao__unidade_tramitacao_destino', 12)]) row4 = to_row([('tramitacao__status', 12)]) - self.form.helper = FormHelper() + self.form.helper = SaplFormHelper() self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Pesquisa de Matéria em Tramitação'), @@ -912,7 +1041,7 @@ class RelatorioMateriasPorAnoAutorTipoFilterSet(django_filters.FilterSet): ano = django_filters.ChoiceFilter(required=True, label='Ano da Matéria', - choices=RANGE_ANOS) + choices=choice_anos_com_materias) class Meta: model = MateriaLegislativa @@ -925,7 +1054,7 @@ class RelatorioMateriasPorAnoAutorTipoFilterSet(django_filters.FilterSet): row1 = to_row( [('ano', 12)]) - self.form.helper = FormHelper() + self.form.helper = SaplFormHelper() self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Pesquisar'), @@ -936,13 +1065,6 @@ class RelatorioMateriasPorAnoAutorTipoFilterSet(django_filters.FilterSet): class RelatorioMateriasPorAutorFilterSet(django_filters.FilterSet): - filter_overrides = {models.DateField: { - 'filter_class': django_filters.DateFromToRangeFilter, - 'extra': lambda f: { - 'label': '%s (%s)' % (f.verbose_name, _('Inicial - Final')), - 'widget': RangeWidgetOverride} - }} - autoria__autor = django_filters.CharFilter(widget=forms.HiddenInput()) @property @@ -951,7 +1073,7 @@ class RelatorioMateriasPorAutorFilterSet(django_filters.FilterSet): return parent.distinct().filter(autoria__primeiro_autor=True)\ .order_by('autoria__autor', '-autoria__primeiro_autor', 'tipo', '-ano', '-numero') - class Meta: + class Meta(FilterOverridesMetaMixin): model = MateriaLegislativa fields = ['tipo', 'data_apresentacao'] @@ -974,7 +1096,7 @@ class RelatorioMateriasPorAutorFilterSet(django_filters.FilterSet): 'limpar Autor', css_class='btn btn-primary btn-sm'), 10)]) - self.form.helper = FormHelper() + self.form.helper = SaplFormHelper() self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Pesquisar'), @@ -986,7 +1108,7 @@ class RelatorioMateriasPorAutorFilterSet(django_filters.FilterSet): ) -class CasaLegislativaForm(ModelForm): +class CasaLegislativaForm(FileFieldCheckMixin, ModelForm): class Meta: @@ -1016,7 +1138,11 @@ class CasaLegislativaForm(ModelForm): } def clean_logotipo(self): - logotipo = self.cleaned_data.get('logotipo', False) + # chama __clean de FileFieldCheckMixin + # por estar em clean de campo + super(CasaLegislativaForm, self)._check() + + logotipo = self.cleaned_data.get('logotipo') if logotipo: if logotipo.size > MAX_IMAGE_UPLOAD_SIZE: raise ValidationError("Imagem muito grande. ( > 2MB )") @@ -1055,13 +1181,16 @@ class ConfiguracoesAppForm(ModelForm): 'texto_articulado_materia', 'texto_articulado_norma', 'proposicao_incorporacao_obrigatoria', + 'protocolo_manual', 'cronometro_discurso', 'cronometro_aparte', 'cronometro_ordem', 'cronometro_consideracoes', 'mostrar_brasao_painel', 'receber_recibo_proposicao', - 'assinatura_ata'] + 'assinatura_ata', + 'estatisticas_acesso_normas', + 'escolher_numero_materia_proposicao'] def __init__(self, *args, **kwargs): super(ConfiguracoesAppForm, self).__init__(*args, **kwargs) @@ -1070,7 +1199,6 @@ class ConfiguracoesAppForm(ModelForm): self.fields['cronometro_ordem'].widget.attrs['class'] = 'cronometro' self.fields['cronometro_consideracoes'].widget.attrs['class'] = 'cronometro' - def clean_mostrar_brasao_painel(self): mostrar_brasao_painel = self.cleaned_data.get( 'mostrar_brasao_painel', False) @@ -1080,9 +1208,9 @@ class ConfiguracoesAppForm(ModelForm): self.logger.error('Não há casa legislativa relacionada.') raise ValidationError("Não há casa legislativa relacionada.") - if (not bool(casa.logotipo) and mostrar_brasao_painel): + if not casa.logotipo and mostrar_brasao_painel: self.logger.error('Não há logitipo configurado para esta ' - 'CasaLegislativa ({}).'.format(casa)) + 'CasaLegislativa ({}).'.format(casa)) raise ValidationError("Não há logitipo configurado para esta " "Casa legislativa.") @@ -1096,7 +1224,7 @@ class RecuperarSenhaForm(PasswordResetForm): def __init__(self, *args, **kwargs): row1 = to_row( [('email', 12)]) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout( Fieldset(_('Insira o e-mail cadastrado com a sua conta'), row1, @@ -1117,7 +1245,7 @@ class RecuperarSenhaForm(PasswordResetForm): if not email_existente: msg = 'Não existe nenhum usuário cadastrado com este e-mail.' self.logger.error('Não existe nenhum usuário cadastrado com este e-mail ({}).' - .format(self.data['email'])) + .format(self.data['email'])) raise ValidationError(msg) return self.cleaned_data @@ -1133,7 +1261,7 @@ class NovaSenhaForm(SetPasswordForm): [('new_password1', 6), ('new_password2', 6)]) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout( row1, form_actions(label='Enviar')) @@ -1166,7 +1294,7 @@ class AlterarSenhaForm(Form): [('new_password1', 6), ('new_password2', 6)]) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout( row1, row2, @@ -1184,33 +1312,84 @@ class AlterarSenhaForm(Form): new_password2 = data['new_password2'] if new_password1 != new_password2: - self.logger.error("'Nova Senha' ({}) diferente de 'Confirmar Senha' ({})".format(new_password1, new_password2)) - raise ValidationError("'Nova Senha' diferente de 'Confirmar Senha'") + self.logger.error("'Nova Senha' ({}) diferente de 'Confirmar Senha' ({})".format( + new_password1, new_password2)) + raise ValidationError( + "'Nova Senha' diferente de 'Confirmar Senha'") # TODO: colocar mais regras como: tamanho mínimo, # TODO: caracteres alfanuméricos, maiúsculas (?), # TODO: senha atual igual a senha anterior, etc if len(new_password1) < 6: - self.logger.error('A senha informada ({}) não tem o mínimo de 6 caracteres.'.format(new_password1)) - raise ValidationError("A senha informada deve ter no mínimo 6 caracteres") + self.logger.error( + 'A senha informada ({}) não tem o mínimo de 6 caracteres.'.format(new_password1)) + raise ValidationError( + "A senha informada deve ter no mínimo 6 caracteres") username = data['username'] old_password = data['old_password'] user = User.objects.get(username=username) if user.is_anonymous(): - self.logger.error('Não é possível alterar senha de usuário anônimo ({}).'.format(username)) - raise ValidationError("Não é possível alterar senha de usuário anônimo") + self.logger.error( + 'Não é possível alterar senha de usuário anônimo ({}).'.format(username)) + raise ValidationError( + "Não é possível alterar senha de usuário anônimo") if not user.check_password(old_password): self.logger.error('Senha atual informada ({}) não confere ' - 'com a senha armazenada.'.format(old_password)) + 'com a senha armazenada.'.format(old_password)) raise ValidationError("Senha atual informada não confere " "com a senha armazenada") if user.check_password(new_password1): - self.logger.error('Nova senha ({}) igual à senha anterior.'.format(new_password1)) - raise ValidationError("Nova senha não pode ser igual à senha anterior") + self.logger.error( + 'Nova senha ({}) igual à senha anterior.'.format(new_password1)) + raise ValidationError( + "Nova senha não pode ser igual à senha anterior") return self.cleaned_data + + +class PartidoForm(FileFieldCheckMixin, ModelForm): + + class Meta: + model = Partido + exclude = [] + + def __init__(self, *args, **kwargs): + + super(PartidoForm, self).__init__(*args, **kwargs) + + # TODO Utilizar esses campos na issue #2161 de alteração de nomes de partidos + # if self.instance: + # if self.instance.nome: + # self.fields['nome'].widget.attrs['readonly'] = True + # self.fields['sigla'].widget.attrs['readonly'] = True + + row1 = to_row( + [('sigla', 2), + ('nome', 6), + ('data_criacao', 2), + ('data_extincao', 2),]) + row2 = to_row([('observacao', 12)]) + row3 = to_row([('logo_partido', 12)]) + + self.helper = SaplFormHelper() + self.helper.layout = Layout( + row1, row2, row3, + form_actions(label='Salvar')) + + def clean(self): + + cleaned_data = super(PartidoForm, self).clean() + + if not self.is_valid(): + return cleaned_data + + if cleaned_data['data_criacao'] and cleaned_data['data_extincao']: + if cleaned_data['data_criacao'] > cleaned_data['data_extincao']: + raise ValidationError("Certifique-se de que a data de criação seja anterior à data de extinção.") + + return cleaned_data \ No newline at end of file diff --git a/sapl/base/migrations/0027_appconfig_relatorios_atos.py b/sapl/base/migrations/0027_appconfig_relatorios_atos.py new file mode 100644 index 000000000..afd3382e1 --- /dev/null +++ b/sapl/base/migrations/0027_appconfig_relatorios_atos.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.8 on 2018-12-11 20:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0026_auto_20181126_1727'), + ] + + operations = [ + migrations.AddField( + model_name='appconfig', + name='relatorios_atos', + field=models.CharField(choices=[('S', 'Sim'), ('N', 'Não')], default='N', max_length=1, verbose_name='Relatórios de atos acessados'), + ), + ] diff --git a/sapl/base/migrations/0028_appconfig_estatisticas_acesso_normas.py b/sapl/base/migrations/0028_appconfig_estatisticas_acesso_normas.py new file mode 100644 index 000000000..7a4af06de --- /dev/null +++ b/sapl/base/migrations/0028_appconfig_estatisticas_acesso_normas.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.8 on 2018-12-18 17:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0027_appconfig_relatorios_atos'), + ] + + operations = [ + migrations.AddField( + model_name='appconfig', + name='estatisticas_acesso_normas', + field=models.CharField(choices=[('S', 'Sim'), ('N', 'Não')], default='N', max_length=1, verbose_name='Estatísticas de acesso a normas'), + ), + ] diff --git a/sapl/base/migrations/0029_remove_appconfig_relatorios_atos.py b/sapl/base/migrations/0029_remove_appconfig_relatorios_atos.py new file mode 100644 index 000000000..fa06b23ac --- /dev/null +++ b/sapl/base/migrations/0029_remove_appconfig_relatorios_atos.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.8 on 2018-12-18 18:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0028_appconfig_estatisticas_acesso_normas'), + ] + + operations = [ + migrations.RemoveField( + model_name='appconfig', + name='relatorios_atos', + ), + ] diff --git a/sapl/base/migrations/0030_appconfig_escolher_numero_materia_proposicao.py b/sapl/base/migrations/0030_appconfig_escolher_numero_materia_proposicao.py new file mode 100644 index 000000000..69bba70bc --- /dev/null +++ b/sapl/base/migrations/0030_appconfig_escolher_numero_materia_proposicao.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-02-19 11:14 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0029_remove_appconfig_relatorios_atos'), + ] + + operations = [ + migrations.AddField( + model_name='appconfig', + name='escolher_numero_materia_proposicao', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Indicar número da matéria a ser gerada na proposição?'), + ), + ] diff --git a/sapl/base/migrations/0030_appconfig_protocolo_manual.py b/sapl/base/migrations/0030_appconfig_protocolo_manual.py new file mode 100644 index 000000000..7782f974d --- /dev/null +++ b/sapl/base/migrations/0030_appconfig_protocolo_manual.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-02-15 18:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0029_remove_appconfig_relatorios_atos'), + ] + + operations = [ + migrations.AddField( + model_name='appconfig', + name='protocolo_manual', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Protocolar proposição somente com recibo?'), + ), + ] diff --git a/sapl/base/migrations/0031_auto_20190218_1109.py b/sapl/base/migrations/0031_auto_20190218_1109.py new file mode 100644 index 000000000..a87922a83 --- /dev/null +++ b/sapl/base/migrations/0031_auto_20190218_1109.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-02-18 14:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0030_appconfig_protocolo_manual'), + ] + + operations = [ + migrations.AlterField( + model_name='appconfig', + name='protocolo_manual', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Informar data e hora de protocolo?'), + ), + ] diff --git a/sapl/base/migrations/0032_merge_20190219_0941.py b/sapl/base/migrations/0032_merge_20190219_0941.py new file mode 100644 index 000000000..0f30001e6 --- /dev/null +++ b/sapl/base/migrations/0032_merge_20190219_0941.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-02-19 12:41 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0031_auto_20190218_1109'), + ('base', '0030_appconfig_escolher_numero_materia_proposicao'), + ] + + operations = [ + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 5caf8b2c0..87e497e84 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -1,25 +1,31 @@ -import reversion from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models.signals import post_migrate from django.db.utils import DEFAULT_DB_ALIAS from django.utils.translation import ugettext_lazy as _ -#from model_utils import Choices +import reversion + from sapl.utils import (LISTA_DE_UFS, YES_NO_CHOICES, get_settings_auth_user_model, models_with_gr_for_model) -TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensiva')), - ('R', _('Restritiva'))) +DOC_ADM_OSTENSIVO = 'O' +DOC_ADM_RESTRITIVO = 'R' + +TIPO_DOCUMENTO_ADMINISTRATIVO = ((DOC_ADM_OSTENSIVO, _('Ostensiva')), + (DOC_ADM_RESTRITIVO, _('Restritiva'))) + +RELATORIO_ATOS_ACESSADOS = (('S', _('Sim')), + ('N', _('Não'))) SEQUENCIA_NUMERACAO = (('A', _('Sequencial por ano')), ('L', _('Sequencial por legislatura')), ('U', _('Sequencial único'))) ESFERA_FEDERACAO_CHOICES = (('M', _('Municipal')), - ('E', _('Estadual')), - ('F', _('Federal')), -) + ('E', _('Estadual')), + ('F', _('Federal')), + ) ASSINATURA_ATA_CHOICES = ( ('M', _('Mesa Diretora da Sessão')), @@ -84,6 +90,11 @@ class AppConfig(models.Model): verbose_name=_('Visibilidade dos Documentos Administrativos'), choices=TIPO_DOCUMENTO_ADMINISTRATIVO, default='O') + estatisticas_acesso_normas = models.CharField( + max_length=1, + verbose_name=_('Estatísticas de acesso a normas'), + choices=RELATORIO_ATOS_ACESSADOS, default='N') + sequencia_numeracao = models.CharField( max_length=1, verbose_name=_('Sequência de numeração'), @@ -92,7 +103,7 @@ class AppConfig(models.Model): esfera_federacao = models.CharField( max_length=1, blank=True, - default = "", + default="", verbose_name=_('Esfera Federação'), choices=ESFERA_FEDERACAO_CHOICES) @@ -149,6 +160,14 @@ class AppConfig(models.Model): verbose_name=_('Protocolar proposição somente com recibo?'), choices=YES_NO_CHOICES, default=True) + protocolo_manual = models.BooleanField( + verbose_name=_('Informar data e hora de protocolo?'), + choices=YES_NO_CHOICES, default=False) + + escolher_numero_materia_proposicao = models.BooleanField( + verbose_name=_('Indicar número da matéria a ser gerada na proposição?'), + choices=YES_NO_CHOICES, default=False) + class Meta: verbose_name = _('Configurações da Aplicação') verbose_name_plural = _('Configurações da Aplicação') diff --git a/sapl/base/search_indexes.py b/sapl/base/search_indexes.py index f1ec87ddd..0e0283ba8 100644 --- a/sapl/base/search_indexes.py +++ b/sapl/base/search_indexes.py @@ -1,6 +1,4 @@ import os.path -import re -import string import textract import logging @@ -8,6 +6,7 @@ from django.db.models import F, Q, Value from django.db.models.fields import TextField from django.db.models.functions import Concat from django.template import loader +from haystack import connections from haystack.constants import Indexable from haystack.fields import CharField from haystack.indexes import SearchIndex @@ -24,6 +23,7 @@ from sapl.utils import RemoveTag class TextExtractField(CharField): + backend = None logger = logging.getLogger(__name__) def __init__(self, **kwargs): @@ -34,24 +34,20 @@ class TextExtractField(CharField): self.model_attr = (self.model_attr, ) def solr_extraction(self, arquivo): - extracted_data = self._get_backend(None).extract_file_contents( - arquivo)['contents'] - # Remove as tags xml - self.logger.debug("Removendo as tags xml.") - extracted_data = re.sub('<[^>]*>', '', extracted_data) - # Remove tags \t e \n - self.logger.debug("Removendo as \t e \n.") - extracted_data = extracted_data.replace( - '\n', ' ').replace('\t', ' ') - # Remove sinais de pontuação - self.logger.debug("Removendo sinais de pontuação.") - extracted_data = re.sub('[' + string.punctuation + ']', - ' ', extracted_data) - # Remove espaços múltiplos - self.logger.debugger("Removendo espaços múltiplos.") - extracted_data = " ".join(extracted_data.split()) - - return extracted_data + if not self.backend: + self.backend = connections['default'].get_backend() + try: + with open(arquivo.path, 'rb') as f: + content = self.backend.extract_file_contents(f) + if not content or not content['contents']: + return '' + data = content['contents'] + except Exception as e: + print('erro processando arquivo: ' % arquivo.path) + self.logger.error(arquivo.path) + self.logger.error('erro processando arquivo: ' % arquivo.path) + data = '' + return data def whoosh_extraction(self, arquivo): @@ -66,11 +62,11 @@ class TextExtractField(CharField): language='pt-br').decode('utf-8').replace('\n', ' ').replace( '\t', ' ') - def print_error(self, arquivo): - self.logger.error("Erro inesperado processando arquivo: {}".format(arquivo.path)) - msg = 'Erro inesperado processando arquivo: %s' % ( - arquivo.path) - print(msg) + def print_error(self, arquivo, error): + msg = 'Erro inesperado processando arquivo %s erro: %s' % ( + arquivo.path, error) + print(msg, error) + self.logger.error(msg, error) def file_extractor(self, arquivo): if not os.path.exists(arquivo.path) or \ @@ -81,9 +77,9 @@ class TextExtractField(CharField): if SOLR_URL: try: return self.solr_extraction(arquivo) - except Exception as e: - self.logger.error("Erro no arquivo {}. ".format(arquivo.path) + str(e)) - self.print_error(arquivo) + 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 @@ -91,13 +87,13 @@ class TextExtractField(CharField): try: self.logger.debug("Tentando whoosh_extraction no arquivo {}".format(arquivo.path)) return self.whoosh_extraction(arquivo) - except ExtensionNotSupported as e: - self.logger.error("Erro no arquivo {}".format(arquivo.path) + str(e)) - print(str(e)) - except Exception as e2: - self.logger.error(str(e)) - print(str(e2)) 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): @@ -133,7 +129,9 @@ class TextExtractField(CharField): value = getattr(obj, attr) if not value: continue - data += getattr(self, func)(value) + data += getattr(self, func)(value) + ' ' + + data = data.replace('\n', ' ') return data @@ -159,6 +157,10 @@ class DocumentoAcessorioIndex(SearchIndex, Indexable): ) ) + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.text.search_index = self + def get_model(self): return self.model diff --git a/sapl/base/templatetags/common_tags.py b/sapl/base/templatetags/common_tags.py index 89f781558..3c72b1654 100644 --- a/sapl/base/templatetags/common_tags.py +++ b/sapl/base/templatetags/common_tags.py @@ -1,9 +1,7 @@ -import logging - -from compressor.utils import get_class from django import template -from django.conf import settings from django.template.defaultfilters import stringfilter +from django.utils.safestring import mark_safe +from webpack_loader import utils from sapl.base.models import AppConfig from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa, Proposicao @@ -15,6 +13,15 @@ from sapl.utils import filiacao_data, SEPARADOR_HASH_PROPOSICAO register = template.Library() +def get_class(class_string): + if not hasattr(class_string, '__bases__'): + class_string = str(class_string) + dot = class_string.rindex('.') + mod_name, class_name = class_string[:dot], class_string[dot + 1:] + if class_name: + return getattr(__import__(mod_name, {}, {}, [str('')]), class_name) + + @register.simple_tag def define(arg): return arg @@ -228,7 +235,7 @@ def file_extension(value): def cronometro_to_seconds(value): if not AppConfig.attr('cronometro_' + value): return 0 - + return AppConfig.attr('cronometro_' + value).seconds @@ -269,3 +276,13 @@ def filiacao_data_filter(parlamentar, data_inicio): @register.filter def filiacao_intervalo_filter(parlamentar, date_range): return filiacao_data(parlamentar, date_range[0], date_range[1]) + + +@register.simple_tag +def render_chunk_vendors(extension=None): + try: + tags = utils.get_as_tags( + 'chunk-vendors', extension=extension, config='DEFAULT', attrs='') + return mark_safe('\n'.join(tags)) + except: + return '' diff --git a/sapl/base/templatetags/menus.py b/sapl/base/templatetags/menus.py index 289cd20e6..5b526002a 100644 --- a/sapl/base/templatetags/menus.py +++ b/sapl/base/templatetags/menus.py @@ -1,3 +1,5 @@ +import logging + from django import template from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ @@ -6,6 +8,8 @@ import yaml register = template.Library() +logger = logging.getLogger(__name__) + @register.inclusion_tag('menus/menu.html', takes_context=True) def menu(context, path=None): @@ -84,7 +88,7 @@ def nav_run(context, path=None): menu = yaml.load(rendered) resolve_urls_inplace(menu, root_pk, rm, context) except Exception as e: - print(_("""Erro na conversão do yaml %s. App: %s. + raise Exception(_("""Erro na conversão do yaml %s. App: %s. Erro: %s """) % ( @@ -113,25 +117,61 @@ def resolve_urls_inplace(menu, pk, rm, context): menu['url'] = '' menu['active'] = '' else: - if ':' in url_name: + if '/' in url_name: + pass + elif ':' in url_name: try: - menu['url'] = reverse('%s' % menu['url'], - kwargs={'pk': pk}) + menu['url'] = reverse('%s' % menu['url']) except: try: - menu['url'] = reverse('%s' % menu['url']) + menu['url'] = reverse('%s' % menu['url'], + kwargs={'pk': pk}) except: - pass + # tem que ser root_pk pois quando está sendo + # renderizado um detail, update, delete + # e ainda sim é necessário colocar o menu, + # nestes, casos o pk da url é do detail, e não + # do master, porém, os menus do subnav, apontam para + # outras áreas que as urls destas são construídas + # com pk do master, e não do detail... por isso + # no contexto deve ter, ou root_pk, ou object + # sendo que qualquer um dos dois,deverá ser o + # master. + # Estes detalhes são relevantes quando usa-se + # o menu isolado. Por outro lado, quando usado + # conjuntamente com o crud, este configura o contexto + # como se deve para o menus.py + log = """ + Erro na construção do Menu: + menu: {} + url: {} + 1) Verifique se a url existe + 2) Se existe no contexto um desses itens: + - context['root_pk'] pk do master + - context['object'] objeto do master + """.format(menu['title'], menu['url']) + logger.error(log) + raise Exception(log) + else: try: menu['url'] = reverse('%s:%s' % ( - rm.app_name, menu['url']), kwargs={'pk': pk}) + rm.app_name, menu['url'])) except: try: menu['url'] = reverse('%s:%s' % ( - rm.app_name, menu['url'])) + rm.app_name, menu['url']), kwargs={'pk': pk}) except: - pass + log = """Erro na construção do Menu: + menu: {} + url: {} + 1) Verifique se a url existe + 2) Se existe no contexto um desses itens: + - context['root_pk'] pk do master + - context['object'] objeto do master + """.format(menu['title'], menu['url']) + logger.error(log) + raise Exception(log) menu['active'] = 'active'\ if context['request'].path == menu['url'] else '' diff --git a/sapl/base/tests/test_login.py b/sapl/base/tests/test_login.py index 91665ea8d..3122fcb7e 100755 --- a/sapl/base/tests/test_login.py +++ b/sapl/base/tests/test_login.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -import pytest from django.contrib.auth import get_user_model +import pytest + pytestmark = pytest.mark.django_db @@ -12,14 +13,15 @@ def user(): def test_login_aparece_na_barra_para_usuario_nao_logado(client): response = client.get('/') - assert '' in str( + assert '' in str( response.content) def test_username_do_usuario_logado_aparece_na_barra(client, user): assert client.login(username='jfirmino', password='123') response = client.get('/') - assert 'Login' not in str(response.content) + assert 'Login' not in str( + response.content) assert 'jfirmino' in str(response.content) assert 'Sair' in str(response.content) diff --git a/sapl/base/urls.py b/sapl/base/urls.py index 93a5b1cd3..005f12d71 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -15,20 +15,33 @@ from .apps import AppConfig from .forms import LoginForm, NovaSenhaForm, RecuperarSenhaForm from .views import (AlterarSenha, AppConfigCrud, CasaLegislativaCrud, CreateUsuarioView, DeleteUsuarioView, EditUsuarioView, - HelpTopicView, ListarUsuarioView, LogotipoView, - RelatorioAtasView, RelatorioAudienciaView, + HelpTopicView, PesquisarUsuarioView, LogotipoView, + RelatorioAtasView, RelatorioAudienciaView, RelatorioDataFimPrazoTramitacaoView, RelatorioHistoricoTramitacaoView, RelatorioMateriasPorAnoAutorTipoView, RelatorioMateriasPorAutorView, RelatorioMateriasTramitacaoView, - RelatorioPresencaSessaoView, - RelatorioReuniaoView, SaplSearchView) + RelatorioPresencaSessaoView, + RelatorioReuniaoView, SaplSearchView, + RelatorioNormasPublicadasMesView, + RelatorioNormasVigenciaView, + EstatisticasAcessoNormas, + RelatoriosListView, + ListarInconsistenciasView, ListarProtocolosDuplicadosView, + ListarProtocolosComMateriasView, + ListarMatProtocoloInexistenteView, + ListarParlMandatosIntersecaoView, + ListarAutoresDuplicadosView, + ListarBancadaComissaoAutorExternoView, + ListarLegislaturaInfindavelView, + ListarMandatoSemDataInicioView) + app_name = AppConfig.name admin_user = [ - url(r'^sistema/usuario/$', ListarUsuarioView.as_view(), name='user_list'), + url(r'^sistema/usuario/$', PesquisarUsuarioView.as_view(), name='usuario'), url(r'^sistema/usuario/create$', CreateUsuarioView.as_view(), name='user_create'), url(r'^sistema/usuario/(?P\d+)/edit$', EditUsuarioView.as_view(), name='user_edit'), url(r'^sistema/usuario/(?P\d+)/delete$', DeleteUsuarioView.as_view(), name='user_delete') @@ -84,10 +97,16 @@ urlpatterns = [ url(r'^sistema/app-config/', include(AppConfigCrud.get_urls())), # TODO mover estas telas para a app 'relatorios' - url(r'^sistema/relatorios/$', TemplateView.as_view( - template_name='base/relatorios_list.html'), name='relatorios_list'), + url(r'^sistema/relatorios/$', + RelatoriosListView.as_view(), name='relatorios_list'), url(r'^sistema/relatorios/materia-por-autor$', RelatorioMateriasPorAutorView.as_view(), name='materia_por_autor'), + url(r'^sistema/relatorios/relatorio-por-mes$', + RelatorioNormasPublicadasMesView.as_view(), name='normas_por_mes'), + url(r'^sistema/relatorios/relatorio-por-vigencia$', + RelatorioNormasVigenciaView.as_view(), name='normas_por_vigencia'), + url(r'^sistema/relatorios/estatisticas-acesso$', + EstatisticasAcessoNormas.as_view(), name='estatisticas_acesso'), url(r'^sistema/relatorios/materia-por-ano-autor-tipo$', RelatorioMateriasPorAnoAutorTipoView.as_view(), name='materia_por_ano_autor_tipo'), @@ -117,6 +136,33 @@ urlpatterns = [ '(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})$', ConfirmarEmailView.as_view(), name='confirmar_email'), + url(r'^sistema/inconsistencias/$', + ListarInconsistenciasView.as_view(), + name='lista_inconsistencias'), + url(r'^sistema/inconsistencias/protocolos_duplicados$', + ListarProtocolosDuplicadosView.as_view(), + name='lista_protocolos_duplicados'), + url(r'^sistema/inconsistencias/protocolos_com_materias$', + ListarProtocolosComMateriasView.as_view(), + name='lista_protocolos_com_materias'), + url(r'^sistema/inconsistencias/materias_protocolo_inexistente$', + ListarMatProtocoloInexistenteView.as_view(), + name='lista_materias_protocolo_inexistente'), + url(r'^sistema/inconsistencias/mandato_sem_data_inicio', + ListarMandatoSemDataInicioView.as_view(), + name='lista_mandato_sem_data_inicio'), + url(r'^sistema/inconsistencias/parlamentares_mandatos_intersecao$', + ListarParlMandatosIntersecaoView.as_view(), + name='lista_parlamentares_mandatos_intersecao'), + url(r'^sistema/inconsistencias/autores_duplicados$', + ListarAutoresDuplicadosView.as_view(), + name='lista_autores_duplicados'), + url(r'^sistema/inconsistencias/bancada_comissao_autor_externo$', + ListarBancadaComissaoAutorExternoView.as_view(), + name='lista_bancada_comissao_autor_externo'), + url(r'^sistema/inconsistencias/legislatura_infindavel$', + ListarLegislaturaInfindavelView.as_view(), + name='lista_legislatura_infindavel'), # 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 ca122ce9f..e41b9e9de 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1,17 +1,23 @@ +import collections +import itertools +import datetime import logging import os +from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.mixins import PermissionRequiredMixin -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from django.contrib.auth.tokens import default_token_generator -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError from django.core.mail import send_mail -from django.core.urlresolvers import reverse -from django.db.models import Count +from django.core.urlresolvers import reverse, reverse_lazy +from django.db import connection +from django.db.models import Count, Q, ProtectedError from django.http import Http404, HttpResponseRedirect from django.template import TemplateDoesNotExist from django.template.loader import get_template +from django.utils import timezone from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.translation import string_concat @@ -28,12 +34,16 @@ from sapl.base.forms import AutorForm, AutorFormForAdmin, TipoAutorForm from sapl.base.models import Autor, TipoAutor from sapl.comissoes.models import Reuniao, Comissao from sapl.crud.base import CrudAux, make_pagination -from sapl.materia.models import (Autoria, MateriaLegislativa, +from sapl.materia.models import (Autoria, MateriaLegislativa, Proposicao, TipoMateriaLegislativa, StatusTramitacao, UnidadeTramitacao) +from sapl.norma.models import (NormaJuridica, NormaEstatisticas) +from sapl.parlamentares.models import Parlamentar, Legislatura, Mandato +from sapl.protocoloadm.models import Protocolo from sapl.sessao.models import (PresencaOrdemDia, SessaoPlenaria, - SessaoPlenariaPresenca) -from sapl.utils import (parlamentares_ativos, - show_results_filter_set, mail_service_configured) + SessaoPlenariaPresenca, Bancada) +from sapl.utils import (parlamentares_ativos, gerar_hash_arquivo, SEPARADOR_HASH_PROPOSICAO, + show_results_filter_set, mail_service_configured, + intervalos_tem_intersecao,) from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, RelatorioAtasFilterSet, @@ -45,7 +55,9 @@ from .forms import (AlterarSenhaForm, CasaLegislativaForm, RelatorioMateriasTramitacaoilterSet, RelatorioPresencaSessaoFilterSet, RelatorioReuniaoFilterSet, UsuarioCreateForm, - UsuarioEditForm) + UsuarioEditForm, RelatorioNormasMesFilterSet, + RelatorioNormasVigenciaFilterSet, + EstatisticasAcessoNormasForm, UsuarioFilterSet) from .models import AppConfig, CasaLegislativa @@ -276,6 +288,20 @@ class AutorCrud(CrudAux): return url_reverse +class RelatoriosListView(TemplateView): + template_name='base/relatorios_list.html' + + def get_context_data(self, **kwargs): + context = super(TemplateView, self).get_context_data(**kwargs) + estatisticas_acesso_normas = AppConfig.objects.first().estatisticas_acesso_normas + if estatisticas_acesso_normas == 'S': + context['estatisticas_acesso_normas'] = True + else: + context['estatisticas_acesso_normas'] = False + + return context + + class RelatorioAtasView(FilterView): model = SessaoPlenaria filterset_class = RelatorioAtasFilterSet @@ -355,8 +381,16 @@ class RelatorioPresencaSessaoView(FilterView): # Completa o dicionario as informacoes parlamentar/sessao/ordem parlamentares_presencas = [] for i, p in enumerate(parlamentares_qs): + m = p.mandato_set.filter(Q(data_inicio_mandato__lte=_range[0], data_fim_mandato__gte=_range[1]) | + Q(data_inicio_mandato__lte=_range[0], data_fim_mandato__isnull=True) | + Q(data_inicio_mandato__gte=_range[0], data_fim_mandato__lte=_range[1]) | + # mandato suplente + Q(data_inicio_mandato__gte=_range[0], data_fim_mandato__lte=_range[1])) + + m = m.last() parlamentares_presencas.append({ 'parlamentar': p, + 'titular': m.titular if m else False, 'sessao_porc': 0, 'ordemdia_porc': 0 }) @@ -744,35 +778,561 @@ class RelatorioMateriasPorAutorView(FilterView): return context -class ListarUsuarioView(PermissionRequiredMixin, ListView): +class RelatorioNormasPublicadasMesView(FilterView): + model = NormaJuridica + filterset_class = RelatorioNormasMesFilterSet + template_name = 'base/RelatorioNormaMes_filter.html' + + def get_context_data(self, **kwargs): + context = super(RelatorioNormasPublicadasMesView, + self).get_context_data(**kwargs) + context['title'] = _('Normas') + + # Verifica se os campos foram preenchidos + if not self.filterset.form.is_valid(): + return context + + qr = self.request.GET.copy() + context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + context['ano'] = self.request.GET['ano'] + + normas_mes = collections.OrderedDict() + meses = {1: 'Janeiro', 2: 'Fevereiro', 3:'Março', 4: 'Abril', 5: 'Maio', 6:'Junho', + 7: 'Julho', 8: 'Agosto', 9:'Setembro', 10:'Outubro', 11:'Novembro', 12:'Dezembro'} + for norma in context['object_list']: + if not meses[norma.data.month] in normas_mes: + normas_mes[meses[norma.data.month]] = [] + normas_mes[meses[norma.data.month]].append(norma) + + context['normas_mes'] = normas_mes + + quant_normas_mes = {} + for key in normas_mes.keys(): + quant_normas_mes[key] = len(normas_mes[key]) + + context['quant_normas_mes'] = quant_normas_mes + + return context + + +class RelatorioNormasVigenciaView(FilterView): + model = NormaJuridica + filterset_class = RelatorioNormasVigenciaFilterSet + template_name = 'base/RelatorioNormasVigencia_filter.html' + + def get_filterset_kwargs(self, filterset_class): + super(RelatorioNormasVigenciaView, + self).get_filterset_kwargs(filterset_class) + + kwargs = {'data': self.request.GET or None} + qs = self.get_queryset().order_by('data').distinct() + if kwargs['data']: + ano = kwargs['data']['ano'] + vigencia = kwargs['data']['vigencia'] + if ano: + qs = qs.filter(ano=ano) + + if vigencia == 'True': + qs_dt_not_null = qs.filter(data_vigencia__isnull=True) + qs = (qs_dt_not_null | qs.filter(data_vigencia__gte=datetime.datetime.now().date())).distinct() + else: + qs = qs.filter(data_vigencia__lt=datetime.datetime.now().date()) + + kwargs.update({ + 'queryset': qs + }) + return kwargs + + + def get_context_data(self, **kwargs): + context = super(RelatorioNormasVigenciaView, + self).get_context_data(**kwargs) + context['title'] = _('Normas por vigência') + + # Verifica se os campos foram preenchidos + if not self.filterset.form.is_valid(): + return context + + normas_totais = NormaJuridica.objects.filter(ano=self.request.GET['ano']) + + context['quant_total'] = len(normas_totais) + if self.request.GET['vigencia'] == 'True': + context['vigencia'] = 'Vigente' + context['quant_vigente'] = len(context['object_list']) + context['quant_nao_vigente'] = context['quant_total'] - context['quant_vigente'] + else: + context['vigencia'] = 'Não vigente' + context['quant_nao_vigente'] = len(context['object_list']) + context['quant_vigente'] = context['quant_total'] - context['quant_nao_vigente'] + + qr = self.request.GET.copy() + context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + context['ano'] = self.request.GET['ano'] + + return context + + +class EstatisticasAcessoNormas(TemplateView): + template_name = 'base/EstatisticasAcessoNormas_filter.html' + + def get(self, request, *args, **kwargs): + context = super(EstatisticasAcessoNormas, + self).get_context_data(**kwargs) + context['title'] = _('Normas') + + form = EstatisticasAcessoNormasForm(request.GET or None) + context['form'] = form + + if not form.is_valid(): + return self.render_to_response(context) + + context['ano'] = self.request.GET['ano'] + + query = ''' + select norma_id, ano, extract(month from horario_acesso) as mes, count(*) + from norma_normaestatisticas + where ano = {} + group by mes, ano, norma_id + order by mes desc; + '''.format(context['ano']) + cursor = connection.cursor() + cursor.execute(query) + rows = cursor.fetchall() + + normas_mes = collections.OrderedDict() + meses = {1: 'Janeiro', 2: 'Fevereiro', 3:'Março', 4: 'Abril', 5: 'Maio', 6:'Junho', + 7: 'Julho', 8: 'Agosto', 9:'Setembro', 10:'Outubro', 11:'Novembro', 12:'Dezembro'} + + for row in rows: + if not meses[int(row[2])] in normas_mes: + normas_mes[meses[int(row[2])]] = [] + norma_est = [NormaJuridica.objects.get(id=row[0]), row[3]] + normas_mes[meses[int(row[2])]].append(norma_est) + + # Ordena por acesso e limita em 5 + for n in normas_mes: + sorted_by_value = sorted(normas_mes[n], key=lambda kv: kv[1], reverse=True) + normas_mes[n] = sorted_by_value[0:5] + + context['normas_mes'] = normas_mes + + return self.render_to_response(context) + + +class ListarInconsistenciasView(PermissionRequiredMixin, ListView): + model = get_user_model() + template_name = 'base/lista_inconsistencias.html' + context_object_name = 'tabela_inconsistencias' + permission_required = ('base.list_appconfig',) + + def get_queryset(self): + tabela = [] + tabela.append( + ('protocolos_duplicados', + 'Protocolos duplicados', + len(protocolos_duplicados()) + ) + ) + tabela.append( + ('protocolos_com_materias', + 'Protocolos que excedem o limite de matérias vinculadas', + len(protocolos_com_materias()) + ) + ) + tabela.append( + ('materias_protocolo_inexistente', + 'Matérias Legislativas com protocolo inexistente', + len(materias_protocolo_inexistente()) + ) + ) + tabela.append( + ('mandato_sem_data_inicio', + 'Mandatos sem data inicial', + len(mandato_sem_data_inicio()) + ) + ) + tabela.append( + ('parlamentares_mandatos_intersecao', + 'Parlamentares com mandatos com interseção', + len(parlamentares_mandatos_intersecao()) + ) + ) + tabela.append( + ('autores_duplicados', + 'Autores duplicados', + len(autores_duplicados()) + ) + ) + tabela.append( + ('bancada_comissao_autor_externo', + 'Bancadas e Comissões com autor externo', + len(bancada_comissao_autor_externo()) + ) + ) + tabela.append( + ('legislatura_infindavel', + 'Legislaturas sem data fim', + len(legislatura_infindavel()) + ) + ) + + return tabela + + +def legislatura_infindavel(): + return Legislatura.objects.filter(data_fim__isnull=True).order_by('-numero') + + +class ListarLegislaturaInfindavelView(PermissionRequiredMixin, ListView): + model = get_user_model() + template_name = 'base/legislatura_infindavel.html' + context_object_name = 'legislatura_infindavel' + permission_required = ('base.list_appconfig',) + paginate_by = 10 + + def get_queryset(self): + return legislatura_infindavel() + + def get_context_data(self, **kwargs): + context = super( + ListarLegislaturaInfindavelView, self + ).get_context_data(**kwargs) + paginator = context['paginator'] + page_obj = context['page_obj'] + context['page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + context[ + 'NO_ENTRIES_MSG' + ] = 'Nenhuma encontrada.' + return context + + +def bancada_comissao_autor_externo(): + tipo_autor_externo = TipoAutor.objects.filter(descricao='Externo') + + lista_bancada_autor_externo = [] + for bancada in Bancada.objects.all().order_by('nome'): + autor_externo = bancada.autor.filter(tipo=tipo_autor_externo) + + if autor_externo: + q_autor_externo = bancada.autor.get(tipo=tipo_autor_externo) + lista_bancada_autor_externo.append( + (q_autor_externo, bancada, 'Bancada', 'sistema/bancada') + ) + + lista_comissao_autor_externo = [] + for comissao in Comissao.objects.all().order_by('nome'): + autor_externo = comissao.autor.filter(tipo=tipo_autor_externo) + + if autor_externo: + q_autor_externo = comissao.autor.get(tipo=tipo_autor_externo) + lista_comissao_autor_externo.append( + (q_autor_externo, comissao, 'Comissão', 'comissao') + ) + + return lista_bancada_autor_externo + lista_comissao_autor_externo + + +class ListarBancadaComissaoAutorExternoView(PermissionRequiredMixin, ListView): + model = get_user_model() + template_name = 'base/bancada_comissao_autor_externo.html' + context_object_name = 'bancada_comissao_autor_externo' + permission_required = ('base.list_appconfig',) + paginate_by = 10 + + def get_queryset(self): + return bancada_comissao_autor_externo() + + def get_context_data(self, **kwargs): + context = super( + ListarBancadaComissaoAutorExternoView, self + ).get_context_data(**kwargs) + paginator = context['paginator'] + page_obj = context['page_obj'] + context['page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + context[ + 'NO_ENTRIES_MSG' + ] = 'Nenhum encontrado.' + return context + + +def autores_duplicados(): + return [autor.values() for autor in Autor.objects.values('nome', 'tipo__descricao').annotate(count=Count('nome')).filter(count__gt=1)] + + +class ListarAutoresDuplicadosView(PermissionRequiredMixin, ListView): + model = get_user_model() + template_name = 'base/autores_duplicados.html' + context_object_name = 'autores_duplicados' + permission_required = ('base.list_appconfig',) + paginate_by = 10 + + def get_queryset(self): + return autores_duplicados() + + def get_context_data(self, **kwargs): + context = super( + ListarAutoresDuplicadosView, self).get_context_data(**kwargs) + paginator = context['paginator'] + page_obj = context['page_obj'] + context['page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + context[ + 'NO_ENTRIES_MSG' + ] = 'Nenhum encontrado.' + return context + + +def parlamentares_mandatos_intersecao(): + intersecoes = [] + + for parlamentar in Parlamentar.objects.all().order_by('nome_completo'): + mandatos = parlamentar.mandato_set.all() + combinacoes = itertools.combinations(mandatos, 2) + + for c in combinacoes: + data_inicio_mandato1 = c[0].data_inicio_mandato + data_fim_mandato1 = c[0].data_fim_mandato if c[0].data_fim_mandato else timezone.now().date() + + data_inicio_mandato2 = c[1].data_inicio_mandato + data_fim_mandato2 = c[1].data_fim_mandato if c[1].data_fim_mandato else timezone.now().date() + + if data_inicio_mandato1 and data_inicio_mandato2: + exists = intervalos_tem_intersecao( + data_inicio_mandato1, data_fim_mandato1, + data_inicio_mandato2, data_fim_mandato2) + if exists: + intersecoes.append((parlamentar, c[0], c[1])) + + return intersecoes + + +class ListarParlMandatosIntersecaoView(PermissionRequiredMixin, ListView): + model = get_user_model() + template_name = 'base/parlamentares_mandatos_intersecao.html' + context_object_name = 'parlamentares_mandatos_intersecao' + permission_required = ('base.list_appconfig',) + paginate_by = 10 + + def get_queryset(self): + return parlamentares_mandatos_intersecao() + + def get_context_data(self, **kwargs): + context = super( + ListarParlMandatosIntersecaoView, self).get_context_data(**kwargs) + paginator = context['paginator'] + page_obj = context['page_obj'] + context['page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + context[ + 'NO_ENTRIES_MSG' + ] = 'Nenhum encontrado.' + return context + + +def mandato_sem_data_inicio(): + return Mandato.objects.filter(data_inicio_mandato__isnull=True).order_by('parlamentar') + + +class ListarMandatoSemDataInicioView(PermissionRequiredMixin, ListView): + model = get_user_model() + template_name = 'base/mandato_sem_data_inicio.html' + context_object_name = 'mandato_sem_data_inicio' + permission_required = ('base.list_appconfig',) + paginate_by = 10 + + def get_queryset(self): + return mandato_sem_data_inicio() + + def get_context_data(self, **kwargs): + context = super( + ListarMandatoSemDataInicioView, self + ).get_context_data(**kwargs) + paginator = context['paginator'] + page_obj = context['page_obj'] + context['page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + context[ + 'NO_ENTRIES_MSG' + ] = 'Nenhum encontrada.' + return context + + +def materias_protocolo_inexistente(): + materias = [] + for materia in MateriaLegislativa.objects.filter(numero_protocolo__isnull=False).order_by('-ano', 'numero'): + exists = Protocolo.objects.filter( + ano=materia.ano, numero=materia.numero_protocolo).exists() + if not exists: + materias.append( + (materia, materia.ano, materia.numero_protocolo)) + return materias + + +class ListarMatProtocoloInexistenteView(PermissionRequiredMixin, ListView): model = get_user_model() - template_name = 'auth/user_list.html' - context_object_name = 'user_list' + template_name = 'base/materias_protocolo_inexistente.html' + context_object_name = 'materias_protocolo_inexistente' permission_required = ('base.list_appconfig',) paginate_by = 10 def get_queryset(self): - qs = super(ListarUsuarioView, self).get_queryset() - return qs.order_by('username') + return materias_protocolo_inexistente() def get_context_data(self, **kwargs): - context = super(ListarUsuarioView, self).get_context_data(**kwargs) + context = super( + ListarMatProtocoloInexistenteView, self + ).get_context_data(**kwargs) paginator = context['paginator'] page_obj = context['page_obj'] context['page_range'] = make_pagination( page_obj.number, paginator.num_pages) - context['NO_ENTRIES_MSG'] = 'Nenhum usuário cadastrado.' + context[ + 'NO_ENTRIES_MSG' + ] = 'Nenhuma encontrada.' return context +def protocolos_com_materias(): + protocolos = {} + + for m in MateriaLegislativa.objects.filter(numero_protocolo__isnull=False).order_by('-ano', 'numero_protocolo'): + if Protocolo.objects.filter(numero=m.numero_protocolo, ano=m.ano).exists(): + key = "{}/{}".format(m.numero_protocolo, m.ano) + val = protocolos.get(key, list()) + val.append(m) + protocolos[key] = val + + return [(v[0], len(v)) for (k, v) in protocolos.items() if len(v) > 1] + + +class ListarProtocolosComMateriasView(PermissionRequiredMixin, ListView): + model = get_user_model() + template_name = 'base/protocolos_com_materias.html' + context_object_name = 'protocolos_com_materias' + permission_required = ('base.list_appconfig',) + paginate_by = 10 + + def get_queryset(self): + return protocolos_com_materias() + + def get_context_data(self, **kwargs): + context = super( + ListarProtocolosComMateriasView, self).get_context_data(**kwargs) + paginator = context['paginator'] + page_obj = context['page_obj'] + context['page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + context[ + 'NO_ENTRIES_MSG' + ] = 'Nenhum encontrado.' + return context + + +def protocolos_duplicados(): + protocolos = {} + for p in Protocolo.objects.order_by('-ano', 'numero'): + key = "{}/{}".format(p.numero, p.ano) + val = protocolos.get(key, list()) + val.append(p) + protocolos[key] = val + + return [(v[0], len(v)) for (k, v) in protocolos.items() if len(v) > 1] + +class ListarProtocolosDuplicadosView(PermissionRequiredMixin, ListView): + model = get_user_model() + template_name = 'base/protocolos_duplicados.html' + context_object_name = 'protocolos_duplicados' + permission_required = ('base.list_appconfig',) + paginate_by = 10 + + def get_queryset(self): + return protocolos_duplicados() + + def get_context_data(self, **kwargs): + context = super( + ListarProtocolosDuplicadosView, self).get_context_data(**kwargs) + paginator = context['paginator'] + page_obj = context['page_obj'] + context['page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + context[ + 'NO_ENTRIES_MSG' + ] = 'Nenhum encontrado.' + return context + + +class PesquisarUsuarioView(PermissionRequiredMixin, FilterView): + model = User + filterset_class = UsuarioFilterSet + permission_required = ('base.list_appconfig',) + paginate_by = 10 + + def get_filterset_kwargs(self, filterset_class): + super(PesquisarUsuarioView, + self).get_filterset_kwargs(filterset_class) + + kwargs = {'data': self.request.GET or None} + + qs = self.get_queryset().order_by('username').distinct() + + kwargs.update({ + 'queryset': qs, + }) + return kwargs + + def get_context_data(self, **kwargs): + context = super(PesquisarUsuarioView, + self).get_context_data(**kwargs) + + paginator = context['paginator'] + page_obj = context['page_obj'] + + context['page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + + context['NO_ENTRIES_MSG'] = 'Nenhum usuário encontrado!' + + context['title'] = _('Usuários') + + return context + + def get(self, request, *args, **kwargs): + super(PesquisarUsuarioView, self).get(request) + + data = self.filterset.data + url = '' + if data: + url = "&" + str(self.request.environ['QUERY_STRING']) + if url.startswith("&page"): + ponto_comeco = url.find('username=') - 1 + url = url[ponto_comeco:] + + context = self.get_context_data(filter=self.filterset, + object_list=self.object_list, + filter_url=url, + numero_res=len(self.object_list) + ) + + context['show_results'] = show_results_filter_set( + self.request.GET.copy()) + + return self.render_to_response(context) + + class CreateUsuarioView(PermissionRequiredMixin, CreateView): model = get_user_model() form_class = UsuarioCreateForm - success_message = 'Usuário criado com sucesso' + success_message = 'Usuário criado com sucesso!' permission_required = ('base.add_appconfig',) def get_success_url(self): - return reverse('sapl.base:user_list') + return reverse('sapl.base:usuario') def form_valid(self, form): @@ -791,33 +1351,49 @@ class CreateUsuarioView(PermissionRequiredMixin, CreateView): for g in groups: g.user_set.add(new_user) + messages.success(self.request, self.success_message) return HttpResponseRedirect(self.get_success_url()) class DeleteUsuarioView(PermissionRequiredMixin, DeleteView): - model = get_user_model() + template_name = "crud/confirm_delete.html" permission_required = ('base.delete_appconfig',) + success_url = reverse_lazy('sapl.base:usuario') + success_message = "Usuário removido com sucesso!" - def get_success_url(self): - return reverse('sapl.base:user_list') + def delete(self, request, *args, **kwargs): + try: + super(DeleteUsuarioView, self).delete(request, *args, **kwargs) + except ProtectedError as exception: + error_url = reverse_lazy('sapl.base:user_delete', kwargs={'pk': self.kwargs['pk']}) + error_message = "O usuário não pode ser removido, pois é referenciado por:
    " - def get(self, request, *args, **kwargs): - return self.post(request, *args, **kwargs) + for e in exception.protected_objects: + error_message += '
  • {} - {}
  • '.format( + e._meta.verbose_name, e + ) + error_message += '
' + messages.error(self.request, error_message) + return HttpResponseRedirect(error_url) - def get_queryset(self): - qs = super(DeleteUsuarioView, self).get_queryset() - return qs.filter(id=self.kwargs['pk']) + messages.success(self.request, self.success_message) + return HttpResponseRedirect(self.success_url) + + @property + def cancel_url(self): + return reverse('sapl.base:user_edit', + kwargs={'pk': self.kwargs['pk']}) class EditUsuarioView(PermissionRequiredMixin, UpdateView): model = get_user_model() form_class = UsuarioEditForm - success_message = 'Usuário editado com sucesso' + success_message = 'Usuário editado com sucesso!' permission_required = ('base.change_appconfig',) def get_success_url(self): - return reverse('sapl.base:user_list') + return reverse('sapl.base:usuario') def get_initial(self): initial = super(EditUsuarioView, self).get_initial() @@ -854,6 +1430,7 @@ class EditUsuarioView(PermissionRequiredMixin, UpdateView): for g in groups: g.user_set.add(user) + messages.success(self.request, self.success_message) return super(EditUsuarioView, self).form_valid(form) @@ -907,10 +1484,31 @@ class AppConfigCrud(CrudAux): class BaseMixin(CrudAux.BaseMixin): form_class = ConfiguracoesAppForm - list_url = '' create_url = '' + def form_valid(self, form): + recibo_prop_atual = AppConfig.objects.last().receber_recibo_proposicao + recibo_prop_novo = self.request.POST['receber_recibo_proposicao'] + if recibo_prop_novo == 'False' and recibo_prop_atual: + props = Proposicao.objects.filter(hash_code='') + for prop in props: + self.gerar_hash(prop) + return super().form_valid(form) + + def gerar_hash(self, inst): + inst.save() + if inst.texto_original: + try: + inst.hash_code = gerar_hash_arquivo( + inst.texto_original.path, str(inst.pk)) + except IOError: + raise ValidationError("Existem proposicoes com arquivos inexistentes.") + elif inst.texto_articulado.exists(): + ta = inst.texto_articulado.first() + inst.hash_code = 'P' + ta.hash() + SEPARADOR_HASH_PROPOSICAO + str(inst.pk) + inst.save() + class CreateView(CrudAux.CreateView): def get(self, request, *args, **kwargs): diff --git a/sapl/comissoes/forms.py b/sapl/comissoes/forms.py index 14d42f53e..9b0e69d72 100644 --- a/sapl/comissoes/forms.py +++ b/sapl/comissoes/forms.py @@ -12,11 +12,15 @@ from sapl.base.models import Autor, TipoAutor from sapl.comissoes.models import (Comissao, Composicao, DocumentoAcessorio, Participacao, Reuniao, Periodo) from sapl.parlamentares.models import Legislatura, Mandato, Parlamentar +from sapl.utils import FileFieldCheckMixin + class ComposicaoForm(forms.ModelForm): - comissao = forms.CharField(required=False, label='Comissao', widget=forms.HiddenInput()) + comissao = forms.CharField( + required=False, label='Comissao', widget=forms.HiddenInput()) logger = logging.getLogger(__name__) + class Meta: model = Composicao exclude = [] @@ -43,8 +47,8 @@ class ComposicaoForm(forms.ModelForm): if intersecao_periodo: self.logger.error('O período informado ({} a {})' - 'choca com períodos já ' - 'cadastrados para esta comissão'.format(periodo.data_inicio, periodo.data_fim)) + 'choca com períodos já ' + 'cadastrados para esta comissão'.format(periodo.data_inicio, periodo.data_fim)) raise ValidationError('O período informado ' 'choca com períodos já ' 'cadastrados para esta comissão') @@ -55,6 +59,7 @@ class ComposicaoForm(forms.ModelForm): class PeriodoForm(forms.ModelForm): logger = logging.getLogger(__name__) + class Meta: model = Periodo exclude = [] @@ -70,31 +75,29 @@ class PeriodoForm(forms.ModelForm): if data_fim and data_fim < data_inicio: self.logger.error('A Data Final ({}) é menor que ' - 'a Data Inicial({}).'.format(data_fim, data_inicio)) + 'a Data Inicial({}).'.format(data_fim, data_inicio)) raise ValidationError('A Data Final não pode ser menor que ' - 'a Data Inicial') + 'a Data Inicial') # Evita NoneType exception se não preenchida a data_fim if not data_fim: data_fim = data_inicio legislatura = Legislatura.objects.filter(data_inicio__lte=data_inicio, - data_fim__gte=data_fim, - ) + data_fim__gte=data_fim, + ) if not legislatura: self.logger.error('O período informado ({} a {})' - 'não está contido em uma única ' - 'legislatura existente'.format(data_inicio, data_fim)) + 'não está contido em uma única ' + 'legislatura existente'.format(data_inicio, data_fim)) raise ValidationError('O período informado ' 'deve estar contido em uma única ' 'legislatura existente') - return cleaned_data - class ParticipacaoCreateForm(forms.ModelForm): logger = logging.getLogger(__name__) @@ -122,9 +125,9 @@ class ParticipacaoCreateForm(forms.ModelForm): parlamentares = Mandato.objects.filter(qs, parlamentar__ativo=True ).prefetch_related('parlamentar').\ - values_list('parlamentar', - flat=True - ).distinct() + values_list('parlamentar', + flat=True + ).distinct() qs = Parlamentar.objects.filter(id__in=parlamentares).distinct().\ exclude(id__in=id_part) @@ -137,7 +140,6 @@ class ParticipacaoCreateForm(forms.ModelForm): qs = Parlamentar.objects.filter(id__in=ids) self.fields['parlamentar'].queryset = qs - def clean(self): cleaned_data = super(ParticipacaoCreateForm, self).clean() @@ -148,22 +150,23 @@ class ParticipacaoCreateForm(forms.ModelForm): data_desligamento = cleaned_data['data_desligamento'] if data_desligamento and \ - data_designacao > data_desligamento: + data_designacao > data_desligamento: self.logger.error('Data de designação ({}) superior ' - 'à data de desligamento ({})'.format(data_designacao, data_desligamento)) + 'à data de desligamento ({})'.format(data_designacao, data_desligamento)) raise ValidationError(_('Data de designação não pode ser superior ' - 'à data de desligamento')) + 'à data de desligamento')) composicao = Composicao.objects.get(id=self.initial['parent_pk']) - cargos_unicos = [c.cargo.nome for c in composicao.participacao_set.filter(cargo__unico=True)] + cargos_unicos = [ + c.cargo.nome for c in composicao.participacao_set.filter(cargo__unico=True)] if cleaned_data['cargo'].nome in cargos_unicos: msg = _('Este cargo é único para esta Comissão.') - self.logger.error('Este cargo ({}) é único para esta Comissão.'.format(cleaned_data['cargo'].nome)) + self.logger.error('Este cargo ({}) é único para esta Comissão.'.format( + cleaned_data['cargo'].nome)) raise ValidationError(msg) return cleaned_data - def create_participacao(self): composicao = Composicao.objects.get(id=self.initial['parent_pk']) data_inicio_comissao = composicao.periodo.data_inicio @@ -237,9 +240,9 @@ class ParticipacaoEditForm(forms.ModelForm): if data_desligamento and \ data_designacao > data_desligamento: self.logger.error('Data de designação ({}) superior ' - 'à data de desligamento ({})'.format(data_designacao, data_desligamento)) + 'à data de desligamento ({})'.format(data_designacao, data_desligamento)) raise ValidationError(_('Data de designação não pode ser superior ' - 'à data de desligamento')) + 'à data de desligamento')) composicao_id = self.instance.composicao_id @@ -250,7 +253,7 @@ class ParticipacaoEditForm(forms.ModelForm): if cleaned_data['cargo'].nome in cargos_unicos: msg = _('Este cargo é único para esta Comissão.') self.logger.error('Este cargo ({}) é único para esta Comissão (id={}).' - .format(cleaned_data['cargo'].nome, composicao_id)) + .format(cleaned_data['cargo'].nome, composicao_id)) raise ValidationError(msg) return cleaned_data @@ -259,6 +262,7 @@ class ParticipacaoEditForm(forms.ModelForm): class ComissaoForm(forms.ModelForm): logger = logging.getLogger(__name__) + class Meta: model = Comissao fields = '__all__' @@ -274,8 +278,6 @@ class ComissaoForm(forms.ModelForm): self.fields['data_prorrogada_temp'].widget.attrs['disabled'] = 'disabled' self.fields['data_fim_comissao'].widget.attrs['disabled'] = 'disabled' - - def clean(self): super(ComissaoForm, self).clean() @@ -283,51 +285,54 @@ class ComissaoForm(forms.ModelForm): return self.cleaned_data if len(self.cleaned_data['nome']) > 100: - msg = _('Nome da Comissão informado ({}) tem mais de 50 caracteres.'.format(self.cleaned_data['nome'])) - self.logger.error('Nome da Comissão deve ter no máximo 50 caracteres.') + msg = _('Nome da Comissão informado ({}) tem mais de 50 caracteres.'.format( + self.cleaned_data['nome'])) + self.logger.error( + 'Nome da Comissão deve ter no máximo 50 caracteres.') raise ValidationError(msg) if (self.cleaned_data['data_extincao'] and self.cleaned_data['data_extincao'] < self.cleaned_data['data_criacao']): - msg = _('Data de extinção não pode ser menor que a de criação') - self.logger.error('Data de extinção ({}) não pode ser menor que a de criação ({}).' - .format(self.cleaned_data['data_extincao'],self.cleaned_data['data_criacao'])) - raise ValidationError(msg) + msg = _('Data de extinção não pode ser menor que a de criação') + self.logger.error('Data de extinção ({}) não pode ser menor que a de criação ({}).' + .format(self.cleaned_data['data_extincao'], self.cleaned_data['data_criacao'])) + raise ValidationError(msg) if (self.cleaned_data['data_final_prevista_temp'] and self.cleaned_data['data_final_prevista_temp'] < self.cleaned_data['data_criacao']): - msg = _('Data Prevista para Término não pode ser menor que a de criação') - self.logger.error('Data Prevista para Término ({}) não pode ser menor que a de criação ({}).' - .format(self.cleaned_data['data_final_prevista_temp'], self.cleaned_data['data_criacao'])) - raise ValidationError(msg) + msg = _('Data Prevista para Término não pode ser menor que a de criação') + self.logger.error('Data Prevista para Término ({}) não pode ser menor que a de criação ({}).' + .format(self.cleaned_data['data_final_prevista_temp'], self.cleaned_data['data_criacao'])) + raise ValidationError(msg) if (self.cleaned_data['data_prorrogada_temp'] and self.cleaned_data['data_prorrogada_temp'] < self.cleaned_data['data_criacao']): - msg = _('Data Novo Prazo não pode ser menor que a de criação') - self.logger.error('Data Novo Prazo ({}) não pode ser menor que a de criação ({}).' - .format(self.cleaned_data['data_prorrogada_temp'], self.cleaned_data['data_criacao'])) - raise ValidationError(msg) + msg = _('Data Novo Prazo não pode ser menor que a de criação') + self.logger.error('Data Novo Prazo ({}) não pode ser menor que a de criação ({}).' + .format(self.cleaned_data['data_prorrogada_temp'], self.cleaned_data['data_criacao'])) + raise ValidationError(msg) if (self.cleaned_data['data_instalacao_temp'] and self.cleaned_data['data_instalacao_temp'] < self.cleaned_data['data_criacao']): - msg = _('Data de Instalação não pode ser menor que a de criação') - self.logger.error('Data de Instalação ({}) não pode ser menor que a de criação ({}).' - .format(self.cleaned_data['data_instalacao_temp'], self.cleaned_data['data_criacao'])) - raise ValidationError(msg) + msg = _('Data de Instalação não pode ser menor que a de criação') + self.logger.error('Data de Instalação ({}) não pode ser menor que a de criação ({}).' + .format(self.cleaned_data['data_instalacao_temp'], self.cleaned_data['data_criacao'])) + raise ValidationError(msg) if (self.cleaned_data['data_final_prevista_temp'] and self.cleaned_data['data_instalacao_temp'] and self.cleaned_data['data_final_prevista_temp'] < self.cleaned_data['data_instalacao_temp']): - msg = _('Data Prevista para Término não pode ser menor que a de Instalação.') - self.logger.error('Data Prevista para Término ({}) não pode ser menor que a de Instalação ({}).' - .format(self.cleaned_data['data_final_prevista_temp'], self.cleaned_data['data_instalacao_temp'])) - raise ValidationError(msg) + msg = _( + 'Data Prevista para Término não pode ser menor que a de Instalação.') + self.logger.error('Data Prevista para Término ({}) não pode ser menor que a de Instalação ({}).' + .format(self.cleaned_data['data_final_prevista_temp'], self.cleaned_data['data_instalacao_temp'])) + raise ValidationError(msg) if (self.cleaned_data['data_prorrogada_temp'] and self.cleaned_data['data_instalacao_temp'] and self.cleaned_data['data_prorrogada_temp'] < self.cleaned_data['data_instalacao_temp']): - msg = _('Data Novo Prazo não pode ser menor que a de Instalação.') - self.logger.error('Data Novo Prazo ({}) não pode ser menor que a de Instalação ({}).' - .format(self.cleaned_data['data_prorrogada_temp'], self.cleaned_data['data_instalacao_temp'])) - raise ValidationError(msg) + msg = _('Data Novo Prazo não pode ser menor que a de Instalação.') + self.logger.error('Data Novo Prazo ({}) não pode ser menor que a de Instalação ({}).' + .format(self.cleaned_data['data_prorrogada_temp'], self.cleaned_data['data_instalacao_temp'])) + raise ValidationError(msg) return self.cleaned_data @transaction.atomic @@ -337,7 +342,7 @@ class ComissaoForm(forms.ModelForm): comissao = super(ComissaoForm, self).save(commit) content_type = ContentType.objects.get_for_model(Comissao) object_id = comissao.pk - tipo = TipoAutor.objects.get(descricao__icontains='Comiss') + tipo = TipoAutor.objects.get(content_type=content_type) nome = comissao.sigla + ' - ' + comissao.nome Autor.objects.create( content_type=content_type, @@ -363,7 +368,6 @@ class ReuniaoForm(ModelForm): def clean(self): super(ReuniaoForm, self).clean() - if not self.is_valid(): return self.cleaned_data @@ -371,13 +375,15 @@ class ReuniaoForm(ModelForm): if self.cleaned_data['hora_fim']: if (self.cleaned_data['hora_fim'] < self.cleaned_data['hora_inicio']): - msg = _('A hora de término da reunião não pode ser menor que a de início') + msg = _( + 'A hora de término da reunião não pode ser menor que a de início') self.logger.error("A hora de término da reunião ({}) não pode ser menor que a de início ({})." - .format(self.cleaned_data['hora_fim'], self.cleaned_data['hora_inicio'])) + .format(self.cleaned_data['hora_fim'], self.cleaned_data['hora_inicio'])) raise ValidationError(msg) return self.cleaned_data -class DocumentoAcessorioCreateForm(forms.ModelForm): + +class DocumentoAcessorioCreateForm(FileFieldCheckMixin, forms.ModelForm): parent_pk = forms.CharField(required=False) # widget=forms.HiddenInput()) @@ -395,12 +401,11 @@ class DocumentoAcessorioCreateForm(forms.ModelForm): documentos = reuniao.documentoacessorio_set.all() return self.create_documentoacessorio() - def create_documentoacessorio(self): reuniao = Reuniao.objects.get(id=self.initial['parent_pk']) -class DocumentoAcessorioEditForm(forms.ModelForm): +class DocumentoAcessorioEditForm(FileFieldCheckMixin, forms.ModelForm): parent_pk = forms.CharField(required=False) # widget=forms.HiddenInput()) diff --git a/sapl/comissoes/migrations/0019_auto_20181214_1023.py b/sapl/comissoes/migrations/0019_auto_20181214_1023.py new file mode 100644 index 000000000..669ea20c6 --- /dev/null +++ b/sapl/comissoes/migrations/0019_auto_20181214_1023.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.8 on 2018-12-14 12:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('comissoes', '0018_auto_20180924_1724'), + ] + + operations = [ + migrations.AlterField( + model_name='reuniao', + name='hora_fim', + field=models.TimeField(blank=True, null=True, verbose_name='Horário de Término (hh:mm)'), + ), + ] diff --git a/sapl/comissoes/models.py b/sapl/comissoes/models.py index f9ffa97fa..2792c80d6 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -221,6 +221,7 @@ class Reuniao(models.Model): null=True, verbose_name=_('Horário de Início (hh:mm)')) hora_fim = models.TimeField( + blank=True, null=True, verbose_name=_('Horário de Término (hh:mm)')) local_reuniao = models.CharField( diff --git a/sapl/comissoes/tests/test_comissoes.py b/sapl/comissoes/tests/test_comissoes.py index d2f8b0bd1..3b45bf337 100644 --- a/sapl/comissoes/tests/test_comissoes.py +++ b/sapl/comissoes/tests/test_comissoes.py @@ -139,7 +139,6 @@ def test_valida_campos_obrigatorios_reuniao_form(): assert errors['nome'] == [_('Este campo é obrigatório.')] assert errors['data'] == [_('Este campo é obrigatório.')] assert errors['hora_inicio'] == [_('Este campo é obrigatório.')] - assert errors['hora_fim'] == [_('Este campo é obrigatório.')] - assert len(errors) == 7 + assert len(errors) == 6 diff --git a/sapl/compilacao/forms.py b/sapl/compilacao/forms.py index f7889604a..71d6ad7e5 100644 --- a/sapl/compilacao/forms.py +++ b/sapl/compilacao/forms.py @@ -3,7 +3,7 @@ from datetime import timedelta from crispy_forms.bootstrap import (Alert, FieldWithButtons, FormActions, InlineCheckboxes, InlineRadios, StrictButton) -from crispy_forms.helper import FormHelper +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,7 +23,8 @@ 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 SaplFormLayout, to_column, to_row +from sapl.crispy_layout_mixin import SaplFormLayout, to_column, to_row,\ + form_actions from sapl.utils import YES_NO_CHOICES error_messages = { @@ -83,7 +84,7 @@ class TipoTaForm(ModelForm): ('perfis', 12), ]) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = SaplFormLayout( Fieldset(_('Identificação Básica'), row1, css_class="col-md-12"), @@ -152,7 +153,7 @@ class TaForm(ModelForm): ('participacao_social', 3), ]) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = SaplFormLayout( Fieldset(_('Identificação Básica'), row1, css_class="col-md-12"), Fieldset( @@ -251,22 +252,27 @@ class NotaForm(ModelForm): ('publicidade', 6), ('publicacao', 3), ('efetividade', 3), + ('dispositivo', 0), + ('pk', 0), ]) buttons = FormActions( - HTML('' - '%s' % _('Cancelar')), + *[ + HTML('%s' % _('Cancelar')) + ], Button( 'submit-form', 'Salvar', - css_class='btn btn-primary pull-right') + css_class='btn btn-primary float-right'), + css_class='form-group row justify-content-between mr-1 ml-1' ) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout( Div( - Div(HTML(_('Notas')), css_class='panel-heading'), + Div(HTML(_('Notas')), css_class='card-header bg-light'), Div( row1, to_row([(Field( @@ -277,9 +283,9 @@ class NotaForm(ModelForm): placeholder=_('URL Externa (opcional)')), 12)]), row3, to_row([(buttons, 12)]), - css_class="panel-body" + css_class="card-body" ), - css_class="panel panel-primary" + css_class="card" ) ) @@ -326,12 +332,15 @@ class VideForm(ModelForm): def __init__(self, *args, **kwargs): buttons = FormActions( - HTML('' - '%s' % _('Cancelar')), + *[ + HTML('%s' % _('Cancelar')) + ], Button( 'submit-form', 'Salvar', - css_class='btn-primary pull-right') + css_class='btn btn-primary float-right'), + css_class='form-group row justify-content-between mr-1 ml-1' ) dispositivo_ref = Field( @@ -354,16 +363,18 @@ class VideForm(ModelForm): 'texto', placeholder=_('Texto Adicional ao Vide')), 12))))) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout( Div( - Div(HTML(_('Vides')), css_class='panel-heading'), + Div(HTML(_('Vides')), css_class='card-header bg-light'), Div( to_column((fields_form[0], 6)), to_column((fields_form[1], 6)), - css_class="panel-body" + to_column(('dispositivo_base', 0)), + to_column(('pk', 0)), + css_class="card-body row" ), - css_class="panel panel-primary" + css_class="card" ) ) @@ -460,7 +471,7 @@ class PublicacaoForm(ModelForm): ('url_externa', 8), ]) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = SaplFormLayout( Fieldset(Publicacao._meta.verbose_name, row1, row2, row3, css_class="col-md-12")) @@ -648,7 +659,7 @@ class DispositivoEdicaoBasicaForm(ModelForm): for f in fields: self.base_fields.update({f: getattr(self, f)}) - self.helper = FormHelper() + self.helper = SaplFormHelper() if not editor_type: cancel_label = _('Ir para o Editor Sequencial') @@ -667,7 +678,7 @@ class DispositivoEdicaoBasicaForm(ModelForm): cancel_label = _('Fechar') more = [ - HTML('%s' % + HTML('%s' % cancel_label), ] @@ -676,7 +687,7 @@ class DispositivoEdicaoBasicaForm(ModelForm): if not (inst.tipo_dispositivo.dispositivo_de_alteracao and inst.tipo_dispositivo.dispositivo_de_articulacao): btns_excluir = [ - HTML('%s' % + HTML('%s' % _('Cancelar')), ] - more.append(Submit('salvar', _('Salvar'), css_class='pull-right')) + more.append(Submit('salvar', _('Salvar'), css_class='float-right')) buttons = FormActions(*more, css_class='form-group') - _fields = [Div(*layout, css_class="row-fluid")] + \ + _fields = [Div(*layout, css_class="row")] + \ [to_row([(buttons, 12)])] - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout(*_fields) super(DispositivoRegistroAlteracaoForm, self).__init__(*args, **kwargs) @@ -1410,17 +1421,17 @@ class DispositivoRegistroRevogacaoForm(Form): layout.append(Field('dispositivo_search_form')) more = [ - HTML('%s' % + HTML('%s' % _('Cancelar')), ] - more.append(Submit('salvar', _('Salvar'), css_class='pull-right')) + more.append(Submit('salvar', _('Salvar'), css_class='float-right')) buttons = FormActions(*more, css_class='form-group') - _fields = [Div(*layout, css_class="row-fluid")] + \ + _fields = [Div(*layout, css_class="row")] + \ [to_row([(buttons, 12)])] - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout(*_fields) super(DispositivoRegistroRevogacaoForm, self).__init__(*args, **kwargs) @@ -1460,17 +1471,17 @@ class DispositivoRegistroInclusaoForm(Form): layout.append(Div(css_class="allowed_inserts col-md-12")) more = [ - HTML('%s' % + HTML('%s' % _('Cancelar')), ] - # more.append(Submit('salvar', _('Salvar'), css_class='pull-right')) + # more.append(Submit('salvar', _('Salvar'), css_class='float-right')) buttons = FormActions(*more, css_class='form-group') - _fields = [Div(*layout, css_class="row-fluid")] + \ + _fields = [Div(*layout, css_class="row")] + \ [to_row([(buttons, 12)])] - self.helper = FormHelper() + self.helper = SaplFormHelper() self.helper.layout = Layout(*_fields) super(DispositivoRegistroInclusaoForm, self).__init__(*args, **kwargs) diff --git a/sapl/compilacao/models.py b/sapl/compilacao/models.py index bb504934b..f36e406f9 100644 --- a/sapl/compilacao/models.py +++ b/sapl/compilacao/models.py @@ -1,4 +1,5 @@ +from bs4 import BeautifulSoup from django.contrib import messages from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -1105,6 +1106,15 @@ class Dispositivo(BaseModel, TimestampedMixin): self.contagem_continua = self.tipo_dispositivo.contagem_continua + try: + if self.texto: + self.texto = str(BeautifulSoup(self.texto, "html.parser")) + if self.texto_atualizador: + self.texto_atualizador = str(BeautifulSoup( + self.texto_atualizador, "html.parser")) + except: + pass + return super().save( force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields, clean=clean) diff --git a/sapl/compilacao/templatetags/compilacao_filters.py b/sapl/compilacao/templatetags/compilacao_filters.py index e56478bae..a7fb2eada 100644 --- a/sapl/compilacao/templatetags/compilacao_filters.py +++ b/sapl/compilacao/templatetags/compilacao_filters.py @@ -83,6 +83,9 @@ def nota_automatica(dispositivo, ta_pub_list): if dispositivo.ta_publicado: d = dispositivo.dispositivo_atualizador.dispositivo_pai + if d.auto_inserido: + d = d.dispositivo_pai + ta_publicado = ta_pub_list[dispositivo.ta_publicado_id] if\ ta_pub_list else dispositivo.ta_publicado diff --git a/sapl/compilacao/urls.py b/sapl/compilacao/urls.py index 781a6d80e..9364985aa 100644 --- a/sapl/compilacao/urls.py +++ b/sapl/compilacao/urls.py @@ -3,7 +3,8 @@ from django.conf.urls import include, url from sapl.compilacao import views from sapl.compilacao.views import (TipoDispositivoCrud, TipoNotaCrud, TipoPublicacaoCrud, TipoVideCrud, - VeiculoPublicacaoCrud) + VeiculoPublicacaoCrud, + TipoTextoArticuladoCrud) from .apps import AppConfig @@ -113,14 +114,7 @@ urlpatterns = [ include(TipoPublicacaoCrud.get_urls())), url(r'^sistema/ta/config/veiculo-publicacao/', include(VeiculoPublicacaoCrud.get_urls())), - url(r'^sistema/ta/config/tipo-textoarticulado$', - views.TipoTaListView.as_view(), name='tipo_ta_list'), - url(r'^sistema/ta/config/tipo-textoarticulado/create$', - views.TipoTaCreateView.as_view(), name='tipo_ta_create'), - url(r'^sistema/ta/config/tipo-textoarticulado/(?P[0-9]+)$', - views.TipoTaDetailView.as_view(), name='tipo_ta_detail'), - url(r'^sistema/ta/config/tipo-textoarticulado/(?P[0-9]+)/edit$', - views.TipoTaUpdateView.as_view(), name='tipo_ta_edit'), - url(r'^sistema/ta/config/tipo-textoarticulado/(?P[0-9]+)/delete$', - views.TipoTaDeleteView.as_view(), name='tipo_ta_delete'), + url(r'^sistema/ta/config/tipo/', + include(TipoTextoArticuladoCrud.get_urls())), + ] diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index a82f63556..f09ed8a4f 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -4,6 +4,7 @@ import logging import sys from braces.views import FormMessagesMixin +from bs4 import BeautifulSoup from django import forms from django.conf import settings from django.contrib import messages @@ -49,7 +50,8 @@ from sapl.compilacao.models import (STATUS_TA_EDITION, STATUS_TA_PRIVATE, from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED, DISPOSITIVO_SELECT_RELATED_EDIT, get_integrations_view_names) -from sapl.crud.base import Crud, CrudAux, CrudListView, make_pagination +from sapl.crud.base import RP_DETAIL, RP_LIST, Crud, CrudAux, CrudListView,\ + make_pagination from sapl.settings import BASE_DIR @@ -430,88 +432,32 @@ class CompMixin(PermissionRequiredMixin): return rr -class TipoTaListView(CompMixin, ListView): +class TipoTextoArticuladoCrud(CrudAux): model = TipoTextoArticulado - paginate_by = 10 - verbose_name = model._meta.verbose_name - permission_required = 'compilacao.list_tipotextoarticulado' - - @property - def title(self): - return self.model._meta.verbose_name_plural - - @property - def create_url(self): - return reverse_lazy('sapl.compilacao:tipo_ta_create') - - -class TipoTaCreateView(CompMixin, FormMessagesMixin, CreateView): - model = TipoTextoArticulado - form_class = TipoTaForm - template_name = "crud/form.html" - form_valid_message = _('Registro criado com sucesso!') - form_invalid_message = _('O registro não foi criado.') - permission_required = 'compilacao.add_tipotextoarticulado' - - def get(self, request, *args, **kwargs): - self.object = None - form = self.get_form() - form.fields['content_type'] = forms.ChoiceField( - choices=choice_models_in_extenal_views(), - label=_('Modelo Integrado'), required=False) - - return self.render_to_response(self.get_context_data(form=form)) - - def get_success_url(self): - return reverse_lazy('sapl.compilacao:tipo_ta_detail', - kwargs={'pk': self.object.id}) - - @property - def cancel_url(self): - return reverse_lazy('sapl.compilacao:tipo_ta_list') + public = [RP_LIST, RP_DETAIL, ] + class CreateView(CrudAux.CreateView): + form_class = TipoTaForm -class TipoTaDetailView(CompMixin, DetailView): - model = TipoTextoArticulado - permission_required = 'compilacao.detail_tipotextoarticulado' + def get(self, request, *args, **kwargs): + self.object = None + form = self.get_form() + form.fields['content_type'] = forms.ChoiceField( + choices=choice_models_in_extenal_views(), + label=_('Modelo Integrado'), required=False) + return self.render_to_response(self.get_context_data(form=form)) -class TipoTaUpdateView(CompMixin, UpdateView): - model = TipoTextoArticulado - form_class = TipoTaForm - template_name = "crud/form.html" - permission_required = 'compilacao.change_tipotextoarticulado' - - def get(self, request, *args, **kwargs): - self.object = self.get_object() - form = self.get_form() - form.fields['content_type'] = forms.ChoiceField( - choices=choice_models_in_extenal_views(), - label=_('Modelo Integrado'), required=False) - return self.render_to_response(self.get_context_data(form=form)) + class UpdateView(CrudAux.UpdateView): + form_class = TipoTaForm - def get_success_url(self): - return reverse_lazy('sapl.compilacao:tipo_ta_detail', - kwargs={'pk': self.kwargs['pk']}) - - @property - def cancel_url(self): - return reverse_lazy('sapl.compilacao:tipo_ta_detail', - kwargs={'pk': self.kwargs['pk']}) - - -class TipoTaDeleteView(CompMixin, DeleteView): - model = TipoTextoArticulado - template_name = "crud/confirm_delete.html" - permission_required = 'compilacao.delete_tipotextoarticulado' - - @property - def detail_url(self): - return reverse_lazy('sapl.compilacao:tipo_ta_detail', - kwargs={'pk': self.kwargs['pk']}) - - def get_success_url(self): - return reverse_lazy('sapl.compilacao:tipo_ta_list') + def get(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.get_form() + form.fields['content_type'] = forms.ChoiceField( + choices=choice_models_in_extenal_views(), + label=_('Modelo Integrado'), required=False) + return self.render_to_response(self.get_context_data(form=form)) class TaListView(CompMixin, ListView): @@ -1319,6 +1265,9 @@ class TextEditView(CompMixin, TemplateView): if dispositivo.ta_publicado_id: d = dispositivo.dispositivo_atualizador.dispositivo_pai + if d.auto_inserido: + d = d.dispositivo_pai + ta_publicado = lista_ta_publicado[dispositivo.ta_publicado_id] if\ lista_ta_publicado else dispositivo.ta_publicado @@ -2937,13 +2886,10 @@ class DispositivoDinamicEditView( if texto != texto_atualizador else '' visibilidade = request.POST['visibilidade'] - # if d.texto != '': - # d.texto = texto - # d.save() - # return self.get(request, *args, **kwargs) d_texto = d.texto d.texto = texto.strip() d.texto_atualizador = texto_atualizador.strip() + d.visibilidade = not visibilidade or visibilidade == 'True' d.save() diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index 9b2f0867b..200f24db1 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -1,6 +1,5 @@ from math import ceil -import rtyaml from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Div, Fieldset, Layout, Submit @@ -8,6 +7,7 @@ from django import template from django.core.urlresolvers import reverse, reverse_lazy from django.utils import formats from django.utils.translation import ugettext as _ +import rtyaml def heads_and_tails(list_of_lists): @@ -21,7 +21,7 @@ def to_column(name_span): def to_row(names_spans): - return Div(*map(to_column, names_spans), css_class='row-fluid') + return Div(*map(to_column, names_spans), css_class='row') def to_fieldsets(fields): @@ -35,7 +35,8 @@ def to_fieldsets(fields): def form_actions(more=[Div(css_class='clearfix')], - label=_('Salvar'), name='salvar', css_class='pull-right', disabled=True): + label=_('Salvar'), name='salvar', + css_class='float-right', disabled=True): if disabled: doubleclick = 'this.form.submit();this.disabled=true;' @@ -43,10 +44,41 @@ def form_actions(more=[Div(css_class='clearfix')], doubleclick = 'return true;' return FormActions( + *more, Submit(name, label, css_class=css_class, # para impedir resubmissão do form onclick=doubleclick), - *more) + css_class='form-group row justify-content-between' + ) + + +class SaplFormHelper(FormHelper): + render_hidden_fields = True # default = False + """ + até a release 1.6.1 do django-crispy-forms, os fields em Meta.Fields eram + renderizados mesmo se não mencionados no helper. + Com esta mudança (https://github.com/django-crispy-forms/django-crispy-forms/commit/6b93e8a362422db8fe54aa731319c7cbc39990ba) + render_hidden_fields foi adicionado uma condição em que a cada + instância do Helper, fosse decidido se os fields não mencionados serião ou + não renderizados... + O Sapl até este commit: https://github.com/interlegis/sapl/commit/22b87f36ebc8659a6ecaf8831ab0f425206b0993 + utilizou o django-crispy-forms na versão 1.6.1, ou seja, + sem a condição render_hidden_fields o que fazia o FormHelper, na 1.6.1 + set comportar como se, agora, na 1.7.2 o default fosse True. + Como todos os Forms do Sapl foram construídos assumindo que fields + não incluídos explicitamente no Helper, o helper o incluiria implicitamente, + e assim o era, de acordo com commit acima do django-crispy-forms, então + cria-se essa classe: + + class SaplFormHelper(FormHelper): + render_hidden_fields = True + + onde torna o default, antes False, agora = True, o esperado pelos forms do sapl, + e substituí-se todos os FormHelper por SaplFormHelper dentro do projeto Sapl + + + esta explicação ficará aqui dentro do código, via commit, e na issue #2456. + """ class SaplFormLayout(Layout): @@ -58,7 +90,7 @@ class SaplFormLayout(Layout): if not buttons: buttons = form_actions(label=save_label, more=[ HTML('%s' % cancel_label) + ' class="btn btn-dark">%s' % cancel_label) if cancel_label else None]) _fields = list(to_fieldsets(fields)) @@ -185,8 +217,11 @@ class CrispyLayoutFormMixin: pass else: if self.layout_key: - form.helper = FormHelper() - form.helper.layout = SaplFormLayout(*self.get_layout()) + form.helper = SaplFormHelper() + layout = self.get_layout() + + form.helper.layout = SaplFormLayout(*layout) + return form @property diff --git a/sapl/crud/base.py b/sapl/crud/base.py index e720b36cd..5fa4f70e5 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -1,8 +1,8 @@ import logging + from braces.views import FormMessagesMixin -from compressor.utils.decorators import cached_property from crispy_forms.bootstrap import FieldWithButtons, StrictButton -from crispy_forms.helper import FormHelper +from sapl.crispy_layout_mixin import SaplFormHelper from crispy_forms.layout import Field, Layout from django import forms from django.conf.urls import url @@ -16,6 +16,7 @@ from django.http.response import Http404 from django.shortcuts import redirect from django.utils.decorators import classonlymethod from django.utils.encoding import force_text +from django.utils.functional import cached_property from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ from django.views.generic import (CreateView, DeleteView, DetailView, ListView, @@ -29,6 +30,7 @@ from sapl.rules.map_rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, from sapl.settings import BASE_DIR from sapl.utils import normalize + ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ 'list', 'create', 'detail', 'update', 'delete' @@ -148,7 +150,7 @@ class ListWithSearchForm(forms.Form): def __init__(self, *args, **kwargs): super(ListWithSearchForm, self).__init__(*args, **kwargs) - self.helper = FormHelper() + self.helper = SaplFormHelper() self.form_class = 'form-inline' self.helper.form_method = 'GET' self.helper.layout = Layout( @@ -158,7 +160,7 @@ class ListWithSearchForm(forms.Form): placeholder=_('Filtrar Lista'), css_class='input-lg'), StrictButton( - _('Filtrar'), css_class='btn-default btn-lg', + _('Filtrar'), css_class='btn-outline-primary btn-lg', type='submit')) ) @@ -558,7 +560,8 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView): fm = model._meta.get_field(fo) except Exception as e: username = self.request.user.username - self.logger.error("user=" + username + ". " + str(e)) + self.logger.error( + "user=" + username + ". " + str(e)) pass if fm and hasattr(fm, 'related_model')\ @@ -824,7 +827,7 @@ class CrudUpdateView(PermissionRequiredContainerCrudMixin, logger = logging.getLogger(__name__) def form_valid(self, form): - + self.object = form.instance try: self.object.modifier = self.request.user @@ -882,12 +885,12 @@ class CrudDeleteView(PermissionRequiredContainerCrudMixin, error_msg2 += '{} - {}, '.format( i._meta.verbose_name, i ) - error_msg2 = error_msg2[:len(error_msg2)-2] + '.' + error_msg2 = error_msg2[:len(error_msg2) - 2] + '.' error_msg += '' - + username = request.user.username self.logger.error("user=" + username + ". Registro não pode ser removido, pois " - "é referenciado por outros registros: " + error_msg2) + "é referenciado por outros registros: " + error_msg2) messages.add_message(request, messages.ERROR, error_msg) diff --git a/sapl/crud/tests/stub_app/templates/base.html b/sapl/crud/tests/stub_app/templates/base.html index 8a9dc002b..fcf92e5eb 100644 --- a/sapl/crud/tests/stub_app/templates/base.html +++ b/sapl/crud/tests/stub_app/templates/base.html @@ -12,9 +12,9 @@ {# Feedback messages #} {% for message in messages %} -