Source code for cruder.views

"""
CRUD view classes and wrapper functions for Django models
"""

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.http import JsonResponse
from django.views.generic import View
from django.core.exceptions import PermissionDenied
from .forms import render_form, create_model_form
from .templates import render_list


[docs] class CRUDView(View): """ Generic CRUD view class that handles all CRUD operations for a model """ model = None framework = 'bootstrap' template_name = None exclude_fields = None list_fields = None per_page = 25 search_fields = None search_field = None # Deprecated, use search_fields permission_required = None # New features readonly_fields = None # List of fields to show as read-only permissions = None # Dict mapping CRUD operations to required roles/groups # Example: {'C': ['admin', 'editor'], 'U': ['admin'], 'D': ['admin']} readonly_mode = False # If True, entire view is read-only (no C,U,D operations)
[docs] def has_crud_permission(self, user, operation): """ Check if user has permission for CRUD operation Args: user: Django user object operation: 'C', 'R', 'U', or 'D' Returns: bool: True if user has permission """ if self.readonly_mode and operation in ['C', 'U', 'D']: return False if not self.permissions: return True # No restrictions defined, allow all required_roles = self.permissions.get(operation, []) if not required_roles: return True # No roles required for this operation # Check if user has any of the required roles/groups user_groups = [group.name for group in user.groups.all()] # Also check if user is superuser if user.is_superuser: return True # Check if user has any of the required roles return any(role in user_groups for role in required_roles)
[docs] def dispatch(self, request, *args, **kwargs): """Check permissions before processing request""" if self.permission_required and not request.user.has_perm(self.permission_required): raise PermissionDenied return super().dispatch(request, *args, **kwargs)
[docs] def get(self, request, pk=None, action='list'): """Handle GET requests for list, create, edit, view actions""" if action == 'list': if not self.has_crud_permission(request.user, 'R'): raise PermissionDenied("You don't have permission to view this content.") return self.list_view(request) elif action == 'create': if not self.has_crud_permission(request.user, 'C'): raise PermissionDenied("You don't have permission to create new items.") return self.create_view(request) elif action == 'edit' and pk: if not self.has_crud_permission(request.user, 'U'): raise PermissionDenied("You don't have permission to edit items.") return self.edit_view(request, pk) elif action == 'view' and pk: if not self.has_crud_permission(request.user, 'R'): raise PermissionDenied("You don't have permission to view this content.") return self.detail_view(request, pk) elif action == 'delete' and pk: if not self.has_crud_permission(request.user, 'D'): raise PermissionDenied("You don't have permission to delete items.") return self.delete_view(request, pk) else: return self.list_view(request)
[docs] def post(self, request, pk=None, action='create'): """Handle POST requests for create, update, delete actions""" if action == 'create': if not self.has_crud_permission(request.user, 'C'): raise PermissionDenied("You don't have permission to create new items.") return self.create_post(request) elif action == 'edit' and pk: if not self.has_crud_permission(request.user, 'U'): raise PermissionDenied("You don't have permission to edit items.") return self.edit_post(request, pk) elif action == 'delete' and pk: if not self.has_crud_permission(request.user, 'D'): raise PermissionDenied("You don't have permission to delete items.") return self.delete_post(request, pk) else: return self.list_view(request)
[docs] def list_view(self, request): """Display list of model objects""" page = request.GET.get('page', 1) search_query = request.GET.get('search', '') # Get current URL base for proper action links current_url = request.path if current_url.endswith('/'): base_url = current_url else: base_url = current_url + '/' # Check permissions for action buttons permissions_context = { 'can_create': self.has_crud_permission(request.user, 'C'), 'can_read': self.has_crud_permission(request.user, 'R'), 'can_update': self.has_crud_permission(request.user, 'U'), 'can_delete': self.has_crud_permission(request.user, 'D'), } list_data = render_list( model_class=self.model, framework=self.framework, fields=self.list_fields, per_page=self.per_page, page=page, search_fields=self.search_fields, search_field=self.search_field, # For backward compatibility search_query=search_query, base_url=base_url, permissions=permissions_context ) context = { 'list_html': list_data['html'], 'page_obj': list_data['page_obj'], 'total_count': list_data['total_count'], 'model_name': self.model._meta.verbose_name, 'model_name_plural': self.model._meta.verbose_name_plural, 'permissions': permissions_context, } template = self.template_name or 'cruder/list.html' return render(request, template, context)
[docs] def create_view(self, request): """Display create form""" from .forms import create_model_form form_class = create_model_form(self.model, self.framework, self.exclude_fields) form = form_class() context = { 'form': form, 'model_name': self.model._meta.verbose_name, 'action': 'Create', 'framework': self.framework, 'readonly_fields': self.readonly_fields or [], 'readonly_mode': self.readonly_mode, } template = self.template_name or 'cruder/form.html' return render(request, template, context)
[docs] def create_post(self, request): """Handle create form submission""" form_class = create_model_form(self.model, self.framework, self.exclude_fields) form = form_class(request.POST, request.FILES) if form.is_valid(): obj = form.save() messages.success(request, f"{self.model._meta.verbose_name} created successfully!") # Redirect back to list view current_path = request.path if '/create/' in current_path: list_url = current_path.replace('/create/', '/') elif current_path.endswith('/create'): list_url = current_path.replace('/create', '/') elif '/edit/' in current_path: list_url = current_path.split('/edit/')[0] + '/' elif '/delete/' in current_path: list_url = current_path.split('/delete/')[0] + '/' else: list_url = current_path if current_path.endswith('/') else current_path + '/' return redirect(list_url) else: # Re-render form with errors context = { 'form': form, 'model_name': self.model._meta.verbose_name, 'action': 'Create', 'errors': form.errors, } template = self.template_name or 'cruder/form.html' return render(request, template, context)
[docs] def edit_view(self, request, pk): """Display edit form""" obj = get_object_or_404(self.model, pk=pk) from .forms import create_model_form form_class = create_model_form(self.model, self.framework, self.exclude_fields) form = form_class(instance=obj) context = { 'form': form, 'object': obj, 'model_name': self.model._meta.verbose_name, 'action': 'Edit', 'framework': self.framework, 'readonly_fields': self.readonly_fields or [], 'readonly_mode': self.readonly_mode, } template = self.template_name or 'cruder/form.html' return render(request, template, context)
[docs] def edit_post(self, request, pk): """Handle edit form submission""" obj = get_object_or_404(self.model, pk=pk) form_class = create_model_form(self.model, self.framework, self.exclude_fields) form = form_class(request.POST, request.FILES, instance=obj) if form.is_valid(): obj = form.save() messages.success(request, f"{self.model._meta.verbose_name} updated successfully!") # Redirect back to list view current_path = request.path if '/create/' in current_path: list_url = current_path.replace('/create/', '/') elif current_path.endswith('/create'): list_url = current_path.replace('/create', '/') elif '/edit/' in current_path: list_url = current_path.split('/edit/')[0] + '/' elif '/delete/' in current_path: list_url = current_path.split('/delete/')[0] + '/' else: list_url = current_path if current_path.endswith('/') else current_path + '/' return redirect(list_url) else: # Re-render form with errors context = { 'form': form, 'object': obj, 'model_name': self.model._meta.verbose_name, 'action': 'Edit', 'errors': form.errors, } template = self.template_name or 'cruder/form.html' return render(request, template, context)
[docs] def detail_view(self, request, pk): """Display object details""" obj = get_object_or_404(self.model, pk=pk) # Get field information for template fields_data = [] for field in self.model._meta.get_fields(): if not field.name.endswith('_set') and field.name != 'id': try: field_name = field.name field_label = getattr(field, 'verbose_name', field_name.replace('_', ' ').title()) field_value = getattr(obj, field_name, None) # Format the value if hasattr(field_value, 'strftime'): # DateTime fields field_value = field_value.strftime('%Y-%m-%d %H:%M') elif isinstance(field_value, bool): field_value = 'Yes' if field_value else 'No' elif field_value is None: field_value = 'Not set' fields_data.append({ 'name': field_name, 'label': field_label, 'value': field_value }) except: continue context = { 'object': obj, 'model_name': self.model._meta.verbose_name, 'fields_data': fields_data, } template = self.template_name or 'cruder/detail.html' return render(request, template, context)
[docs] def delete_view(self, request, pk): """Display delete confirmation""" obj = get_object_or_404(self.model, pk=pk) # Get field information for template (same as detail view) fields_data = [] for field in self.model._meta.get_fields(): if not field.name.endswith('_set') and field.name != 'id': try: field_name = field.name field_label = getattr(field, 'verbose_name', field_name.replace('_', ' ').title()) field_value = getattr(obj, field_name, None) # Format the value if hasattr(field_value, 'strftime'): # DateTime fields field_value = field_value.strftime('%Y-%m-%d %H:%M') elif isinstance(field_value, bool): field_value = 'Yes' if field_value else 'No' elif field_value is None: field_value = 'Not set' fields_data.append({ 'name': field_name, 'label': field_label, 'value': field_value }) except: continue context = { 'object': obj, 'model_name': self.model._meta.verbose_name, 'fields_data': fields_data, } template = self.template_name or 'cruder/delete.html' return render(request, template, context)
[docs] def delete_post(self, request, pk): """Handle delete confirmation""" obj = get_object_or_404(self.model, pk=pk) obj_name = str(obj) obj.delete() messages.success(request, f"{self.model._meta.verbose_name} '{obj_name}' deleted successfully!") # Redirect back to list view current_path = request.path # For /app/contacts/4/delete/ -> /app/contacts/ if '/delete/' in current_path: # Split on /delete/ and take the first part, then remove the ID base_path = current_path.split('/delete/')[0] # /app/contacts/4 path_parts = base_path.rstrip('/').split('/') # ['', 'app', 'contacts', '4'] if path_parts[-1].isdigit(): # Remove ID if present list_url = '/'.join(path_parts[:-1]) + '/' # /app/contacts/ else: list_url = base_path + '/' else: list_url = current_path if current_path.endswith('/') else current_path + '/' return redirect(list_url)
[docs] def crud_view(model_class, framework='bootstrap', **kwargs): """ Create a complete CRUD view for a Django model with one function call. This function generates a Django view that handles all CRUD operations: Create, Read, Update, Delete, and List with search and pagination. Args: model_class (django.db.models.Model): The Django model to create CRUD operations for. framework (str, optional): CSS framework to use. Defaults to 'bootstrap'. Supported: 'bootstrap', 'bulma'. **kwargs: Additional configuration options: Display Options: exclude_fields (list): Fields to exclude from forms. list_fields (list): Fields to show in list view. readonly_fields (list): Fields that should be read-only in forms. readonly_mode (bool): Make entire interface read-only. Search & Pagination: search_fields (list): Fields to enable search across (OR logic). per_page (int): Items per page for pagination. Default: 25. Permissions: permissions (dict): Role-based permissions mapping. Example: {'C': ['admin'], 'U': ['admin'], 'D': ['admin']} permission_required (str): Django permission required for access. Templates: template_name (str): Custom template to use instead of defaults. Returns: function: A Django view function that handles all CRUD operations. Example: Basic usage: >>> from cruder import crud_view >>> from .models import Contact >>> >>> @login_required >>> def contact_crud(request, pk=None, action='list'): ... return crud_view( ... Contact, ... search_fields=['name', 'email', 'phone'], ... readonly_fields=['created_at'], ... per_page=10 ... )(request, pk, action) With permissions: >>> def secure_crud(request, pk=None, action='list'): ... return crud_view( ... MyModel, ... permissions={ ... 'C': ['admin', 'editor'], ... 'U': ['admin'], ... 'D': ['admin'] ... } ... )(request, pk, action) Note: The returned view function expects three parameters: request, pk (optional), and action (defaults to 'list'). The action parameter determines which CRUD operation to perform: 'list', 'create', 'view', 'edit', or 'delete'. """ class DynamicCRUDView(CRUDView): model = model_class # Set framework and other attributes after class creation DynamicCRUDView.framework = framework # Override any provided kwargs for key, value in kwargs.items(): setattr(DynamicCRUDView, key, value) return DynamicCRUDView.as_view()
def render_crud_list(model_class, request, framework='bootstrap', **kwargs): """ Function to render just the list view Args: model_class: Django model class request: Django request object framework: CSS framework name **kwargs: Additional arguments for render_list Returns: HttpResponse with rendered list """ page = request.GET.get('page', 1) search_query = request.GET.get('search', '') list_data = render_list( model_class=model_class, framework=framework, page=page, search_query=search_query, **kwargs ) context = { 'list_html': list_data['html'], 'page_obj': list_data['page_obj'], 'total_count': list_data['total_count'], 'model_name': model_class._meta.verbose_name, 'model_name_plural': model_class._meta.verbose_name_plural, } return render(request, 'cruder/list.html', context) def render_crud_form(model_class, request, pk=None, framework='bootstrap', **kwargs): """ Function to render just the form view Args: model_class: Django model class request: Django request object pk: Primary key for editing (None for create) framework: CSS framework name **kwargs: Additional arguments for render_form Returns: HttpResponse with rendered form """ instance = None if pk: instance = get_object_or_404(model_class, pk=pk) if request.method == 'POST': form_class = create_model_form(model_class, framework) form = form_class(request.POST, request.FILES, instance=instance) if form.is_valid(): obj = form.save() action = "updated" if instance else "created" messages.success(request, f"{model_class._meta.verbose_name} {action} successfully!") # Redirect back to list view current_path = request.path if '/create/' in current_path: list_url = current_path.replace('/create/', '/') elif current_path.endswith('/create'): list_url = current_path.replace('/create', '/') elif '/edit/' in current_path: list_url = current_path.split('/edit/')[0] + '/' elif '/delete/' in current_path: list_url = current_path.split('/delete/')[0] + '/' else: list_url = current_path if current_path.endswith('/') else current_path + '/' return redirect(list_url) form_html = render_form( model_class=model_class, instance=instance, framework=framework, **kwargs ) context = { 'form_html': form_html, 'object': instance, 'model_name': model_class._meta.verbose_name, 'action': 'Edit' if instance else 'Create', } return render(request, 'cruder/form.html', context)