diff --git a/.gitignore b/.gitignore index dfdeb2b5c..c656b3b8f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Nodejs node_modules/ +yarn.lock # Distribution / packaging .Python 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 f4953de84..8c2dc0fb5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ 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 \ + 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 @@ -17,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/ @@ -37,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 diff --git a/docker-compose.yml b/docker-compose.yml index 5b0ee2315..ab841b87b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ sapldb: ports: - "5432:5432" sapl: - image: interlegis/sapl:3.1.142 + image: interlegis/sapl:3.1.143 restart: always environment: ADMIN_PASSWORD: interlegis diff --git a/docs/instalacao31.rst b/docs/instalacao31.rst index 6e9146ee4..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. ----------------------------------------------------- @@ -153,6 +145,8 @@ Criação da `SECRET_KEY =1.11,<2.0 -django-bootstrap3==11.0.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-bower==5.2.0 django-braces==1.9.0 django-crispy-forms==1.7.2 django-floppyforms==1.7.0 django-extra-views==0.12.0 django-model-utils==3.1.2 -django-sass-processor==0.7.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 +django-webpack-loader==0.6.0 +drf-yasg==1.13.0 easy-thumbnails==2.5 -libsass==0.17.0 python-decouple==3.1 psycopg2-binary==2.7.6.1 pyyaml==4.2b1 @@ -33,7 +31,4 @@ pysolr==3.6.0 whoosh==2.7.4 git+git://github.com/interlegis/trml2pdf.git -git+git://github.com/jasperlittle/django-rest-framework-docs -git+git://github.com/rubgombar1/django-admin-bootstrapped.git - -django-compressor==2.2 \ No newline at end of file +git+git://github.com/interlegis/django-admin-bootstrapped diff --git a/sapl/api/deprecated.py b/sapl/api/deprecated.py new file mode 100644 index 000000000..e38ed3065 --- /dev/null +++ b/sapl/api/deprecated.py @@ -0,0 +1,444 @@ + +import logging + +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from django.http import Http404 +from django.utils.translation import ugettext_lazy as _ +from django_filters.rest_framework.backends import DjangoFilterBackend +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.forms import (AutorChoiceFilterSet, AutoresPossiveisFilterSet, + AutorSearchForFieldFilterSet) +from sapl.api.serializers import ModelChoiceSerializer, AutorSerializer,\ + ChoiceSerializer +from sapl.base.models import TipoAutor, Autor, CasaLegislativa +from sapl.materia.models import MateriaLegislativa +from sapl.sessao.models import SessaoPlenaria, OrdemDia +from sapl.utils import SaplGenericRelation + + +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/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..f416517c5 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -1,8 +1,6 @@ from rest_framework import serializers -from sapl.base.models import Autor, CasaLegislativa -from sapl.materia.models import MateriaLegislativa -from sapl.sessao.models import OrdemDia, SessaoPlenaria +from sapl.base.models import Autor class ChoiceSerializer(serializers.Serializer): @@ -31,144 +29,12 @@ 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: model = Autor fields = '__all__' - - -class MateriaLegislativaSerializer(serializers.ModelSerializer): - - class Meta: - model = MateriaLegislativa - fields = '__all__' - - -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 - - 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 diff --git a/sapl/api/urls.py b/sapl/api/urls.py index 1f19e9d1f..bdcf0e372 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 + +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 = [ + 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', @@ -27,14 +60,14 @@ 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(deprecated_urlpatterns_api)), url(r'^api/', include(urlpatterns_api)), - url(r'^api/', include(urlpatterns_router)) + 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 336bb23d7..4e88d76ca 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -1,279 +1,446 @@ 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 _ +import django_filters from django_filters.rest_framework.backends 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 +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.permissions import SaplModelPermissions +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 -class ModelChoiceView(ListAPIView): +class SaplApiViewSetConstrutor(ModelViewSet): - # FIXME aplicar permissão correta de usuário - permission_classes = (IsAuthenticated,) - serializer_class = ModelChoiceSerializer + filter_backends = (DjangoFilterBackend,) - def get(self, request, *args, **kwargs): - self.model = ContentType.objects.get_for_id( - self.kwargs['content_type']).model_class() + @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(FilterSet): + class Meta: + model = _model + fields = '__all__' + filter_overrides = { + FileField: { + 'filter_class': django_filters.CharFilter, + 'extra': lambda f: { + 'lookup_expr': 'exact', + }, + }, + } + + @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 + + # 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() + +# 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']): + """ + 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 - pagination = request.GET.get('pagination', '') - if pagination == 'False': - self.pagination_class = None + """ - return ListAPIView.get(self, request, *args, **kwargs) + def list_for_content_type(self, content_type): + qs = self.get_queryset() + qs = qs.filter(content_type=content_type) - def get_queryset(self): - return self.model.objects.all() + page = self.paginate_queryset(qs) + if page is not None: + serializer = self.serializer_class(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(page, many=True) + return Response(serializer.data) + + @classonlymethod + def build_class_with_actions(cls): + + models_with_gr_for_autor = models_with_gr_for_model(Autor) + + for _model in models_with_gr_for_autor: + + @action(detail=False, name=_model._meta.model_name) + def actionclass(self, request, *args, **kwargs): + model = getattr(self, self.action)._AutorViewSet__model + + 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 + + setattr(cls, _model._meta.model_name, func) + return cls + + +class _ParlamentarViewSet(SaplSetViews['parlamentares']['parlamentar']): + @action(detail=True) + def proposicoes(self, request, *args, **kwargs): + """ + Lista de proposições públicas de parlamentar específico + + :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 AutorListView(ListAPIView): +class _ProposicaoViewSet(SaplSetViews['materia']['proposicao']): """ - 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 + 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 + """ - logger = logging.getLogger(__name__) + 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: + 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 - TR_AUTOR_CHOICE_SERIALIZER = 1 - TR_AUTOR_SERIALIZER = 3 + permission_classes = (ProposicaoPermission, ) - permission_classes = (IsAuthenticatedOrReadOnly,) - queryset = Autor.objects.all() - model = Autor + 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) - filter_class = AutorChoiceFilterSet - filter_backends = (DjangoFilterBackend, ) - serializer_class = AutorChoiceSerializer + qs = qs.filter(q) + return qs - @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 +class _DocumentoAdministrativoViewSet(SaplSetViews['protocoloadm']['documentoadministrativo']): - def get(self, request, *args, **kwargs): - if self.tr == AutorListView.TR_AUTOR_SERIALIZER: - self.serializer_class = AutorSerializer - self.permission_classes = (IsAuthenticated,) + 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) - if self.filter_class and 'q_0' in request.GET: - self.filter_class = AutorSearchForFieldFilterSet + permission_classes = (DocumentoAdministrativoPermission, ) - return ListAPIView.get(self, request, *args, **kwargs) + 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() + if self.request.user.is_anonymous(): + qs = qs.exclude(restrito=True) + return qs -class AutoresProvaveisListView(ListAPIView): - logger = logging.getLogger(__name__) - permission_classes = (IsAuthenticatedOrReadOnly,) - queryset = Autor.objects.all() - model = Autor +class _DocumentoAcessorioAdministrativoViewSet( + SaplSetViews['protocoloadm']['documentoacessorioadministrativo']): - filter_class = None - filter_backends = [] - serializer_class = ChoiceSerializer + permission_classes = ( + _DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, ) def get_queryset(self): + qs = super().get_queryset() - 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]) + if self.request.user.is_anonymous(): + qs = qs.exclude(documento__restrito=True) + return qs - 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 _TramitacaoAdministrativoViewSet( + SaplSetViews['protocoloadm']['tramitacaoadministrativo']): + # TODO: Implementar regras de manutenção das tramitações de docs adms + permission_classes = ( + _DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, ) -class AutoresPossiveisListView(ListAPIView): + def get_queryset(self): + qs = super().get_queryset() - permission_classes = (IsAuthenticatedOrReadOnly,) - queryset = Autor.objects.all() - model = Autor + if self.request.user.is_anonymous(): + qs = qs.exclude(documento__restrito=True) + return qs - pagination_class = None + def create(self, request, *args, **kwargs): + raise Exception(_("POST Create não implementado")) - filter_class = AutoresPossiveisFilterSet - serializer_class = AutorChoiceSerializer + def put(self, request, *args, **kwargs): + raise Exception(_("PUT Update não implementado")) + def patch(self, request, *args, **kwargs): + raise Exception(_("PATCH Partial Update não implementado")) -class MateriaLegislativaViewSet(ListModelMixin, - RetrieveModelMixin, - GenericViewSet): + def delete(self, request, *args, **kwargs): + raise Exception(_("DELETE Delete não implementado")) - permission_classes = (IsAuthenticated,) - serializer_class = MateriaLegislativaSerializer - queryset = MateriaLegislativa.objects.all() - filter_backends = (DjangoFilterBackend,) - filter_fields = ('numero', 'ano', 'tipo', ) +SaplSetViews['base']['autor'] = _AutorViewSet.build_class_with_actions() -class SessaoPlenariaViewSet(ListModelMixin, - RetrieveModelMixin, - GenericViewSet): +SaplSetViews['materia']['proposicao'] = _ProposicaoViewSet - permission_classes = (AllowAny,) - serializer_class = SessaoPlenariaSerializer - queryset = SessaoPlenaria.objects.all() - filter_backends = (DjangoFilterBackend,) - filter_fields = ('data_inicio', 'data_fim', 'interativa') +SaplSetViews['parlamentares']['parlamentar'] = _ParlamentarViewSet + +SaplSetViews['protocoloadm']['documentoadministrativo'] = _DocumentoAdministrativoViewSet +SaplSetViews['protocoloadm']['documentoacessorioadministrativo'] = _DocumentoAcessorioAdministrativoViewSet +SaplSetViews['protocoloadm']['tramitacaoadministrativo'] = _TramitacaoAdministrativoViewSet diff --git a/sapl/base/forms.py b/sapl/base/forms.py index d603ffc45..d97ed1804 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -18,14 +18,15 @@ 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.materia.models import ( + MateriaLegislativa, UnidadeTramitacao, StatusTramitacao) from sapl.norma.models import (NormaJuridica, NormaEstatisticas) from sapl.parlamentares.models import SessaoLegislativa from sapl.sessao.models import SessaoPlenaria @@ -393,7 +394,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', @@ -401,9 +402,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'), @@ -706,7 +707,7 @@ class RelatorioAtasFilterSet(django_filters.FilterSet): def ultimo_ano_com_norma(): anos_normas = choice_anos_com_normas() - + if anos_normas: return anos_normas[0] return '' @@ -754,7 +755,7 @@ class EstatisticasAcessoNormasForm(Form): class Meta: fields = ['ano'] - + def __init__(self, *args, **kwargs): super(EstatisticasAcessoNormasForm, self).__init__( *args, **kwargs) @@ -857,7 +858,11 @@ class RelatorioHistoricoTramitacaoFilterSet(django_filters.FilterSet): self.filters['tipo'].label = 'Tipo de Matéria' + self.filters['tramitacao__unidade_tramitacao_local' + ].label = _('Unidade Local (Último Local)') + self.filters['tramitacao__status'].label = _('Status') row1 = to_row([('tramitacao__data_tramitacao', 12)]) + row2 = to_row( [('tipo', 4), ('tramitacao__unidade_tramitacao_local', 4), diff --git a/sapl/base/models.py b/sapl/base/models.py index 343a8db9b..55792b147 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -1,16 +1,19 @@ -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'))) @@ -20,9 +23,9 @@ SEQUENCIA_NUMERACAO = (('A', _('Sequencial por ano')), ('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')), @@ -100,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) diff --git a/sapl/base/templatetags/common_tags.py b/sapl/base/templatetags/common_tags.py index 884a6968f..3c72b1654 100644 --- a/sapl/base/templatetags/common_tags.py +++ b/sapl/base/templatetags/common_tags.py @@ -1,5 +1,7 @@ from django import template 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 @@ -274,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/tests/test_login.py b/sapl/base/tests/test_login.py index 91665ea8d..7fe7672e5 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/views.py b/sapl/base/views.py index c45cb8703..3bc003cc0 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -382,7 +382,7 @@ class RelatorioPresencaSessaoView(FilterView): m = m.last() parlamentares_presencas.append({ 'parlamentar': p, - 'titular': m.titular, + 'titular': m.titular if m else True, 'sessao_porc': 0, 'ordemdia_porc': 0 }) diff --git a/sapl/comissoes/forms.py b/sapl/comissoes/forms.py index 14d42f53e..cef967102 100644 --- a/sapl/comissoes/forms.py +++ b/sapl/comissoes/forms.py @@ -13,10 +13,13 @@ from sapl.comissoes.models import (Comissao, Composicao, DocumentoAcessorio, Participacao, Reuniao, Periodo) from sapl.parlamentares.models import Legislatura, Mandato, Parlamentar + 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 +46,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 +58,7 @@ class ComposicaoForm(forms.ModelForm): class PeriodoForm(forms.ModelForm): logger = logging.getLogger(__name__) + class Meta: model = Periodo exclude = [] @@ -70,31 +74,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 +124,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 +139,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 +149,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 +239,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 +252,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 +261,7 @@ class ParticipacaoEditForm(forms.ModelForm): class ComissaoForm(forms.ModelForm): logger = logging.getLogger(__name__) + class Meta: model = Comissao fields = '__all__' @@ -274,8 +277,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 +284,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 +341,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 +367,6 @@ class ReuniaoForm(ModelForm): def clean(self): super(ReuniaoForm, self).clean() - if not self.is_valid(): return self.cleaned_data @@ -371,12 +374,14 @@ 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): parent_pk = forms.CharField(required=False) # widget=forms.HiddenInput()) @@ -395,7 +400,6 @@ 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']) diff --git a/sapl/compilacao/forms.py b/sapl/compilacao/forms.py index 3c4275bee..0e4f933d7 100644 --- a/sapl/compilacao/forms.py +++ b/sapl/compilacao/forms.py @@ -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 = { @@ -256,19 +257,22 @@ class NotaForm(ModelForm): ]) 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.layout = Layout( Div( - Div(HTML(_('Notas')), css_class='panel-heading'), + Div(HTML(_('Notas')), css_class='card-header bg-light'), Div( row1, to_row([(Field( @@ -279,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" ) ) @@ -328,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( @@ -359,15 +366,15 @@ class VideForm(ModelForm): self.helper = FormHelper() self.helper.layout = Layout( Div( - Div(HTML(_('Vides')), css_class='car-header'), + Div(HTML(_('Vides')), css_class='card-header bg-light'), Div( to_column((fields_form[0], 6)), to_column((fields_form[1], 6)), to_column(('dispositivo_base', 0)), to_column(('pk', 0)), - css_class="card-body" + css_class="card-body row" ), - css_class="card bg-light" + css_class="card" ) ) @@ -671,7 +678,7 @@ class DispositivoEdicaoBasicaForm(ModelForm): cancel_label = _('Fechar') more = [ - HTML('%s' % + HTML('%s' % cancel_label), ] @@ -680,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() @@ -1414,14 +1421,14 @@ 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() @@ -1464,14 +1471,14 @@ 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() 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 897e18cd6..f09ed8a4f 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -50,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 @@ -431,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') - - -class TipoTaDetailView(CompMixin, DetailView): - model = TipoTextoArticulado - permission_required = 'compilacao.detail_tipotextoarticulado' - - -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)) - - 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') + public = [RP_LIST, RP_DETAIL, ] + + class CreateView(CrudAux.CreateView): + form_class = TipoTaForm + + 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 UpdateView(CrudAux.UpdateView): + form_class = TipoTaForm + + 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): diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index 9b2f0867b..245be6b82 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,12 @@ 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 SaplFormLayout(Layout): @@ -58,7 +61,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)) diff --git a/sapl/crud/base.py b/sapl/crud/base.py index faa0e1287..ca5469bf0 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -160,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')) ) 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 %} -