CRUD Operations¶
This guide explains how Create, Read, Update, and Delete operations work in django-admin-deux through the action-based pattern.
The Action-Based Pattern¶
Unlike traditional admin interfaces where CRUD is baked into views, django-admin-deux implements CRUD through actions. This provides several benefits:
- Consistency: All operations follow the same action pattern
- Extensibility: Easy to add new operations
- Flexibility: Customize or replace any CRUD action
- Discoverability: All operations are visible through the action system
Understanding the CRUD Actions¶
The core plugin provides four default CRUD actions:
| Action | Type | Description | Trigger Location |
|---|---|---|---|
AddAction |
List Action | Create new record | ListView toolbar |
EditAction |
Record Action | Update existing record | Each row in ListView |
DeleteAction |
Record Action | Delete single record | Each row in ListView |
DeleteBulkAction |
Bulk Action | Delete multiple records | ListView with selections |
These actions are automatically available unless you override them.
Create Operations¶
How It Works¶
- User clicks "Add" button in ListView
AddActionredirects to a CreateView- CreateView displays form with
create_fieldsorfields - On submit, model instance is created
- User is redirected back to ListView
Basic Create¶
from djadmin import ModelAdmin, register
@register(Product)
class ProductAdmin(ModelAdmin):
# AddAction is provided by default
# Uses create_fields, falls back to fields
create_fields = ['name', 'sku', 'price', 'category']
Custom Create Form¶
from django import forms
from .models import Product
class ProductCreateForm(forms.ModelForm):
class Meta:
model = Product
fields = ['name', 'sku', 'price', 'category']
widgets = {
'category': forms.Select(attrs={'class': 'select2'}),
}
def clean_sku(self):
sku = self.cleaned_data['sku']
if Product.objects.filter(sku=sku).exists():
raise forms.ValidationError("SKU already exists")
return sku
@register(Product)
class ProductAdmin(ModelAdmin):
create_form_class = ProductCreateForm
Customizing AddAction¶
You can customize the AddAction or replace it entirely:
from djadmin.plugins.core.actions import AddAction
class CustomAddAction(AddAction):
label = 'Create Product'
icon = 'plus-circle'
def get_fields(self):
# Dynamic field selection based on user
if self.request.user.is_superuser:
return '__all__'
return ['name', 'sku', 'price']
@register(Product)
class ProductAdmin(ModelAdmin):
list_actions = [CustomAddAction]
Create Success URL¶
By default, successful creation redirects to the ListView. Customize in AddAction:
class CustomAddAction(AddAction):
def get_success_url(self):
# Redirect to the newly created object's edit page
opts = self.model._meta
return reverse(
f'djadmin:{opts.app_label}_{opts.model_name}_detail',
args=[self.object.pk],
current_app=self.admin_site.name,
)
@register(Product)
class ProductAdmin(ModelAdmin):
list_actions = [CustomAddAction]
Read Operations¶
List View (Read All)¶
The default ListAction provides the "read all" operation:
@register(Product)
class ProductAdmin(ModelAdmin):
list_display = ['name', 'sku', 'price', 'status']
paginate_by = 50
The ListView shows all records with the configured columns, pagination, and actions.
Detail View (Read One)¶
Currently, detail views are implemented through EditAction, which shows the update form. For read-only detail views, you can customize:
from djadmin.actions import BaseAction, RecordActionMixin
from django.views.generic import DetailView
class ViewAction(RecordActionMixin, BaseAction):
label = 'View'
icon = 'eye'
def get_view_class(self):
return DetailView
def get_template_name(self):
opts = self.model._meta
return [
f'djadmin/{opts.app_label}/{opts.model_name}_view.html',
'djadmin/actions/view.html',
]
@register(Product)
class ProductAdmin(ModelAdmin):
record_actions = [ViewAction, EditAction, DeleteAction]
Update Operations¶
How It Works¶
- User clicks "Edit" button in a ListView row
EditActionredirects to an UpdateView- UpdateView displays form with
update_fieldsorfields - On submit, model instance is updated
- User is redirected back to ListView
Basic Update¶
@register(Product)
class ProductAdmin(ModelAdmin):
# EditAction is provided by default
# Uses update_fields, falls back to fields
update_fields = '__all__'
Different Fields for Update¶
@register(Product)
class ProductAdmin(ModelAdmin):
# Simple create form
create_fields = ['name', 'sku', 'price']
# Full update form
update_fields = '__all__'
Custom Update Form¶
class ProductUpdateForm(forms.ModelForm):
class Meta:
model = Product
fields = '__all__'
def clean_stock_quantity(self):
quantity = self.cleaned_data['stock_quantity']
if quantity < 0:
raise forms.ValidationError("Stock cannot be negative")
return quantity
@register(Product)
class ProductAdmin(ModelAdmin):
update_form_class = ProductUpdateForm
Customizing EditAction¶
from djadmin.plugins.core.actions import EditAction
class CustomEditAction(EditAction):
label = 'Modify'
icon = 'edit'
def get_fields(self):
# Different fields based on object state
if self.object.status == 'published':
# Can't edit critical fields after publishing
return ['title', 'content', 'tags']
return '__all__'
@register(Product)
class ProductAdmin(ModelAdmin):
record_actions = [CustomEditAction, DeleteAction]
Update Success URL¶
class CustomEditAction(EditAction):
def get_success_url(self):
# Stay on edit page after save
opts = self.model._meta
return reverse(
f'djadmin:{opts.app_label}_{opts.model_name}_detail',
args=[self.object.pk],
current_app=self.admin_site.name,
)
Delete Operations¶
Single Record Delete¶
The DeleteAction handles deleting individual records:
@register(Product)
class ProductAdmin(ModelAdmin):
# DeleteAction is provided by default
record_actions = [EditAction, DeleteAction]
Flow: 1. User clicks "Delete" button on a row 2. Confirmation page displays 3. User confirms deletion 4. Record is deleted 5. User redirected to ListView
Bulk Delete¶
The DeleteBulkAction handles deleting multiple selected records:
@register(Product)
class ProductAdmin(ModelAdmin):
# DeleteBulkAction is provided by default
bulk_actions = [DeleteBulkAction]
Flow: 1. User selects multiple checkboxes in ListView 2. User clicks "Delete Selected" button 3. Confirmation page shows count and list 4. User confirms deletion 5. Records are deleted in a transaction 6. User redirected to ListView
Customizing Delete Actions¶
from djadmin.plugins.core.actions import DeleteAction, DeleteBulkAction
class SafeDeleteAction(DeleteAction):
label = 'Archive'
icon = 'archive'
# Declarative permission: Only staff can archive
permission_class = IsStaff() & HasDjangoPermission(perm='delete')
def get_template_name(self):
return 'myapp/confirm_archive.html'
# Override the actual deletion to soft-delete
def delete(self, request, *args, **kwargs):
self.object.is_archived = True
self.object.save()
messages.success(request, f"{self.object} archived successfully")
return redirect(self.get_success_url())
@register(Product)
class ProductAdmin(ModelAdmin):
record_actions = [EditAction, SafeDeleteAction]
Delete Confirmation¶
Customize the confirmation message:
class CustomDeleteAction(DeleteAction):
def get_confirmation_message(self, obj=None, queryset=None):
if obj:
return f"Are you sure you want to delete {obj.name}? This cannot be undone."
return "Are you sure?"
Delete Success URL¶
class CustomDeleteAction(DeleteAction):
def get_success_url(self):
# Redirect to dashboard instead of list
return reverse('djadmin:index', current_app=self.admin_site.name)
Form Validation and Error Handling¶
Server-Side Validation¶
Use Django form validation:
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = '__all__'
def clean_price(self):
price = self.cleaned_data['price']
cost = self.cleaned_data.get('cost')
if price <= 0:
raise forms.ValidationError("Price must be positive")
if cost and price < cost:
raise forms.ValidationError("Price cannot be less than cost")
return price
def clean(self):
cleaned_data = super().clean()
status = cleaned_data.get('status')
stock = cleaned_data.get('stock_quantity')
if status == 'active' and stock == 0:
raise forms.ValidationError("Cannot activate product with zero stock")
return cleaned_data
@register(Product)
class ProductAdmin(ModelAdmin):
form_class = ProductForm
Model-Level Validation¶
Validation in model's clean() method is automatically enforced:
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
cost = models.DecimalField(max_digits=10, decimal_places=2)
def clean(self):
if self.price < self.cost:
raise ValidationError({
'price': 'Price cannot be less than cost'
})
Error Display¶
Validation errors are automatically displayed in the form: - Field-specific errors appear next to the field - Form-wide errors appear at the top - Django messages framework shows success/error messages
Custom Error Messages¶
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = '__all__'
error_messages = {
'sku': {
'unique': 'A product with this SKU already exists.',
'required': 'SKU is required for all products.',
},
}
Success Messages¶
Django's messages framework is used for feedback:
from django.contrib import messages
class CustomAddAction(AddAction):
def form_valid(self, form):
response = super().form_valid(form)
messages.success(
self.request,
f"Product '{self.object.name}' created successfully!"
)
return response
Success URL Patterns¶
Common redirection patterns:
Redirect to List (Default)¶
def get_success_url(self):
opts = self.model._meta
return reverse(
f'djadmin:{opts.app_label}_{opts.model_name}_list',
current_app=self.admin_site.name,
)
Redirect to Edit Page¶
def get_success_url(self):
opts = self.model._meta
return reverse(
f'djadmin:{opts.app_label}_{opts.model_name}_detail',
args=[self.object.pk],
current_app=self.admin_site.name,
)
Redirect to Dashboard¶
Conditional Redirect¶
def get_success_url(self):
# Check for 'continue' button in form
if '_continue' in self.request.POST:
opts = self.model._meta
return reverse(
f'djadmin:{opts.app_label}_{opts.model_name}_detail',
args=[self.object.pk],
current_app=self.admin_site.name,
)
# Default: back to list
return super().get_success_url()
Advanced Patterns¶
Multi-Step Creation¶
Create records with related objects:
from django.db import transaction
class ProductWithVariantsForm(forms.ModelForm):
variant_count = forms.IntegerField(min_value=1)
class Meta:
model = Product
fields = ['name', 'sku', 'price']
class ProductWithVariantsAction(AddAction):
def form_valid(self, form):
with transaction.atomic():
# Create main product
self.object = form.save()
# Create variants
count = form.cleaned_data['variant_count']
for i in range(count):
ProductVariant.objects.create(
product=self.object,
name=f"Variant {i+1}",
)
messages.success(
self.request,
f"Created {self.object.name} with {count} variants"
)
return redirect(self.get_success_url())
@register(Product)
class ProductAdmin(ModelAdmin):
list_actions = [ProductWithVariantsAction]
Conditional Actions¶
For state-dependent actions, check state in the execute() method:
class PublishAction(RecordActionMixin, BaseAction):
label = 'Publish'
icon = 'check'
permission_class = IsStaff() & HasDjangoPermission(perm='change')
def execute(self, request, obj, **kwargs):
# Check state at execution time
if obj.status == 'published':
messages.error(request, f"'{obj.title}' is already published")
return redirect(self.get_success_url())
obj.status = 'published'
obj.published_date = timezone.now()
obj.save()
messages.success(request, f"'{obj.title}' published successfully")
return redirect(self.get_success_url())
@register(Post)
class PostAdmin(ModelAdmin):
record_actions = [EditAction, PublishAction, DeleteAction]
Note: For per-record visibility (e.g., showing "Publish" only for drafts), handle this in templates rather than at the action level, as action filtering happens at the list level, not per-record.
Pre-filled Forms¶
Pre-populate create forms:
class DuplicateAction(RecordActionMixin, BaseAction):
label = 'Duplicate'
icon = 'copy'
def execute(self, request, obj, **kwargs):
# Create form pre-filled with object data
form_class = self.model_admin.create_form_class or ProductForm
form = form_class(initial={
'name': f"{obj.name} (Copy)",
'sku': f"{obj.sku}-copy",
'price': obj.price,
'category': obj.category,
})
# Render create template with pre-filled form
context = {
'form': form,
'original': obj,
'opts': self.model._meta,
}
return render(request, 'djadmin/actions/add.html', context)
@register(Product)
class ProductAdmin(ModelAdmin):
record_actions = [EditAction, DuplicateAction, DeleteAction]
See Also¶
- Basic Usage - ModelAdmin configuration overview
- Actions Guide - Complete action system reference
- Customization - Advanced customization techniques
- Plugin Development - Create custom actions