Skip to main content

A useful view class for creating CRUD views in Django (With Tailwind & HTMX & extensible plugin system)

Project description

django-crud-sundae

A useful view class for creating CRUD views in Django (With Tailwind & HTMX). Following the ice-cream metaphor established by Django Neapolitan and Django Vanilla views. This offers a few extras: a banana, squirt of cream and a drizzle of chocolate sauce!

Features

If you appreciate Django's built-in Admin for its CRUD views but need more flexibility to shape it into your application, Sundae might be just what you're looking for..

  • Customisable CRUD views
  • Plugin Sysytem
  • Validation Hooks
  • Permissions
  • Django-filter on list views
  • Bulk actions on list views
  • Plugin support, you can extend sundae view however you want

Via Plugins

  • DataTables.js
  • Quill.js
  • flatpickr.js
  • Select2

Quick Start for Busy People

Want to try it out immediately? Here are the fastest ways to get started:

Option 1: Using UV

UV is a Python package manager. Perfect for quick testing:

# Install UV if you don't have it
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create a new Django project with django-crud-sundae
uv venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install Django and django-crud-sundae
uv pip install django django-crud-sundae django-filter

# Create a test project
django-admin startproject myproject .
cd myproject
python manage.py startapp articles

# Create a simple model in articles/models.py
cat > articles/models.py << 'EOF'
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title
EOF

# Create a view in articles/views.py
cat > articles/views.py << 'EOF'
from sundae.views import CRUDSundaeView
from .models import Article

class ArticleView(CRUDSundaeView):
    model = Article
    lookup_field = 'pk'
    fields = ['title', 'content']
EOF

# Update settings.py to include the apps
# Add 'sundae' and 'articles' to INSTALLED_APPS

# Create URLs
cat > articles/urls.py << 'EOF'
from django.urls import path, include
from .views import ArticleView

urlpatterns = [
    path('', include(ArticleView.get_urls())),
]
EOF

# Run migrations and start server
python manage.py makemigrations
python manage.py migrate
python manage.py runserver

# Visit http://localhost:8000/article/ to see your CRUD interface!

Option 2: Using Docker

For a completely isolated environment with zero setup:

# Clone the repository
git clone https://github.com/leonh/django-crud-sundae.git
cd django-crud-sundae

# Build and run with docker-compose
docker-compose up --build

# Visit http://localhost:8000 for the demo!

That's it! The Docker setup includes:

  • ✅ Complete Django project with sample Article model
  • ✅ Pre-configured CRUD views with search and filtering
  • ✅ Sample data to explore
  • ✅ Admin interface (login: admin/admin)
  • ✅ All dependencies installed

Useful Docker commands:

# Run in background
docker-compose up -d

# View logs
docker-compose logs -f

# Stop the container
docker-compose down

# Access Django shell
docker-compose exec web python manage.py shell

# Create a superuser
docker-compose exec web python manage.py createsuperuser

Option 3: One-Command Demo Script

Use a automated setup script that creates everything for you (read it before running!):

# Download and run the demo script
curl -sSL https://raw.githubusercontent.com/leonh/django-crud-sundae/main/demo.sh | bash

# Or clone and run locally
git clone https://github.com/leonh/django-crud-sundae.git
cd django-crud-sundae
./demo.sh

The script will:

  • ✅ Create a virtual environment
  • ✅ Install all dependencies
  • ✅ Set up a complete Django project
  • ✅ Create sample models and views
  • ✅ Generate sample data
  • ✅ Create an admin user (admin/admin)

Just follow the printed instructions to start the server!

Regular Installation (pip)

Install from PyPI (once published):

pip install django-crud-sundae

Or install from source:

git clone https://github.com/leonh/django-crud-sundae.git
cd django-crud-sundae
pip install -e .

Quick Start

  1. Add sundae to your INSTALLED_APPS in settings.py:
INSTALLED_APPS = [
    ...
    'sundae',
    ...
]
  1. Use CRUDSundae in your Django app:
from sundae.views import CRUDSundaeView
from .models import Article

class ArticleView(CRUDSundaeView):
    model = Article
    lookup_field = 'pk'  # Use standard primary keys (recommended for most users)
    fields = ['title', 'content', 'author']

Note: The default lookup_field is 'sqid' which requires django-sqids. For a simpler setup without extra dependencies, use lookup_field = 'pk' as shown above.

  1. Wire up your URLs (automatic URL generation):
from django.urls import path, include
from .views import ArticleView

urlpatterns = [
    path('', include(ArticleView.get_urls())),
]

