import logging from braces.views import FormMessagesMixin from crispy_forms.bootstrap import FieldWithButtons, StrictButton from crispy_forms.layout import Field, Layout from django import forms from django.conf import settings from django.conf.urls import url from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models.fields.related import ForeignKey, ManyToManyField from django.http.response import Http404 from django.shortcuts import redirect from django.urls import reverse from django.utils.decorators import classonlymethod from django.utils.encoding import force_text 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 from django.views.generic.list import MultipleObjectMixin from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display from sapl.crispy_layout_mixin import SaplFormHelper from sapl.rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, RP_LIST) from sapl.settings import RATE_LIMITER_RATE from sapl.utils import normalize, ratelimit_ip from ratelimit.decorators import ratelimit from django.utils.decorators import method_decorator logger = logging.getLogger(settings.BASE_DIR.name) ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ 'list', 'create', 'detail', 'update', 'delete' def _form_invalid_message(msg): return '%s %s' % (_('Formulário inválido.'), msg) 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()} def from_to(start, end): return list(range(start, end + 1)) def make_pagination(index, num_pages): '''Make a list of adjacent page ranges interspersed with "None"s The list starts with [1, 2] and end with [num_pages-1, num_pages]. The list includes [index-1, index, index+1] "None"s separate those ranges and mean ellipsis (...) Example: [1, 2, None, 10, 11, 12, None, 29, 30] ''' PAGINATION_LENGTH = 10 if num_pages <= PAGINATION_LENGTH: return from_to(1, num_pages) else: if index - 1 <= 5: tail = [num_pages - 1, num_pages] head = from_to(1, PAGINATION_LENGTH - 3) else: if index + 1 >= num_pages - 3: tail = from_to(index - 1, num_pages) else: tail = [index - 1, index, index + 1, None, num_pages - 1, num_pages] head = from_to(1, PAGINATION_LENGTH - len(tail) - 1) return head + [None] + tail """ variáveis do crud: help_topic 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 ordered_list = False desativa os clicks e controles de ord da listagem parent_field = parentesco reverso separado por '__' namespace return_parent_field_url """ class SearchMixin(models.Model): search = models.TextField(blank=True, default='') logger = logging.getLogger(__name__) 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 Exception as e: username = self.request.user.username self.logger.error("user=" + username + ". " + str(e)) 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 = SaplFormHelper() self.form_class = 'form-inline' self.helper.form_method = 'GET' self.helper.layout = Layout( Field('o'), FieldWithButtons( Field('q', placeholder=_('Filtrar Lista'), css_class='form-control-lg'), StrictButton( _('Filtrar'), css_class='btn-outline-primary btn-lg', type='submit')) ) class PermissionRequiredForAppCrudMixin(PermissionRequiredMixin): def has_permission(self): apps = self.app_label if isinstance(apps, str): apps = apps, # app_label vazio dará acesso geral for app in apps: if not self.request.user.has_module_perms(app): return False return True class PermissionRequiredContainerCrudMixin(PermissionRequiredMixin): def has_permission(self): perms = self.get_permission_required() # Torna a view pública se não possuir conteudo # no atributo permission_required if not len(perms): return True for perm in perms: if self.request.user.has_perm(perm): return True return False # 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() elif self.container_field: container = self.container_field.split('__') if len(container) > 1: container_model = getattr( self.model, container[0]).field.related_model params = {} params['__'.join( container[1:])] = request.user.pk if not container_model.objects.filter(**params).exists(): messages.error( request, 'O Usuário (%s) não está registrado como (%s).' % ( request.user, container_model._meta.verbose_name)) return redirect('/') else: # TODO: implementar caso o user for o próprio o container pass return super(PermissionRequiredMixin, self).dispatch( request, *args, **kwargs) @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 @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 @property def is_contained(self): return self.container_field_set or self.container_field class CrudBaseMixin(CrispyLayoutFormMixin): def __init__(self, **kwargs): super(CrudBaseMixin, self).__init__(**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 not hasattr(obj, 'public'): obj.public = [] if hasattr(self, 'permission_required') and self.permission_required: self.permission_required = tuple( ( self.permission(pr) for pr in ( set(self.permission_required) - set(obj.public) ) ) ) @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 ordered_list(self): return True @property def list_url(self): 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): 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): 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): 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): obj = self.crud if hasattr(self, 'crud') else self if not obj.DeleteView.permission_required: return self.resolve_url(ACTION_DELETE, args=(self.object.id,)) else: return self.resolve_url(ACTION_DELETE, args=(self.object.id,)) \ if self.request.user.has_perm( self.permission(RP_DELETE)) else '' @property def openapi_url(self): obj = self.crud if hasattr(self, 'crud') else self o = self.object url = f'/api/{o._meta.app_label}/{o._meta.model_name}/{o.id}' return url def get_template_names(self): names = super(CrudBaseMixin, self).get_template_names() names.append("crud/%s.html" % 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 @property def verbose_name_plural(self): return self.model._meta.verbose_name_plural @method_decorator(ratelimit(key=ratelimit_ip, rate=RATE_LIMITER_RATE, block=True), name='dispatch') class CrudListView(PermissionRequiredContainerCrudMixin, ListView): permission_required = (RP_LIST,) logger = logging.getLogger(__name__) @classmethod def get_url_regex(cls): return r'^$' paginate_by = 10 no_entries_msg = _('Nenhum registro encontrado.') def get_rows(self, object_list): return [self._as_row(obj) for obj in object_list] def get_headers(self): """ 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') ou ainda: list_field_names = ['composicao__comissao__nome', 'cargo__nome', ( 'composicao__periodo__data_inicio', 'composicao__periodo__data_fim')] """ r = [] for (fieldname, _) in self._parse_field_names(self.list_field_names): if not isinstance(fieldname, tuple): fieldname = fieldname, s = [] for fn in fieldname: m = self.model fn = fn.split('__') for f in fn: if not f: continue try: f = m._meta.get_field(f) if hasattr(f, 'related_model') and f.related_model: m = f.related_model except: f = None if f: hook = 'hook_header_{}'.format(''.join(fn)) if hasattr(self, hook): header = getattr(self, hook)() s.append(force_text(header)) else: s.append(force_text(f.verbose_name)) else: hook = 'hook_header_{}'.format(''.join(fn)) if hasattr(self, hook): header = getattr(self, hook)() s.append(header) s = ' / '.join(filter(lambda x: x, s)) r.append(s) return r def _parse_field_names(self, field_name_list): parsed_list = [] for field_name in field_name_list: field_tuple = tuple(field_name.split('|')) \ if '|' in field_name else (field_name, None) parsed_list.append(field_tuple) return parsed_list def _as_row(self, obj): r = [] for i, (name, func) in enumerate(self._parse_field_names(self.list_field_names)): # URL padrão para primeira coluna da listagem url = self.resolve_url( ACTION_DETAIL, args=(obj.id,)) if i == 0 else None # gera URL para matéria a partir de fk_urlify_for_list em # layouts.yaml if i > 0 and func is not None: url = getattr(self, func)(obj, name)[0] """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 '') if not isinstance(name, tuple): name = name, """ 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): if not n: s += '
' continue m = obj n = n.split('__') for f in n[:-1]: m = getattr(m, f) if not m: break ss = '' try: if m: ss = get_field_display(m, n[-1])[1] ss = ( ('
' if '