From 7cd36e2d82fc1056d2b7af0a5a74e9188c53cc60 Mon Sep 17 00:00:00 2001 From: LeandroRoberto Date: Thu, 29 Sep 2016 16:12:59 -0300 Subject: [PATCH 1/9] Ref crud e dependencias e adequa app comissoes --- .../migrations/0019_auto_20160928_1951.py | 19 + sapl/base/models.py | 111 ++ sapl/base/templatetags/common_tags.py | 11 + sapl/base/urls.py | 6 +- sapl/base/views.py | 15 +- .../migrations/0008_auto_20160929_1611.py | 21 + sapl/comissoes/models.py | 2 +- sapl/comissoes/views.py | 127 +-- sapl/crispy_layout_mixin.py | 27 +- sapl/crud/base.py | 972 +++++++++++++++++- sapl/crud/masterdetail.py | 19 +- sapl/crud/urls.py | 9 + sapl/materia/views.py | 99 +- sapl/norma/views.py | 10 +- sapl/parlamentares/views.py | 74 +- sapl/protocoloadm/views.py | 40 +- sapl/sessao/views.py | 61 +- sapl/static/styles/app.scss | 7 + sapl/templates/base.html | 6 +- sapl/templates/crud/ajax_form.html | 2 + sapl/templates/crud/confirm_delete.html | 9 +- sapl/templates/crud/detail.html | 96 +- sapl/templates/crud/detail_detail.html | 92 ++ sapl/templates/crud/form.html | 2 +- sapl/templates/crud/list.html | 80 +- sapl/utils.py | 10 +- 26 files changed, 1554 insertions(+), 373 deletions(-) create mode 100644 sapl/base/migrations/0019_auto_20160928_1951.py create mode 100644 sapl/comissoes/migrations/0008_auto_20160929_1611.py create mode 100644 sapl/templates/crud/ajax_form.html create mode 100644 sapl/templates/crud/detail_detail.html diff --git a/sapl/base/migrations/0019_auto_20160928_1951.py b/sapl/base/migrations/0019_auto_20160928_1951.py new file mode 100644 index 000000000..9c61f7062 --- /dev/null +++ b/sapl/base/migrations/0019_auto_20160928_1951.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-09-28 22:51 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0018_auto_20160919_1333'), + ] + + operations = [ + migrations.AlterModelOptions( + name='appconfig', + options={'permissions': (('menu_sistemas', 'Renderizar Menu Sistemas'), ('view_tabelas_auxiliares', 'Visualizar Tabelas Auxiliares')), 'verbose_name': 'Configurações da Aplicação', 'verbose_name_plural': 'Configurações da Aplicação'}, + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 86a685ef4..12e840fda 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -1,10 +1,21 @@ +from builtins import LookupError + +from django.apps import apps +from django.conf.urls import include, url +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 +from django.db import models, router +from django.db.utils import DEFAULT_DB_ALIAS +from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ +from rest_framework import permissions from sapl.utils import UF, YES_NO_CHOICES + TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')), ('R', _('Restritivo'))) @@ -94,7 +105,107 @@ class AppConfig(models.Model): class Meta: verbose_name = _('Configurações da Aplicação') verbose_name_plural = _('Configurações da Aplicação') + permissions = ( + ('menu_sistemas', _('Renderizar Menu Sistemas')), + ('view_tabelas_auxiliares', _('Visualizar Tabelas Auxiliares')), + ) def __str__(self): return _('Configurações da Aplicação - %(id)s') % { 'id': self.id} + + +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) + + +models.signals.post_migrate.connect( + receiver=create_proxy_permissions, + dispatch_uid="django.contrib.auth.management.create_permissions") diff --git a/sapl/base/templatetags/common_tags.py b/sapl/base/templatetags/common_tags.py index 30db1a92f..ec21ce48b 100644 --- a/sapl/base/templatetags/common_tags.py +++ b/sapl/base/templatetags/common_tags.py @@ -126,3 +126,14 @@ def get_config_not_exists(user): return True else: return False + +@register.filter +def str2intabs(value): + if not isinstance(value, str): + return '' + try: + v = int(value) + v = abs(v) + return v + except: + return '' diff --git a/sapl/base/urls.py b/sapl/base/urls.py index 784649cfd..22691759d 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -1,5 +1,6 @@ from django.conf.urls import include, url from django.contrib.auth import views +from django.contrib.auth.decorators import permission_required from django.views.generic.base import TemplateView from .apps import AppConfig @@ -11,11 +12,14 @@ from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView, RelatorioMateriasTramitacaoView, RelatorioPresencaSessaoView) + app_name = AppConfig.name urlpatterns = [ - url(r'^sistema/', TemplateView.as_view(template_name='sistema.html')), + url(r'^sistema/', permission_required('base.view_tabelas_auxiliares') + (TemplateView.as_view(template_name='sistema.html'))), + url(r'^ajuda/', TemplateView.as_view(template_name='ajuda.html')), url(r'^relatorios/', TemplateView.as_view( template_name='base/relatorios_list.html')), diff --git a/sapl/base/views.py b/sapl/base/views.py index 4cb3a5c52..2e48efc3a 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1,4 +1,5 @@ -from django.contrib.auth.mixins import PermissionRequiredMixin + +from braces.views import PermissionRequiredMixin from django.core.urlresolvers import reverse from django.db.models import Count, Q from django.http import HttpResponseRedirect @@ -229,16 +230,16 @@ class CasaLegislativaCrud(Crud): model = CasaLegislativa help_path = '' - class BaseMixin(PermissionRequiredMixin, CrudBaseMixin): + class BaseMixin(CrudBaseMixin): list_field_names = ['codigo', 'nome', 'sigla'] def has_permission(self): return permissao_tb_aux(self) - class CreateView(PermissionRequiredMixin, CrudCreateView): + class CreateView(CrudCreateView): form_class = CasaLegislativaForm - class UpdateView(PermissionRequiredMixin, CrudUpdateView): + class UpdateView(CrudUpdateView): form_class = CasaLegislativaForm class DetailView(CrudDetailView): @@ -269,7 +270,7 @@ class AppConfigCrud(Crud): model = AppConfig help_path = '' - class BaseMixin(PermissionRequiredMixin, CrudBaseMixin): + class BaseMixin(CrudBaseMixin): list_field_names = ['documentos_administrativos', 'sequencia_numeracao', 'painel_aberto'] @@ -277,7 +278,7 @@ class AppConfigCrud(Crud): def has_permission(self): return permissao_tb_aux(self) - class CreateView(PermissionRequiredMixin, CrudCreateView): + class CreateView(CrudCreateView): form_class = ConfiguracoesAppForm def get(self, request, *args, **kwargs): @@ -291,5 +292,5 @@ class AppConfigCrud(Crud): return super(BaseCreateView, self).get( request, *args, **kwargs) - class UpdateView(PermissionRequiredMixin, CrudUpdateView): + class UpdateView(CrudUpdateView): form_class = ConfiguracoesAppForm diff --git a/sapl/comissoes/migrations/0008_auto_20160929_1611.py b/sapl/comissoes/migrations/0008_auto_20160929_1611.py new file mode 100644 index 000000000..5e90c95b4 --- /dev/null +++ b/sapl/comissoes/migrations/0008_auto_20160929_1611.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-09-29 19:11 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('comissoes', '0007_merge'), + ] + + operations = [ + migrations.AlterField( + model_name='participacao', + name='composicao', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participacao_set', to='comissoes.Composicao'), + ), + ] diff --git a/sapl/comissoes/models.py b/sapl/comissoes/models.py index 579b95f2a..60aee6368 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -131,7 +131,7 @@ class Composicao(models.Model): # IGNORE class Participacao(models.Model): # ComposicaoComissao - composicao = models.ForeignKey(Composicao) + composicao = models.ForeignKey(Composicao, related_name='participacao_set') parlamentar = models.ForeignKey(Parlamentar) cargo = models.ForeignKey(CargoComissao) titular = models.BooleanField(verbose_name=_('Titular')) diff --git a/sapl/comissoes/views.py b/sapl/comissoes/views.py index e472a69cc..8207ac290 100644 --- a/sapl/comissoes/views.py +++ b/sapl/comissoes/views.py @@ -1,10 +1,11 @@ +from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.urlresolvers import reverse from django.views.generic import ListView from sapl.crud.base import (Crud, CrudBaseMixin, CrudCreateView, - CrudDeleteView, CrudListView, CrudUpdateView) -from sapl.crud.masterdetail import MasterDetailCrud + CrudDeleteView, CrudListView, CrudUpdateView, + MasterDetailCrud, CrudAux) from sapl.materia.models import MateriaLegislativa, Tramitacao from sapl.utils import permissao_tb_aux, permissoes_comissoes @@ -18,130 +19,52 @@ def pegar_url_composicao(pk): url = reverse('sapl.comissoes:composicao_detail', kwargs={'pk': comp_pk}) return url +CargoCrud = CrudAux.build(CargoComissao, 'cargo_comissao') +PeriodoComposicaoCrud = CrudAux.build(Periodo, 'periodo_composicao_comissao') -class CargoCrud(Crud): - model = CargoComissao - help_path = 'cargo_comissao' - - class BaseMixin(PermissionRequiredMixin, CrudBaseMixin): - list_field_names = ['nome', 'unico'] - - def has_permission(self): - return permissao_tb_aux(self) - - -class PeriodoComposicaoCrud(Crud): - model = Periodo - help_path = 'periodo_composicao_comissao' - - class BaseMixin(PermissionRequiredMixin, CrudBaseMixin): - list_field_names = ['data_inicio', 'data_fim'] - - def has_permission(self): - return permissao_tb_aux(self) - - -class TipoComissaoCrud(Crud): - model = TipoComissao - help_path = 'tipo_comissao' - - class BaseMixin(PermissionRequiredMixin, CrudBaseMixin): - list_field_names = ['sigla', 'nome', 'natureza', - 'dispositivo_regimental'] - - def has_permission(self): - return permissao_tb_aux(self) +TipoComissaoCrud = CrudAux.build( + TipoComissao, 'tipo_comissao', list_field_names=[ + 'sigla', 'nome', 'natureza', 'dispositivo_regimental']) class ParticipacaoCrud(MasterDetailCrud): model = Participacao - parent_field = 'composicao' + parent_field = 'composicao__comissao' help_path = '' - class DetailView(MasterDetailCrud.DetailView): - def get(self, request, *args, **kwargs): - self.object = self.get_object() - context = self.get_context_data(object=self.object) - context['root_pk'] = self.object.composicao.comissao.pk - return self.render_to_response(context) - - class CreateView(MasterDetailCrud.CreateView): - - def get_success_url(self): - return reverse( - 'sapl.comissoes:composicao_detail', - kwargs={'pk': self.kwargs['pk']} - ) - - def cancel_url(self): - return reverse( - 'sapl.comissoes:composicao_detail', - kwargs={'pk': self.kwargs['pk']} - ) - - class UpdateView(MasterDetailCrud.UpdateView): - - def get_success_url(self): - return pegar_url_composicao(self.kwargs['pk']) - - def cancel_url(self): - return pegar_url_composicao(self.kwargs['pk']) - - class DeleteView(MasterDetailCrud.DeleteView): - - def get_success_url(self): - return pegar_url_composicao(self.kwargs['pk']) - - def cancel_url(self): - return pegar_url_composicao(self.kwargs['pk']) - - class BaseMixin(PermissionRequiredMixin, MasterDetailCrud.BaseMixin): - permission_required = permissoes_comissoes() + class BaseMixin(MasterDetailCrud.BaseMixin): list_field_names = ['composicao', 'parlamentar', 'cargo'] + class DetailView(MasterDetailCrud.DetailView): + permission_required = [] + class ComposicaoCrud(MasterDetailCrud): model = Composicao parent_field = 'comissao' help_path = '' + model_set = 'participacao_set' - class DetailView(MasterDetailCrud.DetailView): - - def get(self, request, *args, **kwargs): - self.object = self.get_object() - context = self.get_context_data(object=self.object) - composicao = Composicao.objects.get(id=self.kwargs['pk']) - context['participacoes'] = composicao.participacao_set.all() - return self.render_to_response(context) - - class CreateView(PermissionRequiredMixin, MasterDetailCrud.CreateView): - permission_required = permissoes_comissoes() + class ListView(MasterDetailCrud.ListView): + permission_required = [] - class UpdateView(PermissionRequiredMixin, MasterDetailCrud.UpdateView): - permission_required = permissoes_comissoes() - - class DeleteView(PermissionRequiredMixin, MasterDetailCrud.DeleteView): - permission_required = permissoes_comissoes() + class DetailView(MasterDetailCrud.DetailView): + permission_required = [] class ComissaoCrud(Crud): model = Comissao help_path = 'modulo_comissoes' - class CreateView(PermissionRequiredMixin, CrudCreateView): - permission_required = permissoes_comissoes() - - class UpdateView(PermissionRequiredMixin, CrudUpdateView): - permission_required = permissoes_comissoes() - - class DeleteView(PermissionRequiredMixin, CrudDeleteView): - permission_required = permissoes_comissoes() + class BaseMixin(Crud.BaseMixin): + list_field_names = ['nome', 'sigla', 'tipo', 'data_criacao', 'ativa'] + initial_order = '-ativa', 'sigla' - class ListView(CrudListView): - ordering = ['-ativa', 'sigla'] + class ListView(Crud.ListView): + permission_required = [] - class BaseMixin(CrudBaseMixin): - list_field_names = ['nome', 'sigla', 'tipo', 'data_criacao', 'ativa'] + class DetailView(Crud.DetailView): + permission_required = [] class MateriasTramitacaoListView(ListView): diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index 6ac4a2e22..89dd2da8e 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): @@ -97,10 +97,25 @@ class CrispyLayoutFormMixin: else: return self.model.__name__ + @property + def layout_key_set(self): + if hasattr(super(CrispyLayoutFormMixin, self), 'layout_key_set'): + return super(CrispyLayoutFormMixin, self).layout_key_set + else: + obj = self.crud if hasattr(self, 'crud') else self + return getattr(obj.model, + obj.model_set).field.model.__name__ + def get_layout(self): yaml_layout = '%s/layouts.yaml' % self.model._meta.app_config.label return read_layout_from_yaml(yaml_layout, self.layout_key) + def get_layout_set(self): + obj = self.crud if hasattr(self, 'crud') else self + yaml_layout = '%s/layouts.yaml' % getattr( + obj.model, obj.model_set).field.model._meta.app_config.label + return read_layout_from_yaml(yaml_layout, self.layout_key_set) + @property def fields(self): if hasattr(self, 'form_class') and self.form_class: @@ -132,6 +147,16 @@ class CrispyLayoutFormMixin: rows = self.get_layout()[0][1:] return [fieldname for row in rows for fieldname, __ in row] + @property + def list_field_names_set(self): + '''The list of field names to display on table + + This base implementation returns the field names + in the first fieldset of the layout. + ''' + rows = self.get_layout_set()[0][1:] + return [fieldname for row in rows for fieldname, __ in row] + def get_column(self, fieldname, span): obj = self.get_object() verbose_name, text = get_field_display(obj, fieldname) diff --git a/sapl/crud/base.py b/sapl/crud/base.py index 289bd21e1..d480684b6 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -1,26 +1,52 @@ +import logging + from braces.views import FormMessagesMixin +from compressor.utils.decorators import cached_property +from crispy_forms.bootstrap import FieldWithButtons, StrictButton +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Field +from django import forms +from django.apps import apps from django.conf.urls import url +from django.contrib.auth.management import _get_all_permissions +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.core import exceptions from django.core.urlresolvers import reverse +from django.db import models, router +from django.db.utils import DEFAULT_DB_ALIAS +from django.http.response import Http404 +from django.shortcuts import redirect from django.utils.decorators import classonlymethod -from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import force_text +from django.utils.translation import ugettext_lazy as _, string_concat from django.views.generic import (CreateView, DeleteView, DetailView, ListView, UpdateView) +from django.views.generic.base import ContextMixin +from django.views.generic.list import MultipleObjectMixin from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display +from sapl.utils import normalize + + +logger = logging.getLogger(__name__) -LIST, CREATE, DETAIL, UPDATE, DELETE = \ +ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ 'list', 'create', 'detail', 'update', 'delete' +# RP - Radical das permissões para "..." +RP_LIST, RP_DETAIL, RP_ADD, RP_CHANGE, RP_DELETE =\ + '.list_', '.detail_', '.add_', '.change_', '.delete_', + def _form_invalid_message(msg): return '%s %s' % (_('Formulário inválido.'), msg) -FORM_MESSAGES = {CREATE: (_('Registro criado com sucesso!'), - _('O registro não foi criado.')), - UPDATE: (_('Registro alterado com sucesso!'), - _('Suas alterações não foram salvas.')), - DELETE: (_('Registro excluído com sucesso!'), - _('O registro não foi excluído.'))} +FORM_MESSAGES = {ACTION_CREATE: (_('Registro criado com sucesso!'), + _('O registro não foi criado.')), + ACTION_UPDATE: (_('Registro alterado com sucesso!'), + _('Suas alterações não foram salvas.')), + ACTION_DELETE: (_('Registro excluído com sucesso!'), + _('O registro não foi excluído.'))} FORM_MESSAGES = {k: (a, _form_invalid_message(b)) for k, (a, b) in FORM_MESSAGES.items()} @@ -55,37 +81,228 @@ def make_pagination(index, num_pages): head = from_to(1, PAGINATION_LENGTH - len(tail) - 1) return head + [None] + tail +""" +variáveis do crud: + container_field + container_field_set + is_m2m + model + model_set + form_search_class -> depende de o model relativo implementar SearchMixin + list_field_names + list_field_names_set -> lista reversa em details + permission_required -> este atributo ser vazio não nulo torna a view publ + layout_key_set + layout_key + parent_field = parentesco reverso separado por '__' + initial_order = deve ser um elemento de list_field_names + FIXME: as setas indicativas de ordem em crud/list.html + não está se comportando como esperado para initial_order descrescente +""" + + +class SearchMixin(models.Model): + + search = models.TextField(blank=True, default='') + + class Meta: + abstract = True + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None, auto_update_search=True): + + if auto_update_search and hasattr(self, 'fields_search'): + search = '' + for str_field in self.fields_search: + fields = str_field.split('__') + if len(fields) == 1: + try: + search += str(getattr(self, str_field)) + ' ' + except: + pass + else: + _self = self + for field in fields: + _self = getattr(_self, field) + search += str(_self) + ' ' + self.search = search + self.search = normalize(self.search) + + return super(SearchMixin, self).save( + force_insert=force_insert, force_update=force_update, + using=using, update_fields=update_fields) + + +class ListWithSearchForm(forms.Form): + q = forms.CharField(required=False, label='', + widget=forms.TextInput( + attrs={'type': 'search'})) + + o = forms.CharField(required=False, label='', + widget=forms.HiddenInput()) + + class Meta: + fields = ['q', 'o'] + + def __init__(self, *args, **kwargs): + super(ListWithSearchForm, self).__init__(*args, **kwargs) + + self.helper = FormHelper() + self.form_class = 'form-inline' + self.helper.form_method = 'GET' + self.helper.layout = Layout( + Field('o'), + FieldWithButtons( + Field('q', + placeholder=_('Filtrar Lista'), + css_class='input-lg'), + StrictButton( + _('Filtrar'), css_class='btn-default btn-lg', + type='submit')) + ) + + +class PermissionRequiredContainerCrudMixin(PermissionRequiredMixin): + + def has_permission(self): + perms = self.get_permission_required() + # Torna a view pública se não possuir o atributo permission_required + return self.request.user.has_perms(perms) if len(perms) else True + + def dispatch(self, request, *args, **kwargs): + if not self.has_permission(): + return self.handle_no_permission() + + if 'pk' in kwargs: + params = {'pk': kwargs['pk']} + + if self.container_field: + params[self.container_field] = request.user.pk + + if not self.model.objects.filter(**params).exists(): + raise Http404() + + return super(PermissionRequiredMixin, self).dispatch( + request, *args, **kwargs) + + @cached_property + def container_field(self): + if hasattr(self, 'crud') and not hasattr(self.crud, 'container_field'): + self.crud.container_field = '' + if hasattr(self, 'crud'): + return self.crud.container_field + + @cached_property + def container_field_set(self): + if hasattr(self, 'crud') and\ + not hasattr(self.crud, 'container_field_set'): + self.crud.container_field_set = '' + if hasattr(self, 'crud'): + return self.crud.container_field_set + + @cached_property + def is_contained(self): + return self.container_field_set or self.container_field + class CrudBaseMixin(CrispyLayoutFormMixin): + def __init__(self, **kwargs): + obj = self.crud if hasattr(self, 'crud') else self + self.app_label = obj.model._meta.app_label + self.model_name = obj.model._meta.model_name + + if hasattr(obj, 'model_set') and obj.model_set: + self.app_label_set = getattr( + obj.model, obj.model_set).field.model._meta.app_label + self.model_name_set = getattr( + obj.model, obj.model_set).field.model._meta.model_name + + if hasattr(self, 'permission_required') and\ + self.permission_required: + self.permission_required = tuple(( + self.permission(pr) for pr in self.permission_required)) + @classmethod def url_name(cls, suffix): return '%s_%s' % (cls.model._meta.model_name, suffix) + def url_name_set(self, suffix): + obj = self.crud if hasattr(self, 'crud') else self + return '%s_%s' % (getattr( + obj.model, obj.model_set).field.model._meta.model_name, suffix) + + def permission(self, rad): + return '%s%s%s' % (self.app_label if rad.endswith('_') else '', + rad, + self.model_name if rad.endswith('_') else '') + + def permission_set(self, rad): + return '%s%s%s' % (self.app_label_set if rad.endswith('_') else '', + rad, + self.model_name_set if rad.endswith('_') else '') + def resolve_url(self, suffix, args=None): namespace = self.model._meta.app_config.name return reverse('%s:%s' % (namespace, self.url_name(suffix)), args=args) + def resolve_url_set(self, suffix, args=None): + obj = self.crud if hasattr(self, 'crud') else self + namespace = getattr( + obj.model, obj.model_set).field.model._meta.app_config.name + return reverse('%s:%s' % (namespace, self.url_name_set(suffix)), + args=args) + @property def list_url(self): - return self.resolve_url(LIST) + obj = self.crud if hasattr(self, 'crud') else self + if not obj.ListView.permission_required: + return self.resolve_url(ACTION_LIST) + else: + return self.resolve_url( + ACTION_LIST) if self.request.user.has_perm( + self.permission(RP_LIST)) else '' @property def create_url(self): - return self.resolve_url(CREATE) + obj = self.crud if hasattr(self, 'crud') else self + if not obj.CreateView.permission_required: + return self.resolve_url(ACTION_CREATE) + else: + return self.resolve_url( + ACTION_CREATE) if self.request.user.has_perm( + self.permission(RP_ADD)) else '' @property def detail_url(self): - return self.resolve_url(DETAIL, args=(self.object.id,)) + obj = self.crud if hasattr(self, 'crud') else self + if not obj.DetailView.permission_required: + return self.resolve_url(ACTION_DETAIL, args=(self.object.id,)) + else: + return self.resolve_url(ACTION_DETAIL, args=(self.object.id,))\ + if self.request.user.has_perm( + self.permission(RP_DETAIL)) else '' @property def update_url(self): - return self.resolve_url(UPDATE, args=(self.object.id,)) + obj = self.crud if hasattr(self, 'crud') else self + if not obj.UpdateView.permission_required: + return self.resolve_url(ACTION_UPDATE, args=(self.object.id,)) + else: + return self.resolve_url(ACTION_UPDATE, args=(self.object.id,))\ + if self.request.user.has_perm( + self.permission(RP_CHANGE)) else '' @property def delete_url(self): - return self.resolve_url(DELETE, args=(self.object.id,)) + obj = self.crud if hasattr(self, 'crud') else self + if not obj.DeleteView.permission_required: + return self.resolve_url(ACTION_DELETE) + else: + return self.resolve_url(ACTION_DELETE, args=(self.object.id,))\ + if self.request.user.has_perm( + self.permission(RP_DELETE)) else '' def get_template_names(self): names = super(CrudBaseMixin, self).get_template_names() @@ -93,6 +310,11 @@ class CrudBaseMixin(CrispyLayoutFormMixin): self.template_name_suffix.lstrip('_')) return names + @property + def verbose_name_set(self): + obj = self.crud if hasattr(self, 'crud') else self + return getattr(obj.model, obj.model_set).field.model._meta.verbose_name + @property def verbose_name(self): return self.model._meta.verbose_name @@ -102,12 +324,12 @@ class CrudBaseMixin(CrispyLayoutFormMixin): return self.model._meta.verbose_name_plural -class CrudListView(ListView): +class CrudListView(PermissionRequiredContainerCrudMixin, ListView): + permission_required = (RP_LIST, ) @classmethod def get_url_regex(cls): return r'^$' - paginate_by = 10 no_entries_msg = _('Nenhum registro encontrado.') @@ -115,18 +337,73 @@ class CrudListView(ListView): return [self._as_row(obj) for obj in object_list] def get_headers(self): - return [self.model._meta.get_field(fieldname).verbose_name - for fieldname in self.list_field_names] + """ + Transforma o headers de fields de list_field_names + para junção de fields via tuplas. + list_field_names pode ser construido como + list_field_names=('nome', 'endereco', ('telefone', sexo'), 'dat_nasc') + """ + r = [] + for fieldname in self.list_field_names: + if isinstance(fieldname, tuple): + s = [force_text(self.model._meta.get_field( + fn).verbose_name) for fn in fieldname] + s = ' / '.join(s) + r.append(s) + else: + r.append( + self.model._meta.get_field(fieldname).verbose_name) + return r def _as_row(self, obj): - return [ - (get_field_display(obj, name)[1], - self.resolve_url(DETAIL, args=(obj.id,)) if i == 0 else None) - for i, name in enumerate(self.list_field_names)] + r = [] + for i, name in enumerate(self.list_field_names): + url = self.resolve_url( + ACTION_DETAIL, args=(obj.id,)) if i == 0 else None + + """Caso o crud list seja para uma relação ManyToManyField""" + if url and hasattr(self, 'crud') and\ + hasattr(self.crud, 'is_m2m') and self.crud.is_m2m: + url = url + ('?pkk=' + self.kwargs['pk'] + if 'pk' in self.kwargs else '') + + """ se elemento de list_field_name for uma tupla, constrói a + informação com ' - ' se os campos forem simples, + ou com
se for m2m """ + if isinstance(name, tuple): + s = '' + for j, n in enumerate(name): + ss = get_field_display(obj, n)[1] + ss = ( + ('
' if ' - {% if user|ver_menu_sistema_perm %} -