This automatically generates all CRUD URLs:

  • /article/ - List articles
  • /article/create/ - Create article
  • /article/<int:pk>/ - View article detail
  • /article/<int:pk>/update/ - Edit article
  • /article/<int:pk>/delete/ - Delete article

How CRUDSundae Works

CRUDSundae provides a single base view class (CRUDSundaeView) that handles all CRUD operations through a unified interface. Instead of creating separate view classes for list, create, update, and delete operations, you define one view class and CRUDSundae automatically generates all the necessary URL patterns and view handlers.

Core Concepts

  1. Single View Class: Define your model and fields once, get complete CRUD functionality
  2. Automatic URL Generation: URLs are automatically generated following the pattern {model-name}-{action} (e.g., article-list, article-create)
  3. Convention-Based Templates: Templates are resolved automatically based on your model name, with fallback to default templates
  4. Decorator-Based Actions: Add custom actions using simple @action and @bulk_action decorators

URL Structure

When you call MyView.get_urls(), CRUDSundae automatically generates these URL patterns:

Using standard primary keys (default Django, no extra dependencies):

  • {model}/ - List view
  • {model}/create/ - Create view
  • {model}/<int:pk>/ - Detail view
  • {model}/<int:pk>/update/ - Update view
  • {model}/<int:pk>/delete/ - Delete view
  • {model}/bulk-update/ - Bulk action processing

Using sqids (optional, requires django-sqids for URL-safe IDs):

  • {model}/ - List view
  • {model}/create/ - Create view
  • {model}/<slug:sqid>/ - Detail view
  • {model}/<slug:sqid>/update/ - Update view
  • {model}/<slug:sqid>/delete/ - Delete view
  • {model}/bulk-update/ - Bulk action processing

The lookup field is configured via the lookup_field attribute (default: 'sqid'). Set to 'pk' to use primary keys instead.

Template Resolution

Templates are resolved in this order:

  1. Explicitly defined template_name
  2. {app_label}/{model_name}{suffix}.html (e.g., articles/article_list.html)
  3. sundae/object{suffix}.html (default fallback templates)

Features

Core CRUD Operations

  • List View: Paginated listing with search and filter support
  • Create View: Form-based object creation with validation
  • Detail View: Single object display
  • Update View: Form-based object editing
  • Delete View: Confirmation-based deletion

Search & Filtering

  • Full-Text Search: Search across multiple fields using search_fields attribute
  • Django-Filter Integration: Advanced filtering using django-filter library
  • Active Filter Display: Shows currently applied filters with remove links
  • Preserved Query Strings: Filters are preserved across pagination
class ArticleListView(CRUDSundaeView):
    model = Article
    search_fields = ['title', 'content', 'author__name']  # Search across these fields
    filterset_fields = ['status', 'category']  # Enable filtering

Pagination

  • Automatic Pagination: Configure with paginate_by attribute
  • Filter Preservation: Pagination links preserve search and filter parameters
  • Customizable: Override get_paginator() for custom pagination logic

Bulk Actions

Register bulk actions using the @bulk_action decorator to operate on multiple selected objects:

@bulk_action(display_name="Archive Selected", confirmation_required=True)
def archive_selected(self, request, queryset):
    queryset.update(archived=True)
    return len(queryset), "archived"

@bulk_action(display_name="Delete Selected", use_transaction=True)
def delete_selected(self, request, queryset):
    count = queryset.count()
    queryset.delete()
    return count, "deleted"

Features:

  • Optional confirmation dialogs
  • Permission checks
  • Transaction support (all-or-nothing operations)
  • Automatic success/error messaging
  • Logging integration

Custom Actions

Add custom actions using the @action decorator:

# Detail action (operates on single object)
@action(detail=True, url_path="approve", permission_required="myapp.approve_article")
def approve_item(self, request, pk):  # Parameter name matches lookup_field
    obj = self.get_object()
    obj.approved = True
    obj.save()
    messages.success(request, f"{obj} has been approved!")
    return HttpResponseRedirect(self.get_list_url())

# List action (operates on list view)
@action(detail=False, url_path="export", methods=["GET"])
def export_list(self, request):
    # Export logic...
    return HttpResponse(csv_data, content_type='text/csv')

Note: The URL parameter name in your action method should match your lookup_field setting (e.g., pk, sqid, slug).

Validation Hooks

CRUDSundae provides comprehensive hooks for customizing behavior at every stage:

