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..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 _ @@ -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_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) @@ -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/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 = '