diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9ddd7a255..4387cb37d 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -8,7 +8,7 @@ django-compressor==2.0 django-crispy-forms==1.6.0 django-extensions==1.6.7 django-extra-views==0.8.0 -django-filter==0.13.0 +django-filter==0.15.3 django-floppyforms==1.6.2 django-model-utils==2.5 django-sass-processor==0.4.6 diff --git a/sapl/api/forms.py b/sapl/api/forms.py index 87e2c2990..c220c01b2 100644 --- a/sapl/api/forms.py +++ b/sapl/api/forms.py @@ -1,6 +1,7 @@ from django.db.models import Q from django_filters.filters import MethodFilter, ModelChoiceFilter from rest_framework.filters import FilterSet + from sapl.base.models import Autor, TipoAutor from sapl.utils import generic_relations_for_model @@ -21,7 +22,6 @@ class SaplGenericRelationSearchFilterSet(FilterSet): order_by = [] for gr in generic_relations_for_model(self._meta.model): - model = gr[0] sgr = gr[1] for item in sgr: if item.related_model != self._meta.model: diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index 0791ab22f..bd3fb7def 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers + from sapl.base.models import Autor from sapl.materia.models import MateriaLegislativa diff --git a/sapl/api/urls.py b/sapl/api/urls.py index 0d197efc9..195c7f84a 100644 --- a/sapl/api/urls.py +++ b/sapl/api/urls.py @@ -1,9 +1,9 @@ from django.conf import settings -from django.conf.urls import url, include +from django.conf.urls import include, url from rest_framework.routers import DefaultRouter -from sapl.api.views import MateriaLegislativaViewSet, AutorListView,\ - ModelChoiceView +from sapl.api.views import (AutorListView, MateriaLegislativaViewSet, + ModelChoiceView) from .apps import AppConfig diff --git a/sapl/api/views.py b/sapl/api/views.py index d0d1a875d..41d327bff 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -5,13 +5,14 @@ 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 IsAuthenticated, AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import GenericViewSet from sapl.api.forms import AutorChoiceFilterSet -from sapl.api.serializers import ChoiceSerializer, AutorSerializer,\ - AutorChoiceSerializer, ModelChoiceSerializer, MateriaLegislativaSerializer - +from sapl.api.serializers import (AutorChoiceSerializer, AutorSerializer, + ChoiceSerializer, + MateriaLegislativaSerializer, + ModelChoiceSerializer) from sapl.base.models import Autor, TipoAutor from sapl.materia.models import MateriaLegislativa from sapl.utils import SaplGenericRelation, sapl_logger diff --git a/sapl/base/apps.py b/sapl/base/apps.py index 0ca7a90cd..c3af888b7 100644 --- a/sapl/base/apps.py +++ b/sapl/base/apps.py @@ -1,115 +1,9 @@ -from builtins import LookupError -from django.apps import apps -from django.contrib.auth.management import _get_all_permissions -from django.core import exceptions -from django.db import router -from django.db.models.signals import pre_migrate, post_migrate -from django.db.utils import DEFAULT_DB_ALIAS -from django.utils.translation import string_concat -from django.utils.translation import ugettext_lazy as _ import django - - -def create_proxy_permissions( - app_config, verbosity=2, interactive=True, - using=DEFAULT_DB_ALIAS, **kwargs): - if not app_config.models_module: - return - - # print(app_config) - - try: - Permission = apps.get_model('auth', 'Permission') - except LookupError: - return - - if not router.allow_migrate_model(using, Permission): - return - - from django.contrib.contenttypes.models import ContentType - - permission_name_max_length = Permission._meta.get_field('name').max_length - - # This will hold the permissions we're looking for as - # (content_type, (codename, name)) - searched_perms = list() - # The codenames and ctypes that should exist. - ctypes = set() - for klass in list(app_config.get_models()): - opts = klass._meta - permissions = ( - ("list_" + opts.model_name, - string_concat( - _('Visualizaçao da lista de'), ' ', - opts.verbose_name_plural)), - ("detail_" + opts.model_name, - string_concat( - _('Visualização dos detalhes de'), ' ', - opts.verbose_name_plural)), - ) - opts.permissions = tuple( - set(list(permissions) + list(opts.permissions))) - - if opts.proxy: - # Force looking up the content types in the current database - # before creating foreign keys to them. - app_label, model = opts.app_label, opts.model_name - - try: - ctype = ContentType.objects.db_manager( - using).get_by_natural_key(app_label, model) - except: - ctype = ContentType.objects.db_manager( - using).create(app_label=app_label, model=model) - else: - ctype = ContentType.objects.db_manager(using).get_for_model(klass) - - ctypes.add(ctype) - for perm in _get_all_permissions(klass._meta, ctype): - searched_perms.append((ctype, perm)) - - # Find all the Permissions that have a content_type for a model we're - # looking for. We don't need to check for codenames since we already have - # a list of the ones we're going to create. - all_perms = set(Permission.objects.using(using).filter( - content_type__in=ctypes, - ).values_list( - "content_type", "codename" - )) - - perms = [ - Permission(codename=codename, name=name, content_type=ct) - for ct, (codename, name) in searched_perms - if (ct.pk, codename) not in all_perms - ] - # Validate the permissions before bulk_creation to avoid cryptic database - # error when the name is longer than 255 characters - for perm in perms: - if len(perm.name) > permission_name_max_length: - raise exceptions.ValidationError( - 'The permission name %s of %s.%s ' - 'is longer than %s characters' % ( - perm.name, - perm.content_type.app_label, - perm.content_type.model, - permission_name_max_length, - ) - ) - Permission.objects.using(using).bulk_create(perms) - if verbosity >= 2: - for perm in perms: - print("Adding permission '%s'" % perm) +from django.utils.translation import ugettext_lazy as _ class AppConfig(django.apps.AppConfig): name = 'sapl.base' label = 'base' verbose_name = _('Dados Básicos') - - def ready(self): - #pre_migrate.connect(run_sql_organizers, self) - - post_migrate.connect( - receiver=create_proxy_permissions, - dispatch_uid="django.contrib.auth.management.create_permissions") diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 46357ee00..5b4d9f734 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -1,22 +1,17 @@ +import django_filters from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row -from crispy_forms.templatetags.crispy_forms_field import css_class - from django import forms from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.models import Group from django.contrib.auth.password_validation import validate_password -from django.contrib.contenttypes.fields import GenericRel from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import models, transaction from django.forms import ModelForm -from django.utils.translation import ugettext_lazy as _, string_concat - -import django_filters from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat @@ -26,11 +21,9 @@ from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column, from sapl.materia.models import MateriaLegislativa from sapl.sessao.models import SessaoPlenaria from sapl.settings import MAX_IMAGE_UPLOAD_SIZE -from sapl.utils import (RANGE_ANOS, ImageThumbnailFileInput, - RangeWidgetOverride, autor_label, autor_modal, - - SaplGenericRelation, models_with_gr_for_model, - ChoiceWithoutValidationField) +from sapl.utils import (RANGE_ANOS, ChoiceWithoutValidationField, + ImageThumbnailFileInput, RangeWidgetOverride, + autor_label, autor_modal, models_with_gr_for_model) from .models import AppConfig, CasaLegislativa diff --git a/sapl/base/legacy.yaml b/sapl/base/legacy.yaml new file mode 100644 index 000000000..a54c4ae06 --- /dev/null +++ b/sapl/base/legacy.yaml @@ -0,0 +1,8 @@ +TipoAutor: + descricao: des_tipo_autor + +Autor: + nome: nom_autor + cargo: des_cargo + tipo: tip_autor + username: col_username diff --git a/sapl/base/models.py b/sapl/base/models.py index ad7b7a5aa..ddf48628c 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -1,18 +1,10 @@ -from builtins import LookupError - -from django.apps import apps -from django.contrib.auth.management import _get_all_permissions from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType -from django.core import exceptions -from django.db import models, router -from django.db.utils import DEFAULT_DB_ALIAS -from django.utils.translation import string_concat +from django.db import models from django.utils.translation import ugettext_lazy as _ from sapl.utils import UF, YES_NO_CHOICES, get_settings_auth_user_model - TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')), ('R', _('Restritivo'))) diff --git a/sapl/base/templatetags/common_tags.py b/sapl/base/templatetags/common_tags.py index 60cb80f62..b4f4bb23d 100644 --- a/sapl/base/templatetags/common_tags.py +++ b/sapl/base/templatetags/common_tags.py @@ -3,7 +3,6 @@ from django import template from sapl.base.models import AppConfig from sapl.parlamentares.models import Filiacao -from sapl.utils import permissoes_adm register = template.Library() @@ -87,26 +86,6 @@ def get_delete_perm(value, arg): return perm.__contains__(nome_app + can_delete) -@register.filter -def get_doc_adm_template_perms(user): - app_config = AppConfig.objects.last() - - if app_config: - if app_config.documentos_administrativos == 'O': - return True - - return user.has_perms(permissoes_adm()) - - -@register.filter -def ver_menu_sistema_perm(value): - u = value - if u.groups.filter(name='Operador Geral').exists() or u.is_superuser: - return True - else: - return False - - @register.filter def ultima_filiacao(value): parlamentar = value @@ -120,11 +99,6 @@ def ultima_filiacao(value): return None -@register.filter -def get_config_not_exists(user): - return not AppConfig.objects.exists() - - @register.filter def get_config_attr(attribute): return AppConfig.attr(attribute) diff --git a/sapl/base/templatetags/menus.py b/sapl/base/templatetags/menus.py index 1ce1d4ea5..e5b6b80d7 100644 --- a/sapl/base/templatetags/menus.py +++ b/sapl/base/templatetags/menus.py @@ -1,11 +1,10 @@ +import yaml from django import template from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ -import yaml from sapl.utils import sapl_logger - register = template.Library() diff --git a/sapl/base/views.py b/sapl/base/views.py index 1edcb58f4..4de2da9fc 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -13,8 +13,8 @@ from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import TemplateView from django_filters.views import FilterView -from sapl.base.forms import AutorForm, TipoAutorForm, AutorFormForAdmin +from sapl.base.forms import AutorForm, AutorFormForAdmin, TipoAutorForm from sapl.base.models import Autor, TipoAutor from sapl.crud.base import CrudAux from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa diff --git a/sapl/compilacao/forms.py b/sapl/compilacao/forms.py index b5b7cc09c..1559b97ef 100644 --- a/sapl/compilacao/forms.py +++ b/sapl/compilacao/forms.py @@ -42,10 +42,18 @@ class TipoTaForm(ModelForm): label=TipoTextoArticulado._meta.get_field( 'descricao').verbose_name) - participacao_social = forms.NullBooleanField( + participacao_social = forms.ChoiceField( label=TipoTextoArticulado._meta.get_field( 'participacao_social').verbose_name, - widget=forms.Select(choices=YES_NO_CHOICES), + choices=YES_NO_CHOICES, + widget=forms.RadioSelect(), + required=True) + + publicacao_func = forms.ChoiceField( + label=TipoTextoArticulado._meta.get_field( + 'publicacao_func').verbose_name, + choices=YES_NO_CHOICES, + widget=forms.RadioSelect(), required=True) class Meta: @@ -54,21 +62,27 @@ class TipoTaForm(ModelForm): 'descricao', 'content_type', 'participacao_social', + 'publicacao_func' ] def __init__(self, *args, **kwargs): row1 = to_row([ - ('sigla', 2), - ('descricao', 4), - ('content_type', 3), - ('participacao_social', 3), + ('sigla', 3), + ('descricao', 5), + ('content_type', 4), + ]) + row2 = to_row([ + (InlineRadios('participacao_social'), 3), + (InlineRadios('publicacao_func'), 3), ]) self.helper = FormHelper() self.helper.layout = SaplFormLayout( Fieldset(_('Identificação Básica'), - row1, css_class="col-md-12")) + row1, css_class="col-md-12"), + Fieldset(_('Funcionalidades'), + row2, css_class="col-md-12")) super(TipoTaForm, self).__init__(*args, **kwargs) diff --git a/sapl/compilacao/migrations/0059_auto_20161027_1323.py b/sapl/compilacao/migrations/0059_auto_20161027_1323.py new file mode 100644 index 000000000..fdfe8de37 --- /dev/null +++ b/sapl/compilacao/migrations/0059_auto_20161027_1323.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-27 13:23 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0058_auto_20161008_0143'), + ] + + operations = [ + migrations.AlterModelOptions( + name='dispositivo', + options={'ordering': ['ta', 'ordem'], 'permissions': (('change_dispositivo_edicao_dinamica', 'Permissão de edição de dispositivos originais via editor dinâmico.'), ('change_dispositivo_edicao_avancada', 'Permissão de edição de dispositivos originais via formulários de edição avançada.'), ('change_dispositivo_registros_compilacao', 'Permissão de registro de compilação via editor dinâmico.'), ('change_dispositivo_notificacoes', 'Permissão de acesso às notificações de pendências.')), 'verbose_name': 'Dispositivo', 'verbose_name_plural': 'Dispositivos'}, + ), + ] diff --git a/sapl/compilacao/migrations/0060_auto_20161101_0913.py b/sapl/compilacao/migrations/0060_auto_20161101_0913.py new file mode 100644 index 000000000..851746ec0 --- /dev/null +++ b/sapl/compilacao/migrations/0060_auto_20161101_0913.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-01 09:13 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0059_auto_20161027_1323'), + ] + + operations = [ + migrations.AlterModelOptions( + name='dispositivo', + options={'ordering': ['ta', 'ordem'], 'permissions': (('change_dispositivo_edicao_dinamica', 'Permissão de edição de dispositivos originais via editor dinâmico.'), ('change_dispositivo_edicao_avancada', 'Permissão de edição de dispositivos originais via formulários de edição avançada.'), ('change_dispositivo_registros_compilacao', 'Permissão de registro de compilação via editor dinâmico.'), ('view_dispositivo_notificacoes', 'Permissão de acesso às notificações de pendências.')), 'verbose_name': 'Dispositivo', 'verbose_name_plural': 'Dispositivos'}, + ), + ] diff --git a/sapl/compilacao/migrations/0061_auto_20161101_1025.py b/sapl/compilacao/migrations/0061_auto_20161101_1025.py new file mode 100644 index 000000000..d74bdc8da --- /dev/null +++ b/sapl/compilacao/migrations/0061_auto_20161101_1025.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-01 10:25 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0060_auto_20161101_0913'), + ] + + operations = [ + migrations.AlterField( + model_name='dispositivo', + name='ta', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dispositivos_set', to='compilacao.TextoArticulado', verbose_name='Texto Articulado'), + ), + ] diff --git a/sapl/compilacao/migrations/0062_auto_20161101_1221.py b/sapl/compilacao/migrations/0062_auto_20161101_1221.py new file mode 100644 index 000000000..63e703843 --- /dev/null +++ b/sapl/compilacao/migrations/0062_auto_20161101_1221.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-01 12:21 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0061_auto_20161101_1025'), + ] + + operations = [ + migrations.AlterModelOptions( + name='dispositivo', + options={'ordering': ['ta', 'ordem'], 'permissions': (('change_dispositivo_edicao_dinamica', 'Permissão de edição de dispositivos originais via editor dinâmico.'), ('change_dispositivo_edicao_avancada', 'Permissão de edição de dispositivos originais via formulários de edição avançada.'), ('change_dispositivo_registros_compilacao', 'Permissão de registro de compilação via editor dinâmico.'), ('view_dispositivo_notificacoes', 'Permissão de acesso às notificações de pendências.'), ('change_dispositivo_de_vigencia_global', 'Permissão alteração global do dispositivo de vigência')), 'verbose_name': 'Dispositivo', 'verbose_name_plural': 'Dispositivos'}, + ), + ] diff --git a/sapl/compilacao/migrations/0063_tipotextoarticulado_publicacao_func.py b/sapl/compilacao/migrations/0063_tipotextoarticulado_publicacao_func.py new file mode 100644 index 000000000..53dac62fc --- /dev/null +++ b/sapl/compilacao/migrations/0063_tipotextoarticulado_publicacao_func.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-03 11:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0062_auto_20161101_1221'), + ] + + operations = [ + migrations.AddField( + model_name='tipotextoarticulado', + name='publicacao_func', + field=models.NullBooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=True, verbose_name='Histórico de Publicação'), + ), + ] diff --git a/sapl/compilacao/migrations/0064_auto_20161104_1420.py b/sapl/compilacao/migrations/0064_auto_20161104_1420.py new file mode 100644 index 000000000..508bc17ac --- /dev/null +++ b/sapl/compilacao/migrations/0064_auto_20161104_1420.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-04 14:20 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('compilacao', '0063_tipotextoarticulado_publicacao_func'), + ] + + operations = [ + migrations.AlterModelOptions( + name='textoarticulado', + options={'ordering': ['-data', '-numero'], 'permissions': (('view_restricted_textoarticulado', 'Pode ver qualquer Texto Articulado'),), 'verbose_name': 'Texto Articulado', 'verbose_name_plural': 'Textos Articulados'}, + ), + migrations.AddField( + model_name='textoarticulado', + name='editable_only_by_owners', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=True, verbose_name='Editável apenas pelos donos do Texto Articulado'), + ), + migrations.AddField( + model_name='textoarticulado', + name='editing_locked', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=True, verbose_name='Texto Articulado em Edição'), + ), + migrations.AddField( + model_name='textoarticulado', + name='owners', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Donos do Texto Articulado'), + ), + migrations.AddField( + model_name='textoarticulado', + name='visibilidade', + field=models.IntegerField(choices=[(99, 'Privado'), (79, 'Restrito'), (89, 'Em Edição'), (0, 'Público')], default=99, verbose_name='Visibilidade'), + ), + migrations.AlterField( + model_name='tipotextoarticulado', + name='publicacao_func', + field=models.NullBooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Histórico de Publicação'), + ), + ] diff --git a/sapl/compilacao/migrations/0065_auto_20161107_1024.py b/sapl/compilacao/migrations/0065_auto_20161107_1024.py new file mode 100644 index 000000000..5da908346 --- /dev/null +++ b/sapl/compilacao/migrations/0065_auto_20161107_1024.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-07 10:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0064_auto_20161104_1420'), + ] + + operations = [ + migrations.RenameField( + model_name='textoarticulado', + old_name='visibilidade', + new_name='privacidade' + ), + ] diff --git a/sapl/compilacao/migrations/0066_auto_20161107_1028.py b/sapl/compilacao/migrations/0066_auto_20161107_1028.py new file mode 100644 index 000000000..a7c3ca92c --- /dev/null +++ b/sapl/compilacao/migrations/0066_auto_20161107_1028.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-07 10:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0065_auto_20161107_1024'), + ] + + operations = [ + migrations.AlterField( + model_name='textoarticulado', + name='privacidade', + field=models.IntegerField(choices=[(99, 'Privado'), (79, 'Restrito'), (89, 'Em Edição'), (0, 'Público')], default=99, verbose_name='Privacidade'), + ), + ] diff --git a/sapl/compilacao/migrations/0067_auto_20161107_1351.py b/sapl/compilacao/migrations/0067_auto_20161107_1351.py new file mode 100644 index 000000000..54fcc16dc --- /dev/null +++ b/sapl/compilacao/migrations/0067_auto_20161107_1351.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-07 13:51 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0066_auto_20161107_1028'), + ] + + operations = [ + migrations.AlterModelOptions( + name='textoarticulado', + options={'ordering': ['-data', '-numero'], 'permissions': (('view_restricted_textoarticulado', 'Pode ver qualquer Texto Articulado'), ('lock_unlock_textoarticulado', 'Pode bloquear/desbloquear edição de Texto Articulado')), 'verbose_name': 'Texto Articulado', 'verbose_name_plural': 'Textos Articulados'}, + ), + ] diff --git a/sapl/compilacao/migrations/0068_auto_20161107_1546.py b/sapl/compilacao/migrations/0068_auto_20161107_1546.py new file mode 100644 index 000000000..96c5f726b --- /dev/null +++ b/sapl/compilacao/migrations/0068_auto_20161107_1546.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-07 15:46 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0067_auto_20161107_1351'), + ] + + operations = [ + migrations.AlterModelOptions( + name='dispositivo', + options={'ordering': ['ta', 'ordem'], 'permissions': (('change_dispositivo_edicao_dinamica', 'Permissão de edição de dispositivos originais via editor dinâmico.'), ('change_your_dispositivo_edicao_dinamica', 'Permissão de edição de dispositivos originais via editor dinâmico desde que seja dono.'), ('change_dispositivo_edicao_avancada', 'Permissão de edição de dispositivos originais via formulários de edição avançada.'), ('change_dispositivo_registros_compilacao', 'Permissão de registro de compilação via editor dinâmico.'), ('view_dispositivo_notificacoes', 'Permissão de acesso às notificações de pendências.'), ('change_dispositivo_de_vigencia_global', 'Permissão alteração global do dispositivo de vigência')), 'verbose_name': 'Dispositivo', 'verbose_name_plural': 'Dispositivos'}, + ), + ] diff --git a/sapl/compilacao/migrations/0069_auto_20161107_1932.py b/sapl/compilacao/migrations/0069_auto_20161107_1932.py new file mode 100644 index 000000000..743167a56 --- /dev/null +++ b/sapl/compilacao/migrations/0069_auto_20161107_1932.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-07 19:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0068_auto_20161107_1546'), + ] + + operations = [ + migrations.AlterField( + model_name='textoarticulado', + name='privacidade', + field=models.IntegerField(choices=[(99, 'Privado'), (79, 'Imotável Restrito'), (69, 'Imutável Público'), (89, 'Em Edição'), (0, 'Público')], default=99, verbose_name='Privacidade'), + ), + ] diff --git a/sapl/compilacao/models.py b/sapl/compilacao/models.py index da2708cfe..6752a82c1 100644 --- a/sapl/compilacao/models.py +++ b/sapl/compilacao/models.py @@ -1,8 +1,10 @@ +from django.contrib import messages from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models import F, Q from django.db.models.aggregates import Max +from django.http.response import Http404 from django.template import defaultfilters from django.utils.translation import ugettext_lazy as _ @@ -81,6 +83,12 @@ class TipoTextoArticulado(models.Model): choices=YES_NO_CHOICES, verbose_name=_('Participação Social')) + publicacao_func = models.NullBooleanField( + default=False, + blank=True, null=True, + choices=YES_NO_CHOICES, + verbose_name=_('Histórico de Publicação')) + class Meta: verbose_name = _('Tipo de Texto Articulado') verbose_name_plural = _('Tipos de Texto Articulados') @@ -95,6 +103,25 @@ PARTICIPACAO_SOCIAL_CHOICES = [ (False, _('Não'))] +STATUS_TA_PRIVATE = 99 # Só os donos podem ver +STATUS_TA_EDITION = 89 +STATUS_TA_IMMUTABLE_RESTRICT = 79 + +STATUS_TA_IMMUTABLE_PUBLIC = 69 + +STATUS_TA_PUBLIC = 0 + +PRIVACIDADE_STATUS = ( + (STATUS_TA_PRIVATE, _('Privado')), # só dono ve e edita + # só quem tem permissão para ver + (STATUS_TA_IMMUTABLE_RESTRICT, _('Imotável Restrito')), + # só quem tem permissão para ver + (STATUS_TA_IMMUTABLE_PUBLIC, _('Imutável Público')), + (STATUS_TA_EDITION, _('Em Edição')), # só quem tem permissão para editar + (STATUS_TA_PUBLIC, _('Público')), # visualização pública +) + + class TextoArticulado(TimestampedMixin): data = models.DateField(blank=True, null=True, verbose_name=_('Data')) ementa = models.TextField(verbose_name=_('Ementa')) @@ -118,10 +145,35 @@ class TextoArticulado(TimestampedMixin): blank=True, null=True, default=None) content_object = GenericForeignKey('content_type', 'object_id') + owners = models.ManyToManyField( + get_settings_auth_user_model(), + blank=True, verbose_name=_('Donos do Texto Articulado')) + + editable_only_by_owners = models.BooleanField( + choices=YES_NO_CHOICES, + default=True, + verbose_name=_('Editável apenas pelos donos do Texto Articulado')) + + editing_locked = models.BooleanField( + choices=YES_NO_CHOICES, + default=True, + verbose_name=_('Texto Articulado em Edição')) + + privacidade = models.IntegerField( + _('Privacidade'), + choices=PRIVACIDADE_STATUS, + default=STATUS_TA_PRIVATE) + class Meta: verbose_name = _('Texto Articulado') verbose_name_plural = _('Textos Articulados') ordering = ['-data', '-numero'] + permissions = ( + ('view_restricted_textoarticulado', + _('Pode ver qualquer Texto Articulado')), + ('lock_unlock_textoarticulado', + _('Pode bloquear/desbloquear edição de Texto Articulado')), + ) def __str__(self): if self.content_object: @@ -132,6 +184,102 @@ class TextoArticulado(TimestampedMixin): 'numero': self.numero, 'data': defaultfilters.date(self.data, "d \d\e F \d\e Y")} + def hash(self): + from django.core import serializers + import hashlib + data = serializers.serialize( + "xml", Dispositivo.objects.filter( + Q(ta_id=self.id) | Q(ta_publicado_id=self.id))) + md5 = hashlib.md5() + md5.update(data.encode('utf-8')) + return md5.hexdigest() + + def can_use_dynamic_editing(self, user): + return not self.editing_locked and\ + (not self.editable_only_by_owners and + user.has_perm( + 'compilacao.change_dispositivo_edicao_dinamica') or + self.editable_only_by_owners and user in self.owners.all() and + user.has_perm( + 'compilacao.change_your_dispositivo_edicao_dinamica')) + + def has_view_permission(self, request): + if self.privacidade in (STATUS_TA_IMMUTABLE_PUBLIC, STATUS_TA_PUBLIC): + return True + + if request.user in self.owners.all(): + return True + + if self.privacidade == STATUS_TA_IMMUTABLE_RESTRICT and\ + request.user.has_perm( + 'compilacao.view_restricted_textoarticulado'): + return True + + elif self.privacidade == STATUS_TA_EDITION: + if request.user.has_perm( + 'compilacao.change_dispositivo_edicao_dinamica'): + return True + else: + messages.error(request, _( + 'Este Texto Articulado está em edição.')) + + elif self.privacidade == STATUS_TA_PRIVATE: + if request.user in self.owners.all(): + return True + else: + raise Http404() + + return False + + def has_edit_permission(self, request): + + if self.privacidade == STATUS_TA_PRIVATE: + if request.user not in self.owners.all(): + raise Http404() + + if not self.can_use_dynamic_editing(request.user): + messages.error(request, _( + 'Usuário sem permissão para edição.')) + return False + else: + return True + + if self.privacidade == STATUS_TA_IMMUTABLE_RESTRICT: + messages.error(request, _( + 'A edição deste Texto Articulado está bloqueada. ' + 'Este documento é imutável e de acesso é restrito.')) + return False + + if self.privacidade == STATUS_TA_IMMUTABLE_PUBLIC: + messages.error(request, _( + 'A edição deste Texto Articulado está bloqueada. ' + 'Este documento é imutável.')) + return False + + if self.editing_locked and\ + self.privacidade in (STATUS_TA_PUBLIC, STATUS_TA_EDITION) and\ + not request.user.has_perm( + 'compilacao.lock_unlock_textoarticulado'): + messages.error(request, _( + 'A edição deste Texto Articulado está bloqueada. ' + 'É necessário acessar com usuário que possui ' + 'permissão de desbloqueio.')) + return False + + if not request.user.has_perm( + 'compilacao.change_dispositivo_edicao_dinamica'): + messages.error(request, _( + 'Usuário sem permissão para edição.')) + return False + + if self.editable_only_by_owners and\ + request.user not in self.owners.all(): + messages.error(request, _( + 'Apenas usuários donos do Texto Articulado podem editá-lo.')) + return False + + return True + def reagrupar_ordem_de_dispositivos(self): dpts = Dispositivo.objects.filter(ta=self) @@ -635,7 +783,7 @@ class Dispositivo(BaseModel, TimestampedMixin): ta = models.ForeignKey( TextoArticulado, - on_delete=models.PROTECT, + on_delete=models.CASCADE, related_name='dispositivos_set', verbose_name=_('Texto Articulado')) ta_publicado = models.ForeignKey( @@ -693,6 +841,23 @@ class Dispositivo(BaseModel, TimestampedMixin): 'ta_publicado', 'publicacao',), ) + permissions = ( + ('change_dispositivo_edicao_dinamica', _( + 'Permissão de edição de dispositivos originais ' + 'via editor dinâmico.')), + ('change_your_dispositivo_edicao_dinamica', _( + 'Permissão de edição de dispositivos originais ' + 'via editor dinâmico desde que seja dono.')), + ('change_dispositivo_edicao_avancada', _( + 'Permissão de edição de dispositivos originais ' + 'via formulários de edição avançada.')), + ('change_dispositivo_registros_compilacao', _( + 'Permissão de registro de compilação via editor dinâmico.')), + ('view_dispositivo_notificacoes', _( + 'Permissão de acesso às notificações de pendências.')), + ('change_dispositivo_de_vigencia_global', _( + 'Permissão alteração global do dispositivo de vigência')), + ) def __str__(self): return '%(rotulo)s' % { @@ -1248,7 +1413,7 @@ class Dispositivo(BaseModel, TimestampedMixin): disps[0].get_numero_completo()) # dispositivo.transform_in_next() else: - dispositivo.set_numero_completo([1, 0, 0, 0, 0, 0, ]) + dispositivo.set_numero_completo([0, 0, 0, 0, 0, 0, ]) else: if ';' in tipo_base.rotulo_prefixo_texto: diff --git a/sapl/compilacao/templatetags/compilacao_filters.py b/sapl/compilacao/templatetags/compilacao_filters.py index de8b70530..f3965c43d 100644 --- a/sapl/compilacao/templatetags/compilacao_filters.py +++ b/sapl/compilacao/templatetags/compilacao_filters.py @@ -211,7 +211,7 @@ def heranca(request, d, ignore_ultimo=0, ignore_primeiro=0): ta_id = str(d.ta_id) d_pk = str(d.pk) if ta_id not in ta_dpts_parents or d_pk not in ta_dpts_parents[ta_id]: - print('recarregando estrutura temporaria de heranças') + #print('recarregando estrutura temporaria de heranças') dpts_parents = {} ta_dpts_parents[ta_id] = dpts_parents update_dispositivos_parents(dpts_parents, ta_id) @@ -295,3 +295,7 @@ def urldetail_content_type(obj): @register.filter def list(obj): return [obj, ] + +@register.filter +def can_use_dynamic_editing(texto_articulado, user): + return texto_articulado.can_use_dynamic_editing(user) diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index 9b7f79d95..3b25eb045 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -1,16 +1,16 @@ -import logging -import sys from collections import OrderedDict from datetime import datetime, timedelta +import logging +import sys from braces.views import FormMessagesMixin from django import forms from django.conf import settings from django.contrib import messages -from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.contenttypes.models import ContentType from django.core.signing import Signer -from django.core.urlresolvers import reverse_lazy +from django.core.urlresolvers import reverse_lazy, reverse from django.db import connection, transaction from django.db.models import Q from django.db.utils import IntegrityError @@ -18,10 +18,9 @@ from django.http.response import (HttpResponse, HttpResponseRedirect, JsonResponse) from django.shortcuts import get_object_or_404, redirect from django.utils.dateparse import parse_date -from django.utils.decorators import method_decorator from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import TemplateView from django.views.generic.detail import DetailView from django.views.generic.edit import (CreateView, DeleteView, FormView, @@ -43,12 +42,14 @@ from sapl.compilacao.models import (Dispositivo, Nota, Publicacao, TextoArticulado, TipoDispositivo, TipoNota, TipoPublicacao, TipoTextoArticulado, TipoVide, - VeiculoPublicacao, Vide) + VeiculoPublicacao, Vide, STATUS_TA_EDITION, + STATUS_TA_PRIVATE, STATUS_TA_PUBLIC) from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED, DISPOSITIVO_SELECT_RELATED_EDIT) from sapl.crud.base import Crud, CrudListView, make_pagination from sapl.settings import BASE_DIR + TipoNotaCrud = Crud.build(TipoNota, 'tipo_nota') TipoVideCrud = Crud.build(TipoVide, 'tipo_vide') TipoPublicacaoCrud = Crud.build(TipoPublicacao, 'tipo_publicacao') @@ -87,12 +88,31 @@ def choice_models_in_extenal_views(): return result +def choice_model_type_foreignkey_in_extenal_views(id_tipo_ta=None): + yield None, '-------------' + + if not id_tipo_ta: + return + + tipo_ta = TipoTextoArticulado.objects.get(pk=id_tipo_ta) + + integrations_view_names = get_integrations_view_names() + for item in integrations_view_names: + if hasattr(item, 'model_type_foreignkey'): + if (tipo_ta.content_type.model == item.model.__name__.lower() and + tipo_ta.content_type.app_label == + item.model._meta.app_label): + for i in item.model_type_foreignkey.objects.all(): + yield i.pk, i + + class IntegracaoTaView(TemplateView): def get_redirect_deactivated(self): messages.error( self.request, - _('O modulo de Textos Articulados está desativado.')) + _('O modulo de Textos Articulados para %s está desativado.' + ) % self.model._meta.verbose_name_plural) return redirect('/') def get(self, request, *args, **kwargs): @@ -100,6 +120,16 @@ class IntegracaoTaView(TemplateView): try: if settings.DEBUG or not TipoDispositivo.objects.exists(): self.import_pattern() + + if hasattr(self, 'map_funcs'): + tipo_ta = TipoTextoArticulado.objects.get( + content_type=ContentType.objects.get_for_model( + self.model)) + + for key, value in self.map_funcs.items(): + setattr(tipo_ta, key, value) + tipo_ta.save() + except Exception as e: logger.error( string_concat( @@ -108,6 +138,26 @@ class IntegracaoTaView(TemplateView): str(e))) return self.get_redirect_deactivated() + assert hasattr(self, 'map_fields'), _( + """ + O mapa dos campos não foi definido. Ele deve seguir a estrutura + de chaves abaixo: + + map_fields = { + 'data': 'data', + 'ementa': 'ementa', + 'observacao': 'observacao', + 'numero': 'numero', + 'ano': 'ano', + } + + Caso o model de integração não possua um dos campos, + implemente, ou passe `None` para as chaves que são fixas. + """) + + map_fields = self.map_fields + ta_values = getattr(self, 'ta_values', {}) + item = get_object_or_404(self.model, pk=kwargs['pk']) related_object_type = ContentType.objects.get_for_model(item) @@ -118,81 +168,71 @@ class IntegracaoTaView(TemplateView): tipo_ta = TipoTextoArticulado.objects.filter( content_type=related_object_type) - if not ta.exists(): + ta_exists = bool(ta.exists()) + if not ta_exists: ta = TextoArticulado() tipo_ta = TipoTextoArticulado.objects.filter( content_type=related_object_type)[:1] if tipo_ta.exists(): ta.tipo_ta = tipo_ta[0] ta.content_object = item - else: - ta = ta[0] - if hasattr(item, 'ementa') and item.ementa: - ta.ementa = item.ementa - else: - ta.ementa = _('Integração com %s sem ementa.') % item + ta.privacidade = ta_values.get('privacidade', STATUS_TA_EDITION) - if hasattr(item, 'observacao') and item.observacao: - ta.observacao = item.observacao - else: - ta.observacao = _('Integração com %s sem observacao.') % item + ta.editing_locked = ta_values.get('editing_locked', False) + ta.editable_only_by_owners = ta_values.get( + 'editable_only_by_owners', False) - if hasattr(item, 'numero') and item.numero: - ta.numero = item.numero else: - ta.numero = int('%s%s%s' % ( - int(datetime.now().year), - int(datetime.now().month), - int(datetime.now().day))) - - if hasattr(item, 'ano') and item.ano: - ta.ano = item.ano - else: - ta.ano = datetime.now().year + ta = ta[0] - if hasattr(item, 'data_apresentacao'): - ta.data = item.data_apresentacao - elif hasattr(item, 'data'): - ta.data = item.data - else: - ta.data = datetime.now() + if not ta.data: + ta.data = getattr(item, map_fields['data'] + if map_fields['data'] else 'xxx', + datetime.now()) + if not ta.data: + ta.data = datetime.now() - ta.save() + ta.ementa = getattr( + item, map_fields['ementa'] + if map_fields['ementa'] else 'xxx', _( + 'Integração com %s sem ementa.') % item) - return redirect(to=reverse_lazy('sapl.compilacao:ta_text', - kwargs={'ta_id': ta.pk})) + ta.observacao = getattr( + item, map_fields['observacao'] + if map_fields['observacao'] else 'xxx', '') - """msg = messages.error if not request.user.is_anonymous( - ) else messages.info + ta.numero = getattr( + item, map_fields['numero'] + if map_fields['numero'] else 'xxx', int('%s%s%s' % ( + int(datetime.now().year), + int(datetime.now().month), + int(datetime.now().day)))) - msg(request, - _('A funcionalidade de Textos Articulados está desativada.')) + ta.ano = getattr(item, map_fields['ano'] + if map_fields['ano'] else 'xxx', datetime.now().year) - if not request.user.is_anonymous(): - msg( - request, - _('Para ativá-la, os Tipos de Textos devem ser criados.')) + ta.save() - msg(request, - _('Sua tela foi redirecionada para a tela de ' - 'cadastro de Textos Articulados.')) + if not ta_exists: + if ta.editable_only_by_owners and\ + not self.request.user.is_anonymous(): + ta.owners.add(self.request.user) - return redirect(to=reverse_lazy('sapl.compilacao:tipo_ta_list', - kwargs={})) + if not Dispositivo.objects.filter(ta_id=ta.pk).exists() and\ + ta.can_use_dynamic_editing(self.request.user): + return redirect(to=reverse_lazy('sapl.compilacao:ta_text_edit', + kwargs={'ta_id': ta.pk})) else: - - return redirect(to=reverse_lazy( - '%s:%s_detail' % ( - item._meta.app_config.name, item._meta.model_name), - kwargs={'pk': item.pk}))""" + return redirect(to=reverse_lazy('sapl.compilacao:ta_text', + kwargs={'ta_id': ta.pk})) def import_pattern(self): from unipath import Path compilacao_app = Path(__file__).ancestor(1) - print(compilacao_app) + # print(compilacao_app) with open(compilacao_app + '/compilacao_data_tables.sql', 'r') as f: lines = f.readlines() lines = [line.rstrip('\n') for line in lines] @@ -251,7 +291,20 @@ class IntegracaoTaView(TemplateView): abstract = True -class CompMixin: +class CompMixin(PermissionRequiredMixin): + permission_required = [] + + def has_permission(self): + perms = self.get_permission_required() + # Torna a view pública se não possuir conteudo + # no atributo permission_required + return self.request.user.has_perms(perms) if len(perms) else True + + @property + def ta(self): + ta = TextoArticulado.objects.get( + pk=self.kwargs.get('ta_id', self.kwargs.get('pk', 0))) + return ta def get_context_data(self, **kwargs): context = super(CompMixin, self).get_context_data(**kwargs) @@ -271,6 +324,7 @@ class TipoTaListView(CompMixin, ListView): model = TipoTextoArticulado paginate_by = 10 verbose_name = model._meta.verbose_name + permission_required = 'compilacao.list_tipotextoarticulado' @property def title(self): @@ -287,6 +341,7 @@ class TipoTaCreateView(CompMixin, FormMessagesMixin, CreateView): 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 @@ -308,12 +363,14 @@ class TipoTaCreateView(CompMixin, FormMessagesMixin, CreateView): 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() @@ -336,6 +393,7 @@ class TipoTaUpdateView(CompMixin, UpdateView): class TipoTaDeleteView(CompMixin, DeleteView): model = TipoTextoArticulado template_name = "crud/confirm_delete.html" + permission_required = 'compilacao.delete_tipotextoarticulado' @property def detail_url(self): @@ -350,6 +408,7 @@ class TaListView(CompMixin, ListView): model = TextoArticulado paginate_by = 10 verbose_name = model._meta.verbose_name + permission_required = 'compilacao.list_textoarticulado' @property def title(self): @@ -367,10 +426,26 @@ class TaListView(CompMixin, ListView): page_obj.number, paginator.num_pages) return context + def get_queryset(self): + qs = ListView.get_queryset(self) + + qs = qs.exclude( + ~Q(owners=self.request.user.id), + privacidade=STATUS_TA_PRIVATE) + + return qs + class TaDetailView(CompMixin, DetailView): model = TextoArticulado + def has_permission(self): + self.object = self.ta + if self.object.has_view_permission(self.request): + return CompMixin.has_permission(self) + else: + return False + @property def title(self): if self.get_object().content_object: @@ -389,6 +464,7 @@ class TaCreateView(CompMixin, FormMessagesMixin, CreateView): 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_success_url(self): return reverse_lazy('sapl.compilacao:ta_detail', @@ -403,6 +479,7 @@ class TaUpdateView(CompMixin, UpdateView): model = TextoArticulado form_class = TaForm template_name = "crud/form.html" + permission_required = 'compilacao.change_textoarticulado' def get(self, request, *args, **kwargs): self.object = self.get_object() @@ -425,6 +502,7 @@ class TaUpdateView(CompMixin, UpdateView): class TaDeleteView(CompMixin, DeleteView): model = TextoArticulado template_name = "crud/confirm_delete.html" + permission_required = 'compilacao.delete_textoarticulado' @property def detail_url(self): @@ -464,14 +542,11 @@ class NotaMixin(DispositivoSuccessUrlMixin): return initial - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super(NotaMixin, self).dispatch(*args, **kwargs) - class NotasCreateView(NotaMixin, CreateView): template_name = 'compilacao/ajax_form.html' form_class = NotaForm + permission_required = 'compilacao.add_nota' def get(self, request, *args, **kwargs): flag_action, modelo_nota = self.get_modelo_nota(request) @@ -506,6 +581,7 @@ class NotasEditView(NotaMixin, UpdateView): model = Nota template_name = 'compilacao/ajax_form.html' form_class = NotaForm + permission_required = 'compilacao.change_nota' def get(self, request, *args, **kwargs): flag_action, modelo_nota = self.get_modelo_nota(request) @@ -517,6 +593,8 @@ class NotasEditView(NotaMixin, UpdateView): class NotasDeleteView(NotaMixin, TemplateView): + permission_required = 'compilacao.delete_nota' + def get(self, request, *args, **kwargs): nt = Nota.objects.get(pk=self.kwargs['pk']) nt.delete() @@ -535,59 +613,28 @@ class VideMixin(DispositivoSuccessUrlMixin): return initial - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super(VideMixin, self).dispatch(*args, **kwargs) - - -def choice_model_type_foreignkey_in_extenal_views(id_tipo_ta=None): - yield None, '-------------' - - if not id_tipo_ta: - return - - tipo_ta = TipoTextoArticulado.objects.get(pk=id_tipo_ta) - - integrations_view_names = get_integrations_view_names() - for item in integrations_view_names: - if hasattr(item, 'model_type_foreignkey'): - if (tipo_ta.content_type.model == item.model.__name__.lower() and - tipo_ta.content_type.app_label == - item.model._meta.app_label): - for i in item.model_type_foreignkey.objects.all(): - yield i.pk, i - class VideCreateView(VideMixin, CreateView): model = Vide template_name = 'compilacao/ajax_form.html' form_class = VideForm + permission_required = 'compilacao.add_vide' def get(self, request, *args, **kwargs): self.object = None form = self.get_form() return self.render_to_response(self.get_context_data(form=form)) - """ - def get_form_kwargs(self): - - kwargs = super(VideCreateView, self).get_form_kwargs() - - if 'choice_model_type_foreignkey_in_extenal_views' not in kwargs: - kwargs.update({ - 'choice_model_type_foreignkey_in_extenal_views': - choice_model_type_foreignkey_in_extenal_views - }) - - return kwargs""" class VideEditView(VideMixin, UpdateView): model = Vide template_name = 'compilacao/ajax_form.html' form_class = VideForm + permission_required = 'compilacao.change_vide' class VideDeleteView(VideMixin, TemplateView): + permission_required = 'compilacao.delete_vide' def get(self, request, *args, **kwargs): vd = Vide.objects.get(pk=self.kwargs['pk']) @@ -595,9 +642,26 @@ class VideDeleteView(VideMixin, TemplateView): return HttpResponseRedirect(self.get_success_url()) -class PublicacaoListView(CompMixin, ListView): +class PublicacaoMixin(CompMixin): + + def dispatch(self, request, *args, **kwargs): + ta = self.ta + if not ta.tipo_ta.publicacao_func: + messages.error(request, _( + 'A funcionalidade de %s está desativada para %s.') % ( + TipoTextoArticulado._meta.get_field( + 'publicacao_func').verbose_name, + ta.tipo_ta.descricao)) + return redirect(reverse('sapl.compilacao:ta_text', + kwargs={'ta_id': self.kwargs['ta_id']})) + + return PermissionRequiredMixin.dispatch(self, request, *args, **kwargs) + + +class PublicacaoListView(PublicacaoMixin, ListView): model = Publicacao verbose_name = model._meta.verbose_name + permission_required = [] @property def title(self): @@ -605,11 +669,6 @@ class PublicacaoListView(CompMixin, ListView): self.model._meta.verbose_name_plural, self.ta)) - @property - def ta(self): - ta = TextoArticulado.objects.get(pk=self.kwargs['ta_id']) - return ta - @property def create_url(self): return reverse_lazy( @@ -626,12 +685,13 @@ class PublicacaoListView(CompMixin, ListView): return context -class PublicacaoCreateView(CompMixin, FormMessagesMixin, CreateView): +class PublicacaoCreateView(PublicacaoMixin, FormMessagesMixin, CreateView): model = Publicacao form_class = PublicacaoForm 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_publicacao' def get_success_url(self): return reverse_lazy( @@ -650,14 +710,16 @@ class PublicacaoCreateView(CompMixin, FormMessagesMixin, CreateView): return {'ta': self.kwargs['ta_id']} -class PublicacaoDetailView(CompMixin, DetailView): +class PublicacaoDetailView(PublicacaoMixin, DetailView): model = Publicacao + permission_required = 'compilacao.detail_publicacao' -class PublicacaoUpdateView(CompMixin, UpdateView): +class PublicacaoUpdateView(PublicacaoMixin, UpdateView): model = Publicacao form_class = PublicacaoForm template_name = "crud/form.html" + permission_required = 'compilacao.change_publicacao' def get(self, request, *args, **kwargs): self.object = self.get_object() @@ -678,9 +740,10 @@ class PublicacaoUpdateView(CompMixin, UpdateView): return self.get_success_url() -class PublicacaoDeleteView(CompMixin, DeleteView): +class PublicacaoDeleteView(PublicacaoMixin, DeleteView): model = Publicacao template_name = "crud/confirm_delete.html" + permission_required = 'compilacao.delete_publicacao' @property def detail_url(self): @@ -708,44 +771,14 @@ class TextView(CompMixin, ListView): fim_vigencia = None ta_vigencia = None - def get(self, request, *args, **kwargs): - ta = TextoArticulado.objects.get(pk=self.kwargs['ta_id']) - self.object = ta - if ta.content_object: - item = ta.content_object - self.object = item - if hasattr(item, 'ementa') and item.ementa: - ta.ementa = item.ementa - else: - ta.ementa = _('Integração com %s sem ementa.') % item - - if hasattr(item, 'observacao') and item.observacao: - ta.observacao = item.observacao - else: - ta.observacao = _('Integração com %s sem observacao.') % item - - if hasattr(item, 'numero') and item.numero: - ta.numero = item.numero - else: - ta.numero = int('%s%s%s' % ( - int(datetime.now().year), - int(datetime.now().month), - int(datetime.now().day))) + def has_permission(self): + self.object = self.ta + return self.object.has_view_permission(self.request) - if hasattr(item, 'ano') and item.ano: - ta.ano = item.ano - else: - ta.ano = datetime.now().year - - if hasattr(item, 'data_apresentacao'): - ta.data = item.data_apresentacao - elif hasattr(item, 'data'): - ta.data = item.data - else: - ta.data = datetime.now() - ta.save() - - return super(TextView, self).get(request, *args, **kwargs) + def get(self, request, *args, **kwargs): + if 'print' in request.GET: + self.template_name = 'compilacao/text_list__print_version.html' + return ListView.get(self, request, *args, **kwargs) def get_context_data(self, **kwargs): context = super(TextView, self).get_context_data(**kwargs) @@ -936,9 +969,45 @@ class DispositivoView(TextView): return itens -class TextEditView(TemplateView): +class TextEditView(CompMixin, TemplateView): template_name = 'compilacao/text_edit.html' + def has_permission(self): + self.object = self.ta + return self.object.has_edit_permission(self.request) + + def get(self, request, *args, **kwargs): + + if self.object.editing_locked: + if 'unlock' not in request.GET: + messages.error( + request, _( + 'A edição deste Texto Articulado está bloqueada.')) + return redirect(to=reverse_lazy( + 'sapl.compilacao:ta_text', kwargs={ + 'ta_id': self.object.id})) + else: + # TODO - implementar logging de ação de usuário + self.object.editing_locked = False + self.object.privacidade = STATUS_TA_EDITION + self.object.save() + messages.success(request, _( + 'Texto Articulado desbloqueado com sucesso.')) + else: + if 'lock' in request.GET: + # TODO - implementar logging de ação de usuário + self.object.editing_locked = True + self.object.privacidade = STATUS_TA_PUBLIC + self.object.save() + messages.success(request, _( + 'Texto Articulado bloqueado com sucesso.')) + + return redirect(to=reverse_lazy( + 'sapl.compilacao:ta_text', kwargs={ + 'ta_id': self.object.id})) + + return TemplateView.get(self, request, *args, **kwargs) + def get_context_data(self, **kwargs): dispositivo_id = int(self.kwargs['dispositivo_id']) \ if 'dispositivo_id' in self.kwargs else 0 @@ -946,7 +1015,7 @@ class TextEditView(TemplateView): if dispositivo_id: self.object = Dispositivo.objects.get(pk=dispositivo_id) - context = super(TextEditView, self).get_context_data(**kwargs) + context = super(TemplateView, self).get_context_data(**kwargs) if not dispositivo_id: ta = TextoArticulado.objects.get(pk=self.kwargs['ta_id']) @@ -1721,6 +1790,12 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin): for td in otds: + if td.dispositivo_de_alteracao: + if not self.request.user.has_perm( + 'compilacao.' + 'change_dispositivo_registros_compilacao'): + continue + if paradentro and not td.permitido_inserir_in( tipb, include_relative_autos=True, @@ -2417,11 +2492,10 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin, class DispositivoDinamicEditView( - CompMixin, ActionsEditMixin, TextEditView, UpdateView): + ActionsEditMixin, TextEditView, UpdateView): template_name = 'compilacao/text_edit_bloco.html' model = Dispositivo form_class = DispositivoEdicaoBasicaForm - contador = -1 def get_initial(self): initial = UpdateView.get_initial(self) @@ -2615,7 +2689,6 @@ class DispositivoSearchFragmentFormView(ListView): def get_queryset(self): try: - n = 10 if 'max_results' in self.request.GET: n = int(self.request.GET['max_results']) @@ -2799,6 +2872,8 @@ class DispositivoEdicaoBasicaView(CompMixin, FormMessagesMixin, UpdateView): form_invalid_message = _('Houve erro em registrar ' 'as alterações no Dispositivo') + permission_required = 'compilacao.change_dispositivo_edicao_avancada' + @property def cancel_url(self): return reverse_lazy( @@ -2871,6 +2946,8 @@ class DispositivoEdicaoVigenciaView(CompMixin, FormMessagesMixin, UpdateView): form_invalid_message = _('Houve erro em registrar ' 'as alterações no Dispositivo') + permission_required = 'compilacao.change_dispositivo_edicao_avancada' + @property def cancel_url(self): return reverse_lazy( @@ -2894,6 +2971,9 @@ class DispositivoDefinidorVigenciaView(CompMixin, FormMessagesMixin, FormView): form_invalid_message = _('Houve erro em registrar ' 'as alterações no Dispositivo') + permission_required = ('compilacao.change_dispositivo_edicao_avancada', + 'compilacao.change_dispositivo_de_vigencia_global') + def get_form_kwargs(self): kwargs = FormView.get_form_kwargs(self) kwargs.update({ @@ -2951,6 +3031,8 @@ class DispositivoEdicaoAlteracaoView(CompMixin, FormMessagesMixin, UpdateView): form_invalid_message = _('Houve erro em registrar ' 'as alterações no Dispositivo') + permission_required = 'compilacao.change_dispositivo_registros_compilacao' + @property def cancel_url(self): return reverse_lazy( @@ -2983,6 +3065,8 @@ class TextNotificacoesView(CompMixin, ListView, FormView): template_name = 'compilacao/text_notificacoes.html' form_class = TextNotificacoesForm + permission_required = 'compilacao.view_dispositivo_notificacoes' + def get(self, request, *args, **kwargs): self.object = TextoArticulado.objects.get(pk=self.kwargs['ta_id']) return super(TextNotificacoesView, self).get(request, *args, **kwargs) diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index 51b386d9c..028301502 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -91,7 +91,8 @@ def get_field_display(obj, fieldname): else: display = '' elif 'ManyRelatedManager' in str(type(value))\ - or 'RelatedManager' in str(type(value)): + or 'RelatedManager' in str(type(value))\ + or 'GenericRelatedObjectManager' in str(type(value)): display = ' @@ -61,11 +65,13 @@ {% endfor %} {%endif%} - {% if not object.ta_publicado and not object.dispositivo_subsequente and not object.tipo_dispositivo.dispositivo_de_alteracao%} -
- -
- {%endif%} + {% if perms.compilacao.change_dispositivo_de_vigencia_global %} + {% if not object.ta_publicado and not object.dispositivo_subsequente and not object.tipo_dispositivo.dispositivo_de_alteracao%} +
+ +
+ {% endif %} + {% endif %} diff --git a/sapl/templates/compilacao/dispositivo_form.html b/sapl/templates/compilacao/dispositivo_form.html index d9882b53a..3de5b5b40 100644 --- a/sapl/templates/compilacao/dispositivo_form.html +++ b/sapl/templates/compilacao/dispositivo_form.html @@ -10,10 +10,10 @@ {% url 'sapl.compilacao:dispositivo_edit_vigencia' object.ta_id object.pk as edit_vigencia_url %} {% url 'sapl.compilacao:dispositivo_edit_alteracao' object.ta_id object.pk as edit_alteracao_url %} {% url 'sapl.compilacao:dispositivo_edit_definidor_vigencia' object.ta_id object.pk as edit_definidor_vigencia_url %} -
  • {% trans 'Dados Básicos' %}
  • -
  • {% trans 'Vigência' %}
  • -
  • {% trans 'Definidor de Vigência' %}
  • -
  • {% trans 'Alteração' %}
  • + {% if perms.compilacao.change_dispositivo_edicao_avancada %}
  • {% trans 'Dados Básicos' %}
  • {% endif %} + {% if perms.compilacao.change_dispositivo_edicao_avancada %}
  • {% trans 'Vigência' %}
  • {% endif %} + {% if perms.compilacao.change_dispositivo_de_vigencia_global %}
  • {% trans 'Definidor de Vigência' %}
  • {% endif %} + {% if perms.compilacao.change_dispositivo_registros_compilacao %}
  • {% trans 'Alteração' %}
  • {% endif %} {% endblock sections_nav %}{% trans '' %} diff --git a/sapl/templates/compilacao/publicacao_detail.html b/sapl/templates/compilacao/publicacao_detail.html index 9bd8bc2b4..d157e49f5 100644 --- a/sapl/templates/compilacao/publicacao_detail.html +++ b/sapl/templates/compilacao/publicacao_detail.html @@ -8,7 +8,7 @@ {% block actions %}
    {% trans 'Editar' %} - {% trans 'Excluir' %} + {% trans 'Excluir' %}
    {% endblock actions %} diff --git a/sapl/templates/compilacao/publicacao_list.html b/sapl/templates/compilacao/publicacao_list.html index 3cdc0f785..975fbf1f4 100644 --- a/sapl/templates/compilacao/publicacao_list.html +++ b/sapl/templates/compilacao/publicacao_list.html @@ -5,11 +5,13 @@ {% block base_content %} -
    - - {% trans 'Adicionar'%} {%model_verbose_name 'sapl.compilacao.models.Publicacao'%} - -
    + {% if perms.compilacao.add_publicacao %} +
    + + {% trans 'Adicionar'%} {%model_verbose_name 'sapl.compilacao.models.Publicacao'%} + +
    + {% endif %} {% if not object_list %}

    {{ NO_ENTRIES_MSG }}

    diff --git a/sapl/templates/compilacao/text_edit.html b/sapl/templates/compilacao/text_edit.html index 013a7429d..899df7b20 100644 --- a/sapl/templates/compilacao/text_edit.html +++ b/sapl/templates/compilacao/text_edit.html @@ -11,18 +11,24 @@ {% endblock %} {% block title%} -

    {{object }}. {% trans 'Texto Multivigente em Edição' %}

    +

    {{object }}. {% trans 'Texto Multivigente em Edição' %}

    {% endblock %} {% block actions %} -
    -
    -
    -
    -
    - -

    {{ object.ementa|safe}}

    -
    +
    +
    +
    + +

    {{ object.ementa|safe}}

    +
    {% endblock detail_content %} {% endblock base_content %} diff --git a/sapl/templates/compilacao/textoarticulado_list.html b/sapl/templates/compilacao/textoarticulado_list.html index 645df8081..c6de147cd 100644 --- a/sapl/templates/compilacao/textoarticulado_list.html +++ b/sapl/templates/compilacao/textoarticulado_list.html @@ -8,12 +8,14 @@ {% endblock detail_content %} {% block actions %} -
    - - {% trans 'Adicionar'%} {%model_verbose_name 'sapl.compilacao.models.TextoArticulado'%} - - {% include 'compilacao/textoarticulado_menu_config.html' %} -
    + {% if perms.compilacao.add_textoarticulado %} +
    + + {% trans 'Adicionar'%} {%model_verbose_name 'sapl.compilacao.models.TextoArticulado'%} + + {% include 'compilacao/textoarticulado_menu_config.html' %} +
    + {% endif %} {% endblock actions %} diff --git a/sapl/templates/compilacao/textoarticulado_menu_config.html b/sapl/templates/compilacao/textoarticulado_menu_config.html index 332e1e61e..976f18148 100644 --- a/sapl/templates/compilacao/textoarticulado_menu_config.html +++ b/sapl/templates/compilacao/textoarticulado_menu_config.html @@ -4,11 +4,15 @@ diff --git a/sapl/templates/compilacao/tipotextoarticulado_detail.html b/sapl/templates/compilacao/tipotextoarticulado_detail.html index 177c2e06a..afc0a0a85 100644 --- a/sapl/templates/compilacao/tipotextoarticulado_detail.html +++ b/sapl/templates/compilacao/tipotextoarticulado_detail.html @@ -9,8 +9,8 @@
    {% block actions %}
    - {% trans 'Editar' %} - {% trans 'Excluir' %} + {% if perms.compilacao.change_tipotextoarticulado %}{% trans 'Editar' %}{% endif %} + {% if perms.compilacao.delete_tipotextoarticulado %}{% trans 'Excluir' %}{% endif %}
    {% endblock actions %}
    @@ -19,33 +19,43 @@
    {%trans 'Identificação Básica'%}
    -
    +

    {{ object.sigla}}

    -
    +

    {{ object.descricao}}

    -
    +

    {{ object.content_type|default:""}}

    -
    -
    - -

    {{ object.get_participacao_social_display}}

    -
    +
    +
    + {%trans 'Funcionalidades'%} +
    +
    +
    + +

    {{ object.get_participacao_social_display}}

    +
    +
    +
    +
    + +

    {{ object.get_publicacao_func_display}}

    +
    {% endblock detail_content %} {% endblock base_content %} diff --git a/sapl/templates/compilacao/tipotextoarticulado_list.html b/sapl/templates/compilacao/tipotextoarticulado_list.html index 28e4d7886..e89e8afd4 100644 --- a/sapl/templates/compilacao/tipotextoarticulado_list.html +++ b/sapl/templates/compilacao/tipotextoarticulado_list.html @@ -5,11 +5,13 @@ {% block base_content %} {% block actions %} - + {% if perms.compilacao.add_tipotextoarticulado %} + + {% endif %} {% endblock actions %} {% if not object_list %} diff --git a/sapl/templates/crud/detail.html b/sapl/templates/crud/detail.html index 8d824b0f2..f425ed791 100644 --- a/sapl/templates/crud/detail.html +++ b/sapl/templates/crud/detail.html @@ -21,6 +21,15 @@ {% endif %} + {% if view.extras_url %} +
    + {% for url, css_class, text in view.extras_url %} + + {{text}} + + {% endfor %} +
    + {% endif %} {% endblock sub_actions %}
    @@ -41,30 +50,32 @@ {% endblock actions %}
    - {% block detail_content %} - {% for fieldset in view.layout_display %} -

    {{ fieldset.legend }}

    - {% for row in fieldset.rows %} -
    - {% for column in row %} -
    -
    -

    {{ column.verbose_name }}

    -
    - {% comment %}TODO Transformar os links em URLs diretamente no CRUD{% endcomment %} - {% if column.text|url %} - - {% else %} -
    {{ column.text|safe }}
    - {% endif %} +
    + {% block detail_content %} + {% for fieldset in view.layout_display %} +

    {{ fieldset.legend }}

    + {% for row in fieldset.rows %} +
    + {% for column in row %} +
    +
    +

    {{ column.verbose_name }}

    +
    + {% comment %}TODO Transformar os links em URLs diretamente no CRUD{% endcomment %} + {% if column.text|url %} + + {% else %} +
    {{ column.text|safe }}
    + {% endif %} +
    -
    - {% endfor %} -
    + {% endfor %} +
    + {% endfor %} {% endfor %} - {% endfor %} - {% endblock detail_content %} + {% endblock detail_content %} +
    {% block table_content %}
    diff --git a/sapl/templates/crud/detail_detail.html b/sapl/templates/crud/detail_detail.html index 219e287df..cb0b103ec 100644 --- a/sapl/templates/crud/detail_detail.html +++ b/sapl/templates/crud/detail_detail.html @@ -18,6 +18,17 @@ {% endif %}
    + {% if view.extras_url %} +
    + {% for url, css_class, text in view.extras_url %} + + {{text}} + + {% endfor %} +
    + {% endif %} + + {% if view.update_url or view.delete_url %}
    {% if view.update_url %} diff --git a/sapl/templates/materia/confirmar_proposicao.html b/sapl/templates/materia/confirmar_proposicao.html index e50c92649..0c66c1f0a 100644 --- a/sapl/templates/materia/confirmar_proposicao.html +++ b/sapl/templates/materia/confirmar_proposicao.html @@ -7,7 +7,9 @@ {% block actions %}{{block.super}}
    {% if object.texto_articulado.exists %} - {% trans "Texto Eletrônico da Proposição" %} + + + {% trans "Texto Eletrônico da Proposição" %} {% endif %} {% if object.texto_original %} {% trans "Texto Original da Proposição" %} diff --git a/sapl/templates/materia/proposicao_detail.html b/sapl/templates/materia/proposicao_detail.html index dd6f68046..2a0f6e6de 100644 --- a/sapl/templates/materia/proposicao_detail.html +++ b/sapl/templates/materia/proposicao_detail.html @@ -123,5 +123,4 @@
    {% endif %} - {% endblock detail_content %} diff --git a/sapl/templates/materia/tipoproposicao_form.html b/sapl/templates/materia/tipoproposicao_form.html index a19814e4e..ede423c40 100644 --- a/sapl/templates/materia/tipoproposicao_form.html +++ b/sapl/templates/materia/tipoproposicao_form.html @@ -22,13 +22,10 @@ $(document).ready(function(){ initial_select=''; radios.append(html_radio); }); - - - }); }); - $('#id_content_type').trigger('change'); $("#div_id_tipo_conteudo_related_radio .controls").addClass('controls-radio-checkbox'); + $('#id_content_type').trigger('change'); }); diff --git a/sapl/templates/navbar.yaml b/sapl/templates/navbar.yaml index 4b053ca55..391f55664 100644 --- a/sapl/templates/navbar.yaml +++ b/sapl/templates/navbar.yaml @@ -13,7 +13,7 @@ url: sapl.parlamentares:parlamentar_list - title: {% trans 'Protocolo' %} - check_permission: protolcoloadm.list_protocolo + check_permission: protocoloadm.list_protocolo children: - title: {% trans 'Pesquisar Protocolo' %} url: sapl.protocoloadm:protocolo diff --git a/sapl/templates/norma/layouts.yaml b/sapl/templates/norma/layouts.yaml index e7eff947e..99e14640c 100644 --- a/sapl/templates/norma/layouts.yaml +++ b/sapl/templates/norma/layouts.yaml @@ -1,4 +1,9 @@ {% load i18n %} + +AssuntoNormaRelationship: + {% trans 'Assunto Norma Jurídica' %}: + - assunto + AssuntoNorma: {% trans 'Assunto Norma Jurídica' %}: - assunto descricao @@ -17,6 +22,7 @@ NormaJuridica: - ementa - indexacao - observacao + - assuntos NormaJuridicaCreate: {% trans 'Identificação Básica' %}: @@ -28,6 +34,7 @@ NormaJuridicaCreate: - ementa - indexacao - observacao + - assuntos LegislacaoCitada: {% trans 'Legislação Citada' %}: diff --git a/sapl/templates/norma/list_pesquisa.html b/sapl/templates/norma/list_pesquisa.html index c16cea1b6..8fb65745a 100644 --- a/sapl/templates/norma/list_pesquisa.html +++ b/sapl/templates/norma/list_pesquisa.html @@ -32,7 +32,6 @@ {% endfor %}
    - {% include "paginacao.html" %} {% else %}

    Nenhum Registro recuperado

    {% endif %} diff --git a/sapl/templates/norma/normajuridica_form.html b/sapl/templates/norma/normajuridica_form.html new file mode 100644 index 000000000..7a35b05a7 --- /dev/null +++ b/sapl/templates/norma/normajuridica_form.html @@ -0,0 +1,29 @@ +{% extends "crud/form.html" %} +{% load i18n %} +{% load crispy_forms_tags %} +{% load common_tags %} + +{% block extra_js %} + + + +{% endblock %} diff --git a/sapl/templates/norma/subnav.yaml b/sapl/templates/norma/subnav.yaml index d35607335..be3cf3c0a 100644 --- a/sapl/templates/norma/subnav.yaml +++ b/sapl/templates/norma/subnav.yaml @@ -1,4 +1,5 @@ {% load i18n common_tags%} + - title: {% trans 'Início' %} url: normajuridica_detail @@ -6,6 +7,7 @@ # para integração foram necessárias apenas criar a url norma_ta em urls.py # e a view NormaTaView(IntegracaoTaView) em views.py # Em nada mais a integração interfere em NormaJuridica + {% if 'texto_articulado_norma'|get_config_attr %} - title: {% trans 'Texto' %} url: norma_ta diff --git a/sapl/templates/painel/controlador.html b/sapl/templates/painel/controlador.html index f4aaabc00..32ed27913 100644 --- a/sapl/templates/painel/controlador.html +++ b/sapl/templates/painel/controlador.html @@ -21,4 +21,8 @@ FECHADO +
    +
    +Voltar + {% endblock %} diff --git a/sapl/templates/protocoloadm/MateriaTemplate.html b/sapl/templates/protocoloadm/MateriaTemplate.html new file mode 100644 index 000000000..fc490fcc5 --- /dev/null +++ b/sapl/templates/protocoloadm/MateriaTemplate.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% load i18n common_tags%} + +{% block base_content %} + + +
    + + + +
    + +
    + Continuar +
    +
    +
    +{% endblock base_content %} diff --git a/sapl/templates/protocoloadm/anular_protocoloadm.html b/sapl/templates/protocoloadm/anular_protocoloadm.html index 97e772690..b936ed7ad 100644 --- a/sapl/templates/protocoloadm/anular_protocoloadm.html +++ b/sapl/templates/protocoloadm/anular_protocoloadm.html @@ -1,7 +1,17 @@ {% extends "protocoloadm/protocoloadm_detail.html" %} {% load i18n %} {% load crispy_forms_tags %} + + +{% block actions %} + + {{ block.super }} + +{% endblock %} + {% block detail_content %}
    {{ message }}
    {% crispy form %} -{% endblock detail_content %} \ No newline at end of file +{% endblock detail_content %} diff --git a/sapl/templates/protocoloadm/comprovante.html b/sapl/templates/protocoloadm/comprovante.html index de26eaf78..9fd9b8b32 100644 --- a/sapl/templates/protocoloadm/comprovante.html +++ b/sapl/templates/protocoloadm/comprovante.html @@ -83,9 +83,5 @@ Número Páginas {{ protocolo.numero_paginas }} - - Número Páginas - {{ protocolo.numero_paginas }} - {% endblock detail_content %} diff --git a/sapl/templates/protocoloadm/protocolar_documento.html b/sapl/templates/protocoloadm/protocolar_documento.html index 87259a900..9f2213128 100644 --- a/sapl/templates/protocoloadm/protocolar_documento.html +++ b/sapl/templates/protocoloadm/protocolar_documento.html @@ -2,6 +2,15 @@ {% load i18n %} {% load crispy_forms_tags %} + +{% block actions %} + + {{ block.super }} + +{% endblock %} + {% block detail_content %} {% crispy form %} -{% endblock detail_content %} \ No newline at end of file +{% endblock detail_content %} diff --git a/sapl/templates/protocoloadm/protocolar_materia.html b/sapl/templates/protocoloadm/protocolar_materia.html index 87259a900..744126f0d 100644 --- a/sapl/templates/protocoloadm/protocolar_materia.html +++ b/sapl/templates/protocoloadm/protocolar_materia.html @@ -2,6 +2,14 @@ {% load i18n %} {% load crispy_forms_tags %} +{% block actions %} + + {{ block.super }} + +{% endblock %} + {% block detail_content %} {% crispy form %} -{% endblock detail_content %} \ No newline at end of file +{% endblock detail_content %} diff --git a/sapl/templates/protocoloadm/protocolo_filter.html b/sapl/templates/protocoloadm/protocolo_filter.html index 19125ad95..a18ea7fb9 100644 --- a/sapl/templates/protocoloadm/protocolo_filter.html +++ b/sapl/templates/protocoloadm/protocolo_filter.html @@ -3,20 +3,25 @@ {% load crispy_forms_tags %} {% load static %} -{% block sections_nav %} {% endblock %} -{% block base_content %} -

    Pesquisa de Protocolo

    -

    - {% if filter_url %} - - {% endif %} + {% block actions %} - {% if not filter_url %} - {% crispy filter.form %} - {% endif %} + {{ block.super }} + + {% if filter_url %} + + {% endif %} + + + {% endblock %} + + {% block detail_content %} +
    + {% if not filter_url %} + {% crispy filter.form %} + {% endif %} {% if filter_url %} @@ -33,7 +38,7 @@
    Protocolo: - {{ p.numero|stringformat:'06d' }}/{{ p.ano }}  -   + {{ p.numero|stringformat:'06d' }}/{{ p.ano }}  -   Etiqueta Individual
    Assunto: {{ p.assunto_ementa|default_if_none:"Não Informado"}}
    Data Protocolo: {{ p.data|date:"d/m/Y"|default_if_none:"Não Informado" }} - Horário: {{ p.hora|date:"G:i:s" }}
    @@ -50,4 +55,7 @@
    {% include "paginacao.html" %} {% endif %} -{% endblock base_content %} +{% endblock detail_content %} + +{% block table_content %} +{% endblock table_content %} diff --git a/sapl/templates/protocoloadm/protocolo_mostrar.html b/sapl/templates/protocoloadm/protocolo_mostrar.html index 58882a39e..df35a92e6 100644 --- a/sapl/templates/protocoloadm/protocolo_mostrar.html +++ b/sapl/templates/protocoloadm/protocolo_mostrar.html @@ -15,9 +15,28 @@
    - Documento Vinculado:
    - Criar Documento + Documento Vinculado: + + {% if protocolo.tipo_documento %} + {% if documento %} + {{documento}} +
    + {% else %} +
    + Criar Documento + {% endif %} + {% elif protocolo.tipo_materia %} + {% if materia %} + {{materia}} +
    + {% else %} +
    + Criar Matéria + {% endif %} + {% endif %} + +      - Comprovante + Comprovante -{% endblock detail_content %} \ No newline at end of file +{% endblock detail_content %} diff --git a/sapl/templates/protocoloadm/protocoloadm_detail.html b/sapl/templates/protocoloadm/protocoloadm_detail.html index 07ae81c47..dc91ac252 100644 --- a/sapl/templates/protocoloadm/protocoloadm_detail.html +++ b/sapl/templates/protocoloadm/protocoloadm_detail.html @@ -1,9 +1,9 @@ {% extends "crud/detail.html" %} {% load i18n %} -{% block actions %} - -{% endblock actions %} \ No newline at end of file +{% block editions %} + +{% endblock editions %} diff --git a/sapl/templates/sessao/painel.html b/sapl/templates/sessao/painel.html index 48e422423..febf0a667 100644 --- a/sapl/templates/sessao/painel.html +++ b/sapl/templates/sessao/painel.html @@ -18,7 +18,7 @@ - +

    Operação do Painel Eletrônico

    diff --git a/sapl/test_urls.py b/sapl/test_urls.py index 51c6f5e2e..c38355126 100644 --- a/sapl/test_urls.py +++ b/sapl/test_urls.py @@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat from sapl.crud.base import PermissionRequiredForAppCrudMixin -from scripts.inicializa_grupos_autorizacoes import cria_grupos_permissoes +from sapl.rules.apps import AppConfig, update_groups from scripts.lista_urls import lista_urls from .settings import SAPL_APPS @@ -376,7 +376,7 @@ def test_permissions_urls_for_users_by_apps(url_item, client): # list e detail permissions create_perms_post_migrate(app) # cria usuários de perfil do sapl - cria_grupos_permissoes() + update_groups(AppConfig) users = get_user_model().objects.order_by( 'username').values_list('username', flat=True) diff --git a/sapl/urls.py b/sapl/urls.py index 649265aaa..ee512b31f 100644 --- a/sapl/urls.py +++ b/sapl/urls.py @@ -17,7 +17,7 @@ from django.conf import settings from django.conf.urls import include, url from django.conf.urls.static import static from django.contrib import admin -from django.views.generic.base import TemplateView +from django.views.generic.base import TemplateView, RedirectView from django.views.static import serve as view_static_server import sapl.api.urls @@ -33,6 +33,7 @@ import sapl.protocoloadm.urls import sapl.relatorios.urls import sapl.sessao.urls + urlpatterns = [ url(r'^$', TemplateView.as_view(template_name='index.html')), url(r'^admin/', include(admin.site.urls)), @@ -53,6 +54,9 @@ urlpatterns = [ url(r'', include(sapl.base.urls)), url(r'', include(sapl.api.urls)), + + url(r'^favicon\.ico$', RedirectView.as_view( + url='/static/img/favicon.ico', permanent=True)), ] diff --git a/sapl/utils.py b/sapl/utils.py index 573dffb46..e61d68f85 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -1,13 +1,11 @@ import hashlib import logging +import re from datetime import date from functools import wraps from unicodedata import normalize as unicodedata_normalize -import hashlib -import logging -import re - +import django_filters import magic from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button @@ -15,17 +13,12 @@ from django import forms from django.apps import apps from django.conf import settings from django.contrib import admin -from django.contrib.auth.decorators import user_passes_test -from django.contrib.auth.models import Permission -from django.contrib.contenttypes.fields import GenericRelation, GenericRel,\ - GenericForeignKey -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import PermissionDenied, ValidationError +from django.contrib.contenttypes.fields import (GenericForeignKey, GenericRel, + GenericRelation) +from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from floppyforms import ClearableFileInput -import magic - from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row from sapl.settings import BASE_DIR @@ -103,7 +96,12 @@ def montar_helper_autor(self): class SaplGenericForeignKey(GenericForeignKey): - def __init__(self, ct_field='content_type', fk_field='object_id', for_concrete_model=True, verbose_name=''): + def __init__( + self, + ct_field='content_type', + fk_field='object_id', + for_concrete_model=True, + verbose_name=''): super().__init__(ct_field, fk_field, for_concrete_model) self.verbose_name = verbose_name @@ -383,7 +381,7 @@ def intervalos_tem_intersecao(a_inicio, a_fim, b_inicio, b_fim): menor_fim = min(a_fim, b_fim) return maior_inicio <= menor_fim - +""" def permissoes(nome_grupo, app_label): lista_permissoes = [] try: @@ -398,12 +396,12 @@ def permissoes(nome_grupo, app_label): def permission_required_for_app(app_label, login_url=None, raise_exception=False): - """ + Decorator for views that checks whether a user has a particular permission enabled, redirecting to the log-in page if necessary. If the raise_exception parameter is given the PermissionDenied exception is raised. - """ + def check_perms(user): if user.has_module_perms(app_label): return True @@ -414,7 +412,6 @@ def permission_required_for_app(app_label, login_url=None, return False return user_passes_test(check_perms, login_url=login_url) - def permissoes_materia(): return permissoes('Operador de Matéria', 'materia') @@ -467,6 +464,54 @@ def permissao_tb_aux(self): else: return False +""" + + +class MateriaPesquisaOrderingFilter(django_filters.OrderingFilter): + + choices = ( + ('', 'Selecione'), + ('dataC', 'Data, Tipo, Ano, Numero - Ordem Crescente'), + ('dataD', 'Data, Tipo, Ano, Numero - Ordem Decrescente'), + ('tipoC', 'Tipo, Ano, Numero, Data - Ordem Crescente'), + ('tipoD', 'Tipo, Ano, Numero, Data - Ordem Decrescente') + ) + order_by_mapping = { + '': [], + 'dataC': ['data_apresentacao', 'tipo__sigla', 'ano', 'numero'], + 'dataD': ['-data_apresentacao', '-tipo__sigla', '-ano', '-numero'], + 'tipoC': ['tipo__sigla', 'ano', 'numero', 'data_apresentacao'], + 'tipoD': ['-tipo__sigla', '-ano', '-numero', '-data_apresentacao'], + } + + def __init__(self, *args, **kwargs): + kwargs['choices'] = self.choices + super(MateriaPesquisaOrderingFilter, self).__init__(*args, **kwargs) + + def filter(self, qs, value): + _value = self.order_by_mapping[value[0]] if value else value + return super().filter(qs, _value) + + +class AnoNumeroOrderingFilter(django_filters.OrderingFilter): + + choices = (('', 'Selecione...'), + ('CRE', 'Ordem Crescente'), + ('DEC', 'Ordem Decrescente'),) + order_by_mapping = { + '': [], + 'CRE': ['ano', 'numero'], + 'DEC': ['-ano', '-numero'], + } + + def __init__(self, *args, **kwargs): + kwargs['choices'] = self.choices + super(AnoNumeroOrderingFilter, self).__init__(*args, **kwargs) + + def filter(self, qs, value): + _value = self.order_by_mapping[value[0]] if value else value + return super().filter(qs, _value) + def gerar_hash_arquivo(arquivo, pk, block_size=2**20): md5 = hashlib.md5() diff --git a/scripts/inicializa_grupos_autorizacoes.py b/scripts/inicializa_grupos_autorizacoes.py deleted file mode 100644 index 38606b0a3..000000000 --- a/scripts/inicializa_grupos_autorizacoes.py +++ /dev/null @@ -1,135 +0,0 @@ -import os - -import django - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sapl.settings") -django.setup() - -if True: - from django.apps import apps - from django.contrib.auth import get_user_model - from django.contrib.auth.models import Group, Permission - from django.contrib.contenttypes.models import ContentType - - -class InicializaGruposAutorizacoes(): - - def cria_ou_reseta_grupo(self, nome): - grupo = Group.objects.get_or_create(name=nome)[0] - for p in list(grupo.permissions.all()): - grupo.permissions.remove(p) - return grupo - - def cria_usuario(self, nome, grupo): - nome_usuario = nome - usuario = get_user_model().objects.get_or_create( - username=nome_usuario)[0] - usuario.set_password('interlegis') - usuario.save() - grupo.user_set.add(usuario) - - def cria_grupos_permissoes(self): - - nomes_apps = ['base', 'parlamentares', 'comissoes', - 'materia', 'norma', 'sessao', 'painel'] - - permissoes = {app: list(Permission.objects.filter( - content_type__in=ContentType.objects.filter(app_label=app))) - for app in nomes_apps} - - # permissoes específicas para protocolo e documento administrativo - cts = ContentType.objects.filter(app_label='protocoloadm') - - # documento administrativo - permissoes['documento_administrativo'] = list( - Permission.objects.filter(content_type__in=cts)) - nome_grupo = 'Operador Administrativo' - grupo = self.cria_ou_reseta_grupo(nome_grupo) - for p in permissoes['documento_administrativo']: - grupo.permissions.add(p) - - nome_usuario = 'operador_administrativo' - self.cria_usuario(nome_usuario, grupo) - - # prolocolo administrativo - cts = cts.exclude(model__icontains='tramitacao').exclude( - model__icontains='documentoadministrativo') - permissoes['protocoloadm'] = list( - Permission.objects.filter(content_type__in=cts)) - nome_grupo = 'Operador de Protocolo Administrativo' - grupo = self.cria_ou_reseta_grupo(nome_grupo) - for p in permissoes['protocoloadm']: - grupo.permissions.add(p) - - nome_usuario = 'operador_protocoloadm' - self.cria_usuario(nome_usuario, grupo) - - # permissoes do base - cts = ContentType.objects.filter(app_label='base') - permissoes['base'] = list( - Permission.objects.filter(content_type__in=cts)) - - for nome_app in nomes_apps: - - if nome_app not in {'base', 'parlamentares'}: - # Elimina casos especificos - - # Cria Grupo - nome_grupo = 'Operador de %s' % apps.get_app_config( - nome_app).verbose_name - grupo = self.cria_ou_reseta_grupo(nome_grupo) - - # Elimina o acesso a proposicoes pelo Operador de Matérias - if nome_app == 'materia': - cts = ContentType.objects.filter( - app_label='materia').exclude(model='proposicao') - permissoes['materia'] = list( - Permission.objects.filter(content_type__in=cts)) - - # Configura as permissoes - for p in permissoes[nome_app]: - grupo.permissions.add(p) - - # Cria o Usuario - nome_usuario = 'operador_%s' % nome_app - usuario = get_user_model().objects.get_or_create( - username=nome_usuario)[0] - usuario.set_password('interlegis') - usuario.save() - grupo.user_set.add(usuario) - - # Operador Geral - grupo_geral = self.cria_ou_reseta_grupo('Operador Geral') - for lista in permissoes.values(): - for p in lista: - grupo_geral.permissions.add(p) - - nome_usuario = 'operador_geral' - self.cria_usuario(nome_usuario, grupo_geral) - - # Autor - grupo = self.cria_ou_reseta_grupo('Autor') - - list(map(lambda permissao: grupo.permissions.add(permissao), - list(Permission.objects.filter( - content_type=ContentType.objects.get_by_natural_key( - app_label='materia', model='proposicao'))))) - - """ - Mesmo para teste, um usuário com perfil Autor criado via /admin - não deverá ser criado pois esse é um papel do operador_geral fazer - nas tabelas auxiliares. - A tentativa de acesso a qualquer container (hoje apenas proposições) - do SAPL de Usuários com perfil Autor mas sem um Autor cadastrado - nas tabelas auxiliares será negado e notificado via front-end. - """ - # nome_usuario = 'operador_autor' - # self.cria_usuario(nome_usuario, grupo) - - def __call__(self): - self.cria_grupos_permissoes() - - -cria_grupos_permissoes = InicializaGruposAutorizacoes() -if __name__ == '__main__': - cria_grupos_permissoes.cria_grupos_permissoes() diff --git a/scripts/lista_permissions_in_decorators.py b/scripts/lista_permissions_in_decorators.py new file mode 100644 index 000000000..4d504e6c0 --- /dev/null +++ b/scripts/lista_permissions_in_decorators.py @@ -0,0 +1,141 @@ +import ast +import inspect +import os + +if __name__ == '__main__': + + import django + + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sapl.settings") + django.setup() + +if True: + from scripts.lista_urls import lista_urls + + +def get_decorators(cls): + target = cls + decorators = {} + + def visit_FunctionDef(node): + decorators[node.name] = [] + for n in node.decorator_list: + name = '' + if isinstance(n, ast.Call): + name = n.func.attr if isinstance( + n.func, ast.Attribute) else n.func.id + else: + name = n.attr if isinstance(n, ast.Attribute) else n.id + + decorators[node.name].append(name) + + node_iter = ast.NodeVisitor() + node_iter.visit_FunctionDef = visit_FunctionDef + node_iter.visit(ast.parse(inspect.getsource(target))) + return decorators + + +def get_permission_requireds(cls): + target = cls + decorators = [] + + def get_permission_required(arg): + + for perm in arg.args: + + if isinstance(perm, ast.Str): + decorators.append(getattr(perm, perm._fields[0])) + continue + + if isinstance(perm, (ast.Tuple, ast.List)): + if 'elts' not in perm._fields: + continue + + for elt in perm.elts: + + if isinstance(elt, ast.Str): + decorators.append(getattr(elt, elt._fields[0])) + + def get_method_decorator(n): + for arg in n.args: + + if not isinstance(arg, ast.Call): + continue + + """ + Espera-se que: + - o decorator seja uma função + - esta função tenha o meta atributo 'id' + - id = 'permission_required' + - esta função tenha argumento args + """ + if ('func' not in arg._fields or + 'id' not in arg.func._fields or + arg.func.id != 'permission_required' or + 'args' not in arg._fields): + continue + + get_permission_required(arg) + + def visit_FunctionDef(node): + for n in node.decorator_list: + if not isinstance(n, ast.Call): + continue + + """ + Espera-se que: + - o decorator seja uma função + - esta função tenha o meta atributo 'id' + - id = 'method_decorator' + - esta função tenha argumento args + """ + if ('func' not in n._fields or + 'id' not in n.func._fields or + n.func.id != 'method_decorator' or + 'args' not in n._fields): + get_permission_required(n) + else: + get_method_decorator(n) + + node_iter = ast.NodeVisitor() + node_iter.visit_FunctionDef = visit_FunctionDef + node_iter.visit(ast.parse(inspect.getsource(target))) + return decorators + + +class ListaPermissionInDecorators(): + decorators = [] + + def lista_permissions_in_decorators(self): + urls = lista_urls() + + for url_item in urls: + key, url, var, app_name = url_item + if hasattr(key, 'view_class'): + view = key.view_class + elif hasattr(key, 'cls'): + view = key.cls + else: + view = key + + if not view.__module__.startswith('sapl.'): + continue + + try: + decorators = list(map(lambda x: (x, view), + get_permission_requireds(view) + )) + self.decorators += decorators + except: + pass + return self.decorators + + def __call__(self): + return self.lista_permissions_in_decorators() + + +lista_permissions_in_decorators = ListaPermissionInDecorators() + +if __name__ == '__main__': + _lista_permissions_in_decorators = lista_permissions_in_decorators() + print(_lista_permissions_in_decorators) diff --git a/scripts/test_inicializa_grupos_autorizacoes.py b/scripts/test_inicializa_grupos_autorizacoes.py deleted file mode 100644 index d9f7d90a2..000000000 --- a/scripts/test_inicializa_grupos_autorizacoes.py +++ /dev/null @@ -1,116 +0,0 @@ -import pytest -from django.apps import apps -from django.contrib.auth.management import _get_all_permissions -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ -from django.utils.translation import string_concat - -from inicializa_grupos_autorizacoes import cria_grupos_permissoes - -pytestmark = pytest.mark.django_db - -apps_com_permissao_padrao = [ - 'comissoes', 'norma', 'sessao', 'painel'] - - -def create_perms_post_migrate(app): - - searched_perms = list() - # The codenames and ctypes that should exist. - ctypes = set() - - for klass in list(app.get_models()): - opts = klass._meta - permissions = ( - ("list_" + opts.model_name, - string_concat( - _('Visualizaçao da lista de'), ' ', - opts.verbose_name_plural)), - ("detail_" + opts.model_name, - string_concat( - _('Visualização dos detalhes de'), ' ', - opts.verbose_name_plural)), - ) - opts.permissions = tuple( - set(list(permissions) + list(opts.permissions))) - - if opts.proxy: - # Force looking up the content types in the current database - # before creating foreign keys to them. - app_label, model = opts.app_label, opts.model_name - - try: - ctype = ContentType.objects.get_by_natural_key( - app_label, model) - except: - ctype = ContentType.objects.create( - app_label=app_label, model=model) - else: - ctype = ContentType.objects.get_for_model(klass) - - ctypes.add(ctype) - for perm in _get_all_permissions(klass._meta, ctype): - searched_perms.append((ctype, perm)) - - all_perms = set(Permission.objects.filter( - content_type__in=ctypes, - ).values_list( - "content_type", "codename" - )) - - perms = [ - Permission(codename=codename, name=name, content_type=ct) - for ct, (codename, name) in searched_perms - if (ct.pk, codename) not in all_perms - ] - Permission.objects.bulk_create(perms) - - -@pytest.mark.parametrize('app_label', apps_com_permissao_padrao) -def test_grupo_padrao_tem_permissoes_sobre_todo_o_app(app_label): - - app = apps.get_app_config(app_label) - - create_perms_post_migrate(app) - - # código testado - cria_grupos_permissoes() - - def gerar_permissoes(app): - for model in app.get_models(): - for op in ['add', 'change', 'delete', ]: - yield model, 'Can %s %s' % (op, model._meta.verbose_name) - yield model, force_text(string_concat( - _('Visualizaçao da lista de'), ' ', - model._meta.verbose_name_plural)) - yield model, force_text(string_concat( - _('Visualização dos detalhes de'), - ' ', - model._meta.verbose_name_plural)) - grupo = Group.objects.get(name='Operador de %s' % app.verbose_name) - esperado = set(gerar_permissoes(app)) - - real = set((p.content_type.model_class(), p.name) - for p in grupo.permissions.all()) - assert real == esperado - - -@pytest.mark.parametrize('app_label', apps_com_permissao_padrao) -def test_permissoes_extras_sao_apagadas(app_label): - - app = apps.get_app_config(app_label) - - # create_perms_post_migrate(app) - - grupo = Group.objects.create(name='Operador de %s' % app.verbose_name) - - permissao_errada = Permission.objects.create( - name='STUB', content_type=ContentType.objects.first()) - grupo.permissions.add(permissao_errada) - - # código testado - cria_grupos_permissoes() - - assert not grupo.permissions.filter(id=permissao_errada.id).exists()