class ArticleView(CRUDSundaeView):
    model = Article

    def clean_object(self, obj):
        """Custom validation before saving"""
        if obj.publish_date < obj.created_date:
            raise ValidationError("Publish date cannot be before creation date")

    def before_save(self, form):
        """Called before both create and update"""
        form.instance.modified_by = self.request.user

    def after_save(self, obj, created):
        """Called after both create and update"""
        action = "created" if created else "updated"
        self.logger.info(f"Article {obj.pk} was {action}")

    def before_create(self, form):
        """Called only before creating new objects"""
        form.instance.author = self.request.user

    def after_create(self, obj):
        """Called only after creating new objects"""
        send_notification_email(obj)

HTMX Integration

Built-in support for HTMX dynamic interactions:

  • Automatic Detection: Detects HTMX requests via HX-Request header
  • Client-Side Redirects: htmx_redirect() for seamless navigation
  • Page Refresh: htmx_refresh() to reload current page
  • Custom Events: htmx_trigger() to fire client-side events
  • Partial Rendering: Returns partial templates for HTMX requests
# In your view methods
if self.is_htmx_request():
    headers = self.htmx_redirect(self.get_success_url())
    return HttpResponse(status=204, headers=headers)

Permission & Authentication

Flexible permission system with multiple levels:

class ArticleView(CRUDSundaeView):
    model = Article
    login_required = True  # Require authentication
    permission_required = ['articles.view_article']  # Global permission

    # Per-action permissions
    create_permission_required = ['articles.add_article']
    update_permission_required = ['articles.change_article']
    delete_permission_required = ['articles.delete_article']

    # Custom permission logic
    def has_permission(self):
        if not super().has_permission():
            return False
        # Add custom checks
        return self.request.user.is_staff

Error Handling

Comprehensive error handling with user-friendly messages:

  • Database Errors: IntegrityError, OperationalError automatically caught
  • Validation Errors: Form and model validation errors displayed
  • Permission Errors: PermissionDenied handled gracefully
  • User-Friendly Messages: Technical errors converted to readable messages
  • Logging: All errors logged with context

Success Messages

Automatic success messages for all operations:

class ArticleView(CRUDSundaeView):
    model = Article
    success_message_create = "{verbose_name} '{obj}' was created successfully!"
    success_message_update = "{verbose_name} was updated successfully."
    success_message_delete = "{verbose_name} was deleted successfully."
    enable_success_messages = True  # Enable/disable globally

Tailwind CSS & Styling

  • Pre-built Templates: Default templates styled with Tailwind CSS
  • Customizable: Override templates or extend base templates
  • Responsive: Mobile-friendly out of the box
  • Accessible: ARIA labels and semantic HTML

Additional Features

  • Field-Level Customization: Specify different fields for list, create, update views
  • Context Object Names: Automatic or custom context variable names
  • Logging Integration: Built-in logger for all view operations
  • List Display Links: Configure which fields link to detail view
  • Empty State Handling: Customizable empty state with allow_empty
  • Widget Customization: Override form widgets per field

Complete Usage Example

Standard Example (Using Primary Keys)

Here's a complete example using standard Django primary keys (no extra dependencies):

# models.py
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    status = models.CharField(max_length=20, choices=[
        ('draft', 'Draft'),
        ('published', 'Published'),
        ('archived', 'Archived')
    ])
    category = models.CharField(max_length=50)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title
# views.py
from sundae.views import CRUDSundaeView, action, bulk_action
from django.contrib import messages
from django.http import HttpResponseRedirect
from .models import Article

class ArticleView(CRUDSundaeView):
    model = Article
    lookup_field = 'pk'  # Use primary key for URLs (default is 'sqid')

    # Field configuration
    fields = ['title', 'content', 'category', 'status']
    list_fields = ['title', 'author', 'status', 'created_at']
    search_fields = ['title', 'content', 'author__username']

    # Pagination and filtering
    paginate_by = 20
    filterset_fields = ['status', 'category']

    # Permissions
    login_required = True
    create_permission_required = ['articles.add_article']

    # Validation hooks
    def clean_object(self, obj):
        if obj.status == 'published' and not obj.content:
            raise ValidationError("Published articles must have content")

    def before_create(self, form):
        form.instance.author = self.request.user

    def after_create(self, obj):
        messages.info(self.request, f"New article '{obj.title}' created!")

    # Custom action for single objects
    @action(detail=True, url_path="publish", permission_required="articles.publish_article")
    def publish_article(self, request, pk):
        article = self.get_object()
        article.status = 'published'
        article.save()
        messages.success(request, f"'{article.title}' has been published!")
        return HttpResponseRedirect(self.get_list_url())

    # Bulk action for multiple objects
    @bulk_action(display_name="Archive Selected", confirmation_required=True)
    def archive_selected(self, request, queryset):
        queryset.update(status='archived')
        return len(queryset), "archived"

    # Enable bulk actions
    bulk_edit_actions = ['archive_selected']
