From 023dbd4716d404027b7294af94c9671ec5de5100 Mon Sep 17 00:00:00 2001 From: LeandroRoberto Date: Mon, 7 Nov 2016 19:32:32 -0200 Subject: [PATCH 1/2] Fix #774 e #747 --- .../migrations/0064_auto_20161104_1420.py | 46 ++++++ .../migrations/0065_auto_20161107_1024.py | 20 +++ .../migrations/0066_auto_20161107_1028.py | 20 +++ .../migrations/0067_auto_20161107_1351.py | 19 +++ .../migrations/0068_auto_20161107_1546.py | 19 +++ .../migrations/0069_auto_20161107_1932.py | 20 +++ sapl/compilacao/models.py | 147 +++++++++++++++++- .../templatetags/compilacao_filters.py | 4 + sapl/compilacao/views.py | 98 ++++++++++-- sapl/materia/forms.py | 35 ++++- sapl/materia/views.py | 86 +++++++--- sapl/rules/map_rules.py | 15 +- .../compilacao/publicacao_detail.html | 2 +- sapl/templates/compilacao/text_edit.html | 6 +- sapl/templates/compilacao/text_list.html | 8 + .../compilacao/textoarticulado_detail.html | 23 +-- .../tipotextoarticulado_detail.html | 29 ++-- 17 files changed, 529 insertions(+), 68 deletions(-) create mode 100644 sapl/compilacao/migrations/0064_auto_20161104_1420.py create mode 100644 sapl/compilacao/migrations/0065_auto_20161107_1024.py create mode 100644 sapl/compilacao/migrations/0066_auto_20161107_1028.py create mode 100644 sapl/compilacao/migrations/0067_auto_20161107_1351.py create mode 100644 sapl/compilacao/migrations/0068_auto_20161107_1546.py create mode 100644 sapl/compilacao/migrations/0069_auto_20161107_1932.py 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 189dea8f2..1531e6542 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 _ @@ -82,7 +84,7 @@ class TipoTextoArticulado(models.Model): verbose_name=_('Participação Social')) publicacao_func = models.NullBooleanField( - default=True, + default=False, blank=True, null=True, choices=YES_NO_CHOICES, verbose_name=_('Histórico de Publicação')) @@ -101,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')) @@ -124,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: @@ -138,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_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) @@ -703,6 +845,9 @@ class Dispositivo(BaseModel, TimestampedMixin): ('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.')), diff --git a/sapl/compilacao/templatetags/compilacao_filters.py b/sapl/compilacao/templatetags/compilacao_filters.py index aaeb712b0..f3965c43d 100644 --- a/sapl/compilacao/templatetags/compilacao_filters.py +++ b/sapl/compilacao/templatetags/compilacao_filters.py @@ -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 5f61b2a73..e266f0358 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -7,7 +7,6 @@ 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, permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.contenttypes.models import ContentType from django.core.signing import Signer @@ -19,7 +18,6 @@ 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 string_concat from django.utils.translation import ugettext_lazy as _ @@ -44,7 +42,8 @@ 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 @@ -157,6 +156,7 @@ class IntegracaoTaView(TemplateView): """) 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) @@ -168,18 +168,30 @@ 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 + + ta.privacidade = ta_values.get('privacidade', STATUS_TA_EDITION) + + ta.editing_locked = ta_values.get('editing_locked', False) + ta.editable_only_by_owners = ta_values.get( + 'editable_only_by_owners', False) + else: ta = ta[0] - ta.data = getattr(item, map_fields['data'] - if map_fields['data'] else 'xxx', 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.ementa = getattr( item, map_fields['ementa'] @@ -202,11 +214,17 @@ class IntegracaoTaView(TemplateView): ta.save() - if Dispositivo.objects.filter(ta_id=ta.pk).exists(): - return redirect(to=reverse_lazy('sapl.compilacao:ta_text', + if not ta_exists: + if ta.editable_only_by_owners and\ + not self.request.user.is_anonymous(): + ta.owners.add(self.request.user) + + 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('sapl.compilacao:ta_text_edit', + return redirect(to=reverse_lazy('sapl.compilacao:ta_text', kwargs={'ta_id': ta.pk})) def import_pattern(self): @@ -284,7 +302,8 @@ class CompMixin(PermissionRequiredMixin): @property def ta(self): - ta = TextoArticulado.objects.get(pk=self.kwargs['ta_id']) + ta = TextoArticulado.objects.get( + pk=self.kwargs.get('ta_id', self.kwargs.get('pk', 0))) return ta def get_context_data(self, **kwargs): @@ -407,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: @@ -736,9 +771,9 @@ class TextView(CompMixin, ListView): fim_vigencia = None ta_vigencia = None - def get(self, request, *args, **kwargs): + def has_permission(self): self.object = self.ta - return super(TextView, self).get(request, *args, **kwargs) + return self.object.has_view_permission(self.request) def get_context_data(self, **kwargs): context = super(TextView, self).get_context_data(**kwargs) @@ -931,7 +966,42 @@ class DispositivoView(TextView): class TextEditView(CompMixin, TemplateView): template_name = 'compilacao/text_edit.html' - permission_required = 'compilacao.change_dispositivo_edicao_dinamica' + + 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']) \ @@ -2421,8 +2491,6 @@ class DispositivoDinamicEditView( template_name = 'compilacao/text_edit_bloco.html' model = Dispositivo form_class = DispositivoEdicaoBasicaForm - contador = -1 - permission_required = 'compilacao.change_dispositivo_edicao_dinamica', def get_initial(self): initial = UpdateView.get_initial(self) diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 02b7e3c19..3c44d4d1e 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1,8 +1,7 @@ -import os from datetime import date, datetime +import os -import django_filters from crispy_forms.bootstrap import (Alert, FormActions, InlineCheckboxes, InlineRadios) from crispy_forms.helper import FormHelper @@ -18,10 +17,12 @@ from django.db.models import Max from django.forms import ModelForm, widgets from django.forms.forms import Form from django.utils.translation import ugettext_lazy as _ +import django_filters -import sapl from sapl.base.models import Autor from sapl.comissoes.models import Comissao +from sapl.compilacao.models import STATUS_TA_PRIVATE,\ + STATUS_TA_IMMUTABLE_PUBLIC from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column, to_row) from sapl.materia.models import (MateriaLegislativa, RegimeTramitacao, @@ -35,6 +36,7 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, ChoiceWithoutValidationField, MateriaPesquisaOrderingFilter, RangeWidgetOverride, autor_label, autor_modal, models_with_gr_for_model) +import sapl from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, DocumentoAcessorio, MateriaLegislativa, Numeracao, @@ -1134,6 +1136,12 @@ class ConfirmarProposicaoForm(ProposicaoForm): self.instance.data_envio = None self.instance.save() + if self.instance.texto_articulado.exists(): + ta = self.instance.texto_articulado.first() + ta.privacidade = STATUS_TA_PRIVATE + ta.editing_locked = False + ta.save() + self.instance.results = { 'messages': { 'success': [_('Devolução efetuada com sucesso.'), ] @@ -1147,6 +1155,12 @@ class ConfirmarProposicaoForm(ProposicaoForm): self.instance.data_devolucao = None self.instance.data_recebimento = datetime.now() + if self.instance.texto_articulado.exists(): + ta = self.instance.texto_articulado.first() + ta.privacidade = STATUS_TA_IMMUTABLE_PUBLIC + ta.editing_locked = True + ta.save() + self.instance.save() """ @@ -1187,10 +1201,17 @@ class ConfirmarProposicaoForm(ProposicaoForm): materia.data_apresentacao = datetime.now() materia.em_tramitacao = True materia.regime_tramitacao = cd['regime_tramitacao'] - materia.texto_original = File( - proposicao.texto_original, - os.path.basename(proposicao.texto_original.path)) - materia.texto_articulo = proposicao.texto_articulado + + if proposicao.texto_original: + materia.texto_original = File( + proposicao.texto_original, + os.path.basename(proposicao.texto_original.path)) + + if proposicao.texto_articulado.exists(): + pass + # FIXME - gerar texto_articulado da materia com base na prop. + # materia.texto_articulo = proposicao.texto_articulado + materia.save() conteudo_gerado = materia diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 1c102e1fe..63d456a3e 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -1,6 +1,7 @@ from datetime import datetime from random import choice from string import ascii_letters, digits + from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML from django.contrib import messages @@ -11,7 +12,7 @@ from django.core.mail import send_mail from django.core.urlresolvers import reverse from django.http import JsonResponse from django.http.response import Http404, HttpResponseRedirect -from django.shortcuts import redirect +from django.shortcuts import redirect, get_object_or_404 from django.template import Context, loader from django.utils import formats from django.utils.translation import ugettext_lazy as _ @@ -21,6 +22,9 @@ from django.views.generic.edit import FormView from django_filters.views import FilterView from sapl.base.models import Autor, CasaLegislativa +from sapl.compilacao.models import STATUS_TA_PRIVATE, STATUS_TA_EDITION,\ + STATUS_TA_IMMUTABLE_RESTRICT + from sapl.compilacao.views import IntegracaoTaView from sapl.crispy_layout_mixin import SaplFormLayout, form_actions from sapl.crud.base import (ACTION_CREATE, ACTION_DELETE, ACTION_DETAIL, @@ -107,7 +111,11 @@ class MateriaTaView(IntegracaoTaView): 'ano': 'ano', } map_funcs = { - 'publicacao_func': False + 'publicacao_func': False, + } + ta_values = { + 'editable_only_by_owners': False, + 'editing_locked': False, } def get(self, request, *args, **kwargs): @@ -126,7 +134,7 @@ class ProposicaoTaView(IntegracaoTaView): model = Proposicao model_type_foreignkey = TipoProposicao map_fields = { - 'data': 'data_recebimento', + 'data': 'data_envio', 'ementa': 'descricao', 'observacao': None, 'numero': 'numero_proposicao', @@ -135,6 +143,11 @@ class ProposicaoTaView(IntegracaoTaView): map_funcs = { 'publicacao_func': False } + ta_values = { + 'editable_only_by_owners': True, + 'editing_locked': False, + 'privacidade': STATUS_TA_PRIVATE + } def get(self, request, *args, **kwargs): """ @@ -143,6 +156,13 @@ class ProposicaoTaView(IntegracaoTaView): de usuário. """ if sapl.base.models.AppConfig.attr('texto_articulado_proposicao'): + + proposicao = get_object_or_404(self.model, pk=kwargs['pk']) + + if not proposicao.data_envio and\ + request.user != proposicao.autor.user: + raise Http404() + return IntegracaoTaView.get(self, request, *args, **kwargs) else: return self.get_redirect_deactivated() @@ -313,10 +333,14 @@ class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView): data_envio__isnull=False, data_recebimento__isnull=True) for proposicao in proposicoes: - # FIXME implementar hash para texto eletrônico - hasher = gerar_hash_arquivo( - proposicao.texto_original.path, - str(proposicao.pk)) if proposicao.texto_original else None + if proposicao.texto_articulado.exists(): + ta = proposicao.texto_articulado.first() + # FIXME hash para textos articulados + hasher = 'P' + ta.hash() + '/' + str(proposicao.id) + else: + hasher = gerar_hash_arquivo( + proposicao.texto_original.path, + str(proposicao.pk)) if proposicao.texto_original else None if hasher == form.cleaned_data['cod_hash']: return HttpResponseRedirect( reverse('sapl.materia:proposicao-confirmar', @@ -343,9 +367,6 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView): form_class = ConfirmarProposicaoForm def get_success_url(self): - # FIXME redirecionamento trival, - # ainda por implementar se será para protocolo ou para doc resultante - msgs = self.object.results['messages'] for key, value in msgs.items(): @@ -365,9 +386,15 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView): data_recebimento__isnull=True) self.object = None # FIXME implementar hash para texto eletrônico - hasher = gerar_hash_arquivo( - proposicao.texto_original.path, - str(proposicao.pk)) if proposicao.texto_original else None + + if proposicao.texto_articulado.exists(): + ta = proposicao.texto_articulado.first() + # FIXME hash para textos articulados + hasher = 'P' + ta.hash() + '/' + str(proposicao.id) + else: + hasher = gerar_hash_arquivo( + proposicao.texto_original.path, + str(proposicao.pk)) if proposicao.texto_original else None if hasher == 'P%s/%s' % (self.kwargs['hash'], proposicao.pk): self.object = proposicao @@ -474,6 +501,13 @@ class ProposicaoCrud(Crud): p.data_devolucao = None p.data_envio = datetime.now() p.save() + + if p.texto_articulado.exists(): + ta = p.texto_articulado.first() + ta.privacidade = STATUS_TA_IMMUTABLE_RESTRICT + ta.editing_locked = True + ta.save() + messages.success(request, _( 'Proposição enviada com sucesso.')) @@ -486,6 +520,11 @@ class ProposicaoCrud(Crud): else: p.data_envio = None p.save() + if p.texto_articulado.exists(): + ta = p.texto_articulado.first() + ta.privacidade = STATUS_TA_PRIVATE + ta.editing_locked = False + ta.save() messages.success(request, _( 'Proposição Retornada com sucesso.')) @@ -547,9 +586,8 @@ class ProposicaoCrud(Crud): messages.info(self.request, _('Sempre que uma Proposição é inclusa ou ' 'alterada e a opção "Texto Articulado " for ' - 'marcada, você será redirecionado para o ' - 'Texto Eletrônico. Use a opção "Editar Texto" ' - 'para construir seu texto.')) + 'marcada, você será redirecionado para a ' + 'edição do Texto Eletrônico.')) return reverse('sapl.materia:proposicao_ta', kwargs={'pk': self.object.pk}) else: @@ -618,10 +656,18 @@ class ReciboProposicaoView(TemplateView): context = super(ReciboProposicaoView, self).get_context_data( **kwargs) proposicao = Proposicao.objects.get(pk=self.kwargs['pk']) + + if proposicao.texto_original: + _hash = gerar_hash_arquivo( + proposicao.texto_original.path, + self.kwargs['pk']) + elif proposicao.texto_articulado.exists(): + ta = proposicao.texto_articulado.first() + # FIXME hash para textos articulados + _hash = 'P' + ta.hash() + '/' + str(proposicao.id) + context.update({'proposicao': proposicao, - 'hash': gerar_hash_arquivo( - proposicao.texto_original.path, - self.kwargs['pk'])}) + 'hash': _hash}) return context def get(self, request, *args, **kwargs): @@ -631,7 +677,7 @@ class ReciboProposicaoView(TemplateView): return TemplateView.get(self, request, *args, **kwargs) if not proposicao.data_envio and not proposicao.data_devolucao: - messages.error(request, _('Não é possível gerar recebo para uma ' + messages.error(request, _('Não é possível gerar recibo para uma ' 'Proposição ainda não enviada.')) elif proposicao.data_devolucao: messages.error(request, _('Não é possível gerar recibo.')) diff --git a/sapl/rules/map_rules.py b/sapl/rules/map_rules.py index ccc056aca..ff5208f4a 100644 --- a/sapl/rules/map_rules.py +++ b/sapl/rules/map_rules.py @@ -84,6 +84,7 @@ rules_group_protocolo = { (materia.Autoria, __base__), (materia.Proposicao, __listdetailchange__), + (compilacao.TextoArticulado, ['view_restricted_textoarticulado']) ] } @@ -117,8 +118,8 @@ rules_group_materia = { # quando testes forem feitos para permtir que matérias possam # ser vinculadas a outras matérias via registro de compilação. # Normalmente emendas e/ou projetos substitutivos podem alterar - # uma matéria original. Fazer esse registro de compilação - # oferecia um autografo eletrônico pronto ser convertido em Norma. + # uma matéria original. Fazer esse registro de compilação ofereceria + # um autografo eletrônico pronto para ser convertido em Norma. ]) ] } @@ -175,7 +176,7 @@ rules_group_autor = { 'rules': [ (materia.Proposicao, __base__), (compilacao.Dispositivo, __base__ + [ - 'change_dispositivo_edicao_dinamica', + 'change_your_dispositivo_edicao_dinamica', ]) ] } @@ -258,12 +259,18 @@ rules_group_geral = { # este model é um espelho do model integrado e sua edição pode # confundir Autores, operadores de matéria e/ou norma. # Por isso está adicionado apenas para o operador geral - (compilacao.TextoArticulado, __base__), + (compilacao.TextoArticulado, __base__ + ['lock_unlock_textoarticulado']), # estes tres models são complexos e a principio apenas o admin tem perm (compilacao.TipoDispositivo, []), (compilacao.TipoDispositivoRelationship, []), (compilacao.PerfilEstruturalTextoArticulado, []), + + + + + + ] } 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/text_edit.html b/sapl/templates/compilacao/text_edit.html index c93042c7f..899df7b20 100644 --- a/sapl/templates/compilacao/text_edit.html +++ b/sapl/templates/compilacao/text_edit.html @@ -11,7 +11,7 @@ {% endblock %} {% block title%} -

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