# urls.py
from django.urls import path, include
from .views import ArticleView

urlpatterns = [
    path('', include(ArticleView.get_urls())),
]

This generates the following URLs automatically:

  • /article/ - List all articles (with search and filters)
  • /article/create/ - Create new article
  • /article/<int:pk>/ - View article detail
  • /article/<int:pk>/update/ - Edit article
  • /article/<int:pk>/delete/ - Delete article (with confirmation)
  • /article/<int:pk>/publish/ - Custom publish action
  • /article/bulk-update/ - Process bulk actions

Alternative Example (Using Sqids)

If you want obfuscated, URL-safe IDs instead of sequential integers:

pip install django-sqids
# models.py
from django.db import models
from django_sqids import SqidsField

class Article(models.Model):
    sqid = SqidsField(real_field_name='id')  # Add sqid field
    title = models.CharField(max_length=200)
    content = models.TextField()
    # ... other fields ...
# views.py
class ArticleView(CRUDSundaeView):
    model = Article
    lookup_field = 'sqid'  # Use sqid for URLs (this is the default)

    # ... rest of configuration ...

    @action(detail=True, url_path="publish")
    def publish_article(self, request, sqid):  # Parameter matches lookup_field
        article = self.get_object()
        # ... action logic ...

This generates URLs with sqids:

  • /article/ - List all articles
  • /article/create/ - Create new article
  • /article/<slug:sqid>/ - View article detail (e.g., /article/abc123/)
  • /article/<slug:sqid>/update/ - Edit article
  • /article/<slug:sqid>/delete/ - Delete article
  • /article/<slug:sqid>/publish/ - Custom publish action

Benefits of sqids: Non-sequential IDs, URL-safe, obfuscated from users, prevents enumeration attacks.

Available Views

For those who prefer separate view classes for each action, CRUDSundae also provides individual view classes:

  • SundaeListView - For listing objects
  • SundaeDetailView - For displaying a single object
  • SundaeCreateView - For creating new objects
  • SundaeUpdateView - For updating existing objects
  • SundaeDeleteView - For deleting objects

Note: Using CRUDSundaeView with get_urls() is the recommended approach as it provides the complete feature set including custom actions and bulk operations.

Examples

Check out the examples/ directory for more detailed usage examples.

Requirements

Core Requirements

  • Python 3.8+: Minimum Python version
  • Django 3.2+: Compatible with Django 3.2, 4.0, 4.1, 4.2, and 5.0+
  • django-filter 2.0+: Required dependency for filtering functionality
  • django-widget-tweaks: optional used in the example demo All core requirements are automatically installed when you install django-crud-sundae.

Optional Dependencies

  • HTMX: For dynamic, AJAX-like interactions without writing JavaScript

    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
    
  • Tailwind CSS: For styling the default templates

    <script src="https://cdn.tailwindcss.com"></script>
    
  • django-sqids: Only required if you set lookup_field = 'sqid' for obfuscated, URL-safe IDs

    pip install django-sqids
    

    Note: Most users can skip this and use lookup_field = 'pk' instead (standard Django primary keys)

Configuration Requirements

Add sundae to your INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    'sundae',
    'django_filters',  # If using filters
    # ...
]

Model Requirements

CRUDSundae works with any Django model, but certain features have specific requirements:

  1. Lookup Field: Configure which field to use for URL object lookups via lookup_field:

    • 'pk' (recommended for most users): Uses Django's standard primary key - no extra setup required
    • 'sqid' (default in CRUDSundaeView): Requires adding django-sqids and a sqid field to your model - provides URL-safe, obfuscated IDs
    • 'slug', 'uuid', or any unique field: Use any unique model field for URLs

    Example configurations:

    # Using primary key (most common, no dependencies)
    class ArticleView(CRUDSundaeView):
        model = Article
        lookup_field = 'pk'
    
    # Using slug
    class ArticleView(CRUDSundaeView):
        model = Article
        lookup_field = 'slug'
    
    # Using sqid (requires django-sqids)
    class ArticleView(CRUDSundaeView):
        model = Article
        lookup_field = 'sqid'  # This is the default
    
  2. String Representation: Define __str__() method for readable object names in messages and lists

  3. Permissions: If using permission-based access, ensure your model has appropriate permissions defined

Template Requirements

CRUDSundae provides default templates that work out of the box, but you can customize by creating templates in your app:

your_app/
├── templates/
│   └── your_app/
│       ├── modelname_list.html
│       ├── modelname_form.html
│       ├── modelname_detail.html
│       └── modelname_confirm_delete.html

Templates can extend sundae/base.html or your own base template.

Configuration Reference

Essential Attributes

class MyView(CRUDSundaeView):
    # Model configuration
    model = MyModel                    # Required: The Django model to use
    queryset = MyModel.objects.all()   # Optional: Custom base queryset

    # Field configuration
    fields = ['field1', 'field2']      # Fields to show in forms
    list_fields = ['field1', 'field3'] # Fields to show in list view
    create_fields = ['field1']         # Fields for create form (overrides fields)
    update_fields = ['field1']         # Fields for update form (overrides fields)

    # Search and filtering
    search_fields = ['title', 'content__icontains']  # Fields to search
    filterset_fields = ['status', 'category']        # Fields to filter on
    filterset_class = MyFilterSet                    # Custom FilterSet class

    # Pagination
    paginate_by = 25                   # Items per page (None = no pagination)
    allow_empty = True                 # Allow empty list views

    # Object lookup
    lookup_field = 'pk'                # Field to use for URL lookups
                                       # Common values: 'pk', 'slug', 'uuid', 'sqid'
                                       # Default: 'sqid' (requires django-sqids)
    lookup_url_kwarg = 'pk'            # URL parameter name (usually matches lookup_field)

    # Templates
    template_name = 'my_template.html' # Override template
    context_object_name = 'article'    # Context variable name

    # Permissions
    login_required = True              # Require authentication
    permission_required = ['app.view_model']  # Global permissions
    create_permission_required = ['app.add_model']
    update_permission_required = ['app.change_model']
    delete_permission_required = ['app.delete_model']
    raise_exception = False            # Raise PermissionDenied vs redirect

    # Actions
    list_item_actions = ['update', 'delete', 'detail']  # Actions shown per item
    bulk_edit_actions = ['delete_selected']             # Enabled bulk actions
    excluded_actions = ['detail']                       # Actions to exclude

    # Messages
    enable_success_messages = True
    success_message_create = "{verbose_name} was created successfully."
    success_message_update = "{verbose_name} was updated successfully."
    success_message_delete = "{verbose_name} was deleted successfully."

    # HTMX
    enable_htmx_support = True         # Enable HTMX integration

Validation Hooks (in execution order)

def clean_object(self, obj):           # Custom validation before save
def before_save(self, form):           # Called before create AND update
def before_create(self, form):         # Called only before create
def before_update(self, form):         # Called only before update
# --- Object is saved to database ---
def after_create(self, obj):           # Called only after create
def after_update(self, obj):           # Called only after update
def after_save(self, obj, created):    # Called after create AND update
def before_delete(self, obj):          # Called before delete
# --- Object is deleted from database ---
def after_delete(self, obj_id):        # Called after delete

Custom Methods to Override

def get_queryset(self):                # Customize base queryset
def get_context_data(self, **kwargs):  # Add custom context variables
def get_form_class(self):              # Use custom form class
def get_success_url(self):             # Customize redirect after success
def get_template_names(self):          # Customize template resolution
def has_permission(self):              # Custom permission logic

License

MIT License - see LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_crud_sundae-0.2.0.tar.gz (79.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_crud_sundae-0.2.0-py3-none-any.whl (58.7 kB view details)

Uploaded Python 3

File details

Details for the file django_crud_sundae-0.2.0.tar.gz.

File metadata

  • Download URL: django_crud_sundae-0.2.0.tar.gz
  • Upload date:
  • Size: 79.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for django_crud_sundae-0.2.0.tar.gz
Algorithm Hash digest
SHA256 0109189e6625a7f7c2c8e8ed16488b650fa116de39f9d925ed8979c9a63f96a0
MD5 636b916f29940000bb88b5c2dfff7d20
BLAKE2b-256 67957d699d4690d8839263b6b8979be3bb96c303cdcf942557a87bb1fa8929cd

See more details on using hashes here.

File details

Details for the file django_crud_sundae-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_crud_sundae-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9e80d38aca8dfc7bcfa2ba8d7de8c8896ee6f353193ee20d3704cf6b85a87567
MD5 fd5279ffe9093f7ba05b4decd4c7d3dc
BLAKE2b-256 91a5c957ed552c1d3f207127b6ddbb4ffe9c7f78f1ab171742569dddd8b0e9df

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page