+

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

{% endblock %} {% block actions %} @@ -19,6 +19,10 @@
-
-
-
- -

{{ object.ementa|safe}}

-
+
+
+
+ +

{{ object.ementa|safe}}

+
{% endblock detail_content %} {% endblock base_content %} diff --git a/sapl/templates/compilacao/tipotextoarticulado_detail.html b/sapl/templates/compilacao/tipotextoarticulado_detail.html index 7316ed251..afc0a0a85 100644 --- a/sapl/templates/compilacao/tipotextoarticulado_detail.html +++ b/sapl/templates/compilacao/tipotextoarticulado_detail.html @@ -10,8 +10,7 @@ {% block actions %}
{% if perms.compilacao.change_tipotextoarticulado %}{% trans 'Editar' %}{% endif %} - {% if perms.compilacao.delete_tipotextoarticulado %}{% trans 'Excluir' %}{% endif %} - + {% if perms.compilacao.delete_tipotextoarticulado %}{% trans 'Excluir' %}{% endif %}
{% endblock actions %}
@@ -20,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 %} From 1a33f87eaeb5bd181b67b7be86dbde8223b84dc0 Mon Sep 17 00:00:00 2001 From: LeandroRoberto Date: Tue, 8 Nov 2016 10:28:37 -0200 Subject: [PATCH 2/2] Fix #786 --- sapl/compilacao/models.py | 2 +- sapl/crispy_layout_mixin.py | 5 +-- sapl/crud/base.py | 7 ++-- sapl/parlamentares/views.py | 35 ++++++++++++++++++- .../compilacao/textoarticulado_detail.html | 4 ++- sapl/templates/crud/detail.html | 9 +++++ sapl/templates/crud/detail_detail.html | 11 ++++++ sapl/templates/materia/proposicao_detail.html | 1 - 8 files changed, 63 insertions(+), 11 deletions(-) diff --git a/sapl/compilacao/models.py b/sapl/compilacao/models.py index 1531e6542..6752a82c1 100644 --- a/sapl/compilacao/models.py +++ b/sapl/compilacao/models.py @@ -210,7 +210,7 @@ class TextoArticulado(TimestampedMixin): if request.user in self.owners.all(): return True - if self.privacidade == STATUS_TA_RESTRICT and\ + if self.privacidade == STATUS_TA_IMMUTABLE_RESTRICT and\ request.user.has_perm( 'compilacao.view_restricted_textoarticulado'): return True diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index 34c9ecab4..028301502 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -1,12 +1,12 @@ 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 from django import template from django.utils import formats from django.utils.translation import ugettext as _ +import rtyaml def heads_and_tails(list_of_lists): @@ -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 = '
    ' for v in value.all(): display += '
  • %s
  • ' % str(v) diff --git a/sapl/crud/base.py b/sapl/crud/base.py index a5c029655..cc84c7b4e 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -16,8 +16,8 @@ from django.http.response import Http404 from django.shortcuts import redirect from django.utils.decorators import classonlymethod from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ from django.views.generic import (CreateView, DeleteView, DetailView, ListView, UpdateView) from django.views.generic.base import ContextMixin @@ -29,6 +29,7 @@ from sapl.rules.map_rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, from sapl.settings import BASE_DIR from sapl.utils import normalize + logger = logging.getLogger(BASE_DIR.name) ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ @@ -1401,10 +1402,6 @@ class CrudBaseForListAndDetailExternalAppView(MasterDetailCrud): class BaseMixin(Crud.PublicMixin, MasterDetailCrud.BaseMixin): - @classmethod - def url_name(cls, suffix): - return '%s_parlamentar_%s' % (cls.model._meta.model_name, suffix) - def resolve_url(self, suffix, args=None): obj = self.crud if hasattr(self, 'crud') else self diff --git a/sapl/parlamentares/views.py b/sapl/parlamentares/views.py index 38228d920..b461cdbee 100644 --- a/sapl/parlamentares/views.py +++ b/sapl/parlamentares/views.py @@ -54,6 +54,12 @@ class RelatoriaParlamentarCrud(CrudBaseForListAndDetailExternalAppView): help_path = 'relatoria_parlamentar' namespace = AppConfig.name + class BaseMixin(CrudBaseForListAndDetailExternalAppView.BaseMixin): + + @classmethod + def url_name(cls, suffix): + return '%s_parlamentar_%s' % (cls.model._meta.model_name, suffix) + class ProposicaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView): model = Proposicao @@ -61,10 +67,31 @@ class ProposicaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView): parent_field = 'autor__parlamentar_set' namespace = AppConfig.name + class BaseMixin(CrudBaseForListAndDetailExternalAppView.BaseMixin): + + @classmethod + def url_name(cls, suffix): + return '%s_parlamentar_%s' % (cls.model._meta.model_name, suffix) + class ListView(CrudBaseForListAndDetailExternalAppView.ListView): def get_queryset(self): - return super().get_queryset().filter(data_envio__isnull=False) + return super().get_queryset().filter( + data_envio__isnull=False, + data_recebimento__isnull=False) + + class DetailView(CrudBaseForListAndDetailExternalAppView.DetailView): + + @property + def extras_url(self): + + if self.object.texto_articulado.exists(): + ta = self.object.texto_articulado.first() + yield (str(reverse_lazy( + 'sapl.compilacao:ta_text', + kwargs={'ta_id': ta.pk})) + '?back_type=history', + 'btn-success', + _('Texto Eletrônico')) class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView): @@ -74,6 +101,12 @@ class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView): list_field_names = ['composicao__comissao__nome', 'cargo__nome', ( 'composicao__periodo__data_inicio', 'composicao__periodo__data_fim')] + class BaseMixin(CrudBaseForListAndDetailExternalAppView.BaseMixin): + + @classmethod + def url_name(cls, suffix): + return '%s_parlamentar_%s' % (cls.model._meta.model_name, suffix) + class ListView(CrudBaseForListAndDetailExternalAppView.ListView): ordering = ('-composicao__periodo') diff --git a/sapl/templates/compilacao/textoarticulado_detail.html b/sapl/templates/compilacao/textoarticulado_detail.html index 40c961dfa..82a078697 100644 --- a/sapl/templates/compilacao/textoarticulado_detail.html +++ b/sapl/templates/compilacao/textoarticulado_detail.html @@ -7,7 +7,9 @@
+ {% if view.extras_url %} +
+ {% for url, css_class, text in view.extras_url %} + + {{text}} + + {% endfor %} +
+ {% endif %} {% endblock sub_actions %}
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/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 %}