A Django package for easy integration of jqGrid with automatic configuration, comprehensive CRUD operations, and advanced features. This package makes it trivial to add powerful, interactive data grids to your Django applications with minimal code.
- βοΈ Auto-configuration - Automatically discovers and configures Django models
- π Full CRUD Support - Create, Read, Update, Delete operations out of the box
- π Advanced Filtering - Built-in search and filtering capabilities
- π Import/Export - Excel and CSV import/export functionality
- π¨ Highly Customizable - Extensive configuration options
- β‘ Performance Optimized - Query optimization and caching support
- π Security - CSRF protection and field-level permissions
- ποΈ Multi-database - Support for multiple databases
- π± Responsive - Mobile-friendly grid layouts
- π§ DRY Principle - Reusable components and mixins
- Installation
- Quick Start
- Configuration
- Basic Usage
- Advanced Usage
- Customization
- API Reference
- Examples
- Contributing
- License
pip install django-jqgrid
poetry add django-jqgrid
git clone https://github.com/coder-aniket/django-jqgrid.git
cd django-jqgrid
pip install -e .
- Add
django_jqgrid
to yourINSTALLED_APPS
:
INSTALLED_APPS = [
...
'django_jqgrid',
...
]
- Include the URLconf in your project:
# urls.py
from django.urls import path, include
urlpatterns = [
...
path('jqgrid/', include('django_jqgrid.urls')),
...
]
- Run the auto-discovery command:
python manage.py discover_models
- Add the grid to your template:
{% load jqgrid_tags %}
<!DOCTYPE html>
<html>
<head>
{% jqgrid_css %}
</head>
<body>
{% jqgrid_render 'MyModel' %}
{% jqgrid_js %}
</body>
</html>
That's it! You now have a fully functional data grid with CRUD operations.
Add to your settings.py
:
JQGRID_CONFIG = {
'DEFAULT_ROWS_PER_PAGE': 20,
'ENABLE_EXCEL_EXPORT': True,
'ENABLE_CSV_EXPORT': True,
'ENABLE_FILTERING': True,
'ENABLE_CRUD_OPERATIONS': True,
'DATE_FORMAT': '%Y-%m-%d',
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S',
'DECIMAL_PLACES': 2,
'THOUSAND_SEPARATOR': ',',
'CACHE_TIMEOUT': 300, # seconds
'USE_CACHE': True,
}
from django.db import models
from django_jqgrid.mixins import JQGridMixin
class Product(JQGridMixin, models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
class JQGridMeta:
# Grid configuration
grid_config = {
'caption': 'Product Management',
'height': 'auto',
'autowidth': True,
'rownumbers': True,
'sortname': 'name',
'sortorder': 'asc',
}
# Column configuration
column_config = {
'name': {
'label': 'Product Name',
'width': 200,
'searchable': True,
'editable': True,
},
'price': {
'label': 'Price',
'width': 100,
'formatter': 'currency',
'align': 'right',
},
'stock': {
'label': 'Stock Quantity',
'width': 120,
'formatter': 'integer',
'align': 'center',
},
}
# Fields to exclude from grid
exclude_fields = ['id', 'created_at']
# Enable features
enable_excel_export = True
enable_csv_export = True
enable_crud = True
enable_search = True
# views.py
from django.shortcuts import render
from django_jqgrid.views import JQGridView
from .models import Product
class ProductGridView(JQGridView):
model = Product
template_name = 'products/grid.html'
<!-- products/grid.html -->
{% extends "base.html" %}
{% load jqgrid_tags %}
{% block content %}
<h1>Products</h1>
{% jqgrid_render model="Product" %}
{% endblock %}
{% block extra_js %}
{% jqgrid_js %}
{% endblock %}
# urls.py
from django.urls import path
from .views import ProductGridView
urlpatterns = [
path('products/', ProductGridView.as_view(), name='product-grid'),
]
from django_jqgrid.views import JQGridView
from django.http import JsonResponse
class ProductGridView(JQGridView):
model = Product
def custom_action(self, request, pk):
"""Custom action for grid rows"""
product = self.get_object(pk)
# Your custom logic here
return JsonResponse({'success': True})
def get_grid_config(self):
config = super().get_grid_config()
config.update({
'custom_actions': [
{
'name': 'custom_action',
'label': 'Custom Action',
'icon': 'fa-star',
'callback': 'customActionCallback'
}
]
})
return config
from django_jqgrid.filters import JQGridFilter
class PriceRangeFilter(JQGridFilter):
def filter_queryset(self, queryset, value):
if '-' in value:
min_price, max_price = value.split('-')
return queryset.filter(
price__gte=min_price,
price__lte=max_price
)
return queryset
class ProductGridView(JQGridView):
model = Product
custom_filters = {
'price_range': PriceRangeFilter()
}
class ProductGridView(JQGridView):
model = Product
enable_bulk_operations = True
def bulk_update_stock(self, request, selected_ids):
"""Bulk update stock for selected products"""
new_stock = request.POST.get('new_stock')
Product.objects.filter(id__in=selected_ids).update(stock=new_stock)
return JsonResponse({'success': True, 'updated': len(selected_ids)})
def get_bulk_actions(self):
return [
{
'name': 'bulk_update_stock',
'label': 'Update Stock',
'icon': 'fa-boxes',
'form_fields': [
{'name': 'new_stock', 'type': 'number', 'label': 'New Stock'}
]
}
]
class ProductGridView(JQGridView):
model = Product
def get_column_config(self):
config = super().get_column_config()
# Dynamic column visibility based on user permissions
if not self.request.user.has_perm('products.view_price'):
config['price']['hidden'] = True
# Add computed columns
config['total_value'] = {
'label': 'Total Value',
'formatter': 'currency',
'computed': True,
'formula': 'price * stock'
}
return config
Create custom templates for different parts of the grid:
<!-- templates/jqgrid/custom_grid.html -->
{% extends "django_jqgrid/grid_base.html" %}
{% block grid_toolbar %}
<div class="custom-toolbar">
<button id="custom-button">Custom Action</button>
</div>
{{ block.super }}
{% endblock %}
{% block grid_footer %}
<div class="custom-footer">
Custom footer content
</div>
{% endblock %}
// static/js/grid_callbacks.js
function customActionCallback(rowId) {
$.ajax({
url: '/products/custom-action/' + rowId + '/',
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
success: function(response) {
$('#grid').trigger('reloadGrid');
showNotification('Action completed successfully');
}
});
}
// Grid event handlers
$(document).on('jqGrid:afterLoad', function(e, gridId) {
console.log('Grid loaded:', gridId);
});
$(document).on('jqGrid:beforeSave', function(e, rowData) {
// Validate before saving
if (rowData.price < 0) {
e.preventDefault();
alert('Price cannot be negative');
}
});
/* static/css/custom_grid.css */
.ui-jqgrid {
font-family: 'Roboto', sans-serif;
}
.ui-jqgrid-htable th {
background-color: #2c3e50;
color: white;
}
.ui-jqgrid-bdiv tr:hover {
background-color: #ecf0f1;
}
/* Custom cell styling */
.grid-cell-warning {
background-color: #f39c12 !important;
color: white;
}
.grid-cell-danger {
background-color: #e74c3c !important;
color: white;
}
Base view for rendering jqGrid.
Attributes:
model
- Django model classtemplate_name
- Template to renderpaginate_by
- Number of rows per pageenable_export
- Enable/disable export functionalityenable_crud
- Enable/disable CRUD operations
Methods:
get_queryset()
- Returns the base querysetget_grid_config()
- Returns grid configurationget_column_config()
- Returns column configurationprocess_grid_request()
- Handles AJAX grid requests
REST API view for grid data.
Methods:
list()
- GET method for retrieving grid datacreate()
- POST method for creating recordsupdate()
- PUT method for updating recordsdestroy()
- DELETE method for deleting records
Renders the complete grid.
{% jqgrid_render model="ModelName" config=grid_config %}
Parameters:
model
- Model name or instanceconfig
- Optional configuration dictionaryheight
- Grid height (default: 'auto')width
- Grid width (default: 'auto')
Includes required CSS files.
{% jqgrid_css theme="bootstrap4" %}
Includes required JavaScript files.
{% jqgrid_js include_locale=True %}
Model mixin that adds jqGrid functionality.
Class Attributes:
JQGridMeta
- Configuration class
Methods:
get_grid_data()
- Returns formatted data for gridget_display_value()
- Returns display value for field
Mixin for queryset optimization.
Methods:
optimized_for_grid()
- Returns optimized querysetwith_annotations()
- Adds grid-specific annotations
# models.py
from django.db import models
from django_jqgrid.mixins import JQGridMixin
class Order(JQGridMixin, models.Model):
order_number = models.CharField(max_length=20, unique=True)
customer_name = models.CharField(max_length=100)
product = models.ForeignKey('Product', on_delete=models.CASCADE)
quantity = models.IntegerField()
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, choices=[
('pending', 'Pending'),
('processing', 'Processing'),
('shipped', 'Shipped'),
('delivered', 'Delivered'),
])
created_at = models.DateTimeField(auto_now_add=True)
class JQGridMeta:
grid_config = {
'caption': 'Order Management',
'multiselect': True,
'multiboxonly': True,
}
column_config = {
'order_number': {
'label': 'Order #',
'width': 120,
'frozen': True,
},
'customer_name': {
'label': 'Customer',
'width': 200,
},
'status': {
'label': 'Status',
'width': 100,
'formatter': 'select',
'stype': 'select',
'searchoptions': {
'value': ':All;pending:Pending;processing:Processing;shipped:Shipped;delivered:Delivered'
}
},
'total_amount': {
'label': 'Total',
'width': 100,
'formatter': 'currency',
'align': 'right',
}
}
# views.py
from django_jqgrid.views import JQGridView
from django.db.models import Q
class OrderGridView(JQGridView):
model = Order
template_name = 'orders/list.html'
def get_queryset(self):
queryset = super().get_queryset()
# Add custom filtering
if self.request.GET.get('status'):
queryset = queryset.filter(status=self.request.GET['status'])
return queryset.select_related('product')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['status_choices'] = Order._meta.get_field('status').choices
return context
# templates/orders/list.html
{% extends "base.html" %}
{% load jqgrid_tags %}
{% block content %}
<div class="container-fluid">
<h1>Orders</h1>
<!-- Status Filter -->
<div class="mb-3">
<select id="status-filter" class="form-control">
<option value="">All Status</option>
{% for value, label in status_choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<!-- Grid -->
{% jqgrid_render model="Order" %}
<!-- Custom Buttons -->
<div class="mt-3">
<button id="export-selected" class="btn btn-primary">Export Selected</button>
<button id="bulk-update-status" class="btn btn-warning">Update Status</button>
</div>
</div>
{% endblock %}
{% block extra_js %}
{% jqgrid_js %}
<script>
$(document).ready(function() {
// Status filter
$('#status-filter').on('change', function() {
var status = $(this).val();
$('#grid').jqGrid('setGridParam', {
postData: { status: status }
}).trigger('reloadGrid');
});
// Export selected rows
$('#export-selected').on('click', function() {
var selectedRows = $('#grid').jqGrid('getGridParam', 'selarrrow');
if (selectedRows.length === 0) {
alert('Please select rows to export');
return;
}
window.location.href = '/orders/export/?ids=' + selectedRows.join(',');
});
// Bulk update status
$('#bulk-update-status').on('click', function() {
var selectedRows = $('#grid').jqGrid('getGridParam', 'selarrrow');
if (selectedRows.length === 0) {
alert('Please select rows to update');
return;
}
var newStatus = prompt('Enter new status (pending/processing/shipped/delivered):');
if (newStatus) {
$.ajax({
url: '/orders/bulk-update-status/',
method: 'POST',
data: {
ids: selectedRows,
status: newStatus,
csrfmiddlewaretoken: '{{ csrf_token }}'
},
success: function(response) {
$('#grid').trigger('reloadGrid');
alert('Updated ' + response.updated + ' orders');
}
});
}
});
});
</script>
{% endblock %}
# views.py
from django_jqgrid.views import JQGridView
class MultiDBGridView(JQGridView):
model = Product
using = 'warehouse_db' # Specify database
def get_queryset(self):
# Use specific database
return self.model.objects.using(self.using).all()
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'main_db',
# ... other settings
},
'warehouse_db': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'warehouse',
# ... other settings
}
}
# models.py
class Product(JQGridMixin, models.Model):
name = models.CharField(max_length=100)
stock = models.IntegerField()
class JQGridMeta:
column_config = {
'stock': {
'label': 'Stock',
'width': 100,
'cellattr': 'stockCellAttr',
'formatter': 'stockFormatter'
}
}
# In your template
{% block extra_js %}
<script>
function stockFormatter(cellvalue, options, rowObject) {
if (cellvalue < 10) {
return '<span class="text-danger font-weight-bold">' + cellvalue + '</span>';
} else if (cellvalue < 50) {
return '<span class="text-warning">' + cellvalue + '</span>';
}
return '<span class="text-success">' + cellvalue + '</span>';
}
function stockCellAttr(rowId, val, rawObject, cm, rdata) {
if (parseInt(val) < 10) {
return 'class="bg-danger text-white"';
}
return '';
}
</script>
{% endblock %}
from django_jqgrid.mixins import JQGridOptimizedMixin
class OptimizedProductView(JQGridOptimizedMixin, JQGridView):
model = Product
# Specify related fields to prefetch
prefetch_related = ['category', 'tags']
select_related = ['manufacturer']
# Only load necessary fields
only_fields = ['id', 'name', 'price', 'stock', 'category__name']
# Add database indexes
class Meta:
indexes = [
models.Index(fields=['name', 'price']),
models.Index(fields=['stock']),
]
from django.core.cache import cache
from django_jqgrid.views import JQGridView
class CachedGridView(JQGridView):
model = Product
cache_timeout = 300 # 5 minutes
def get_grid_data(self, request):
cache_key = f'grid_data_{self.model.__name__}_{request.GET.urlencode()}'
data = cache.get(cache_key)
if data is None:
data = super().get_grid_data(request)
cache.set(cache_key, data, self.cache_timeout)
return data
class SecureGridView(JQGridView):
model = Product
def get_column_config(self):
config = super().get_column_config()
user = self.request.user
# Hide sensitive fields based on permissions
if not user.has_perm('products.view_cost'):
config['cost']['hidden'] = True
# Make fields read-only based on permissions
if not user.has_perm('products.change_price'):
config['price']['editable'] = False
return config
def check_crud_permission(self, action):
"""Check if user has permission for CRUD action"""
permission_map = {
'create': 'products.add_product',
'update': 'products.change_product',
'delete': 'products.delete_product',
}
return self.request.user.has_perm(permission_map.get(action, ''))
All AJAX requests include CSRF protection by default. Custom implementations:
// Ensure CSRF token is included in all AJAX requests
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!(/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type)) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});
-
Grid not loading data
- Check browser console for JavaScript errors
- Verify URLs are correctly configured
- Check Django debug toolbar for query issues
-
Export not working
- Ensure
django_jqgrid
is in INSTALLED_APPS - Check that
MEDIA_ROOT
is configured - Verify user has export permissions
- Ensure
-
Editing not saving
- Check CSRF token is included
- Verify model has proper permissions
- Check for validation errors in response
Enable debug mode for detailed logging:
# settings.py
JQGRID_CONFIG = {
'DEBUG': True,
'LOG_LEVEL': 'DEBUG',
}
# views.py
import logging
logger = logging.getLogger('django_jqgrid')
class DebugGridView(JQGridView):
model = Product
def get_grid_data(self, request):
logger.debug(f"Grid request: {request.GET}")
data = super().get_grid_data(request)
logger.debug(f"Returning {len(data['rows'])} rows")
return data
We welcome contributions! Please see our Contributing Guide for details.
# Clone the repository
git clone https://github.com/coder-aniket/django-jqgrid.git
cd django-jqgrid
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install development dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run linting
flake8
black .
isort .
# Run all tests
pytest
# Run with coverage
pytest --cov=django_jqgrid
# Run specific test file
pytest tests/test_views.py
# Run with verbose output
pytest -v
This project is licensed under the MIT License - see the LICENSE file for details.
- Built on top of the excellent jqGrid library
- Inspired by Django's admin interface
- Thanks to all our contributors
- π§ Email: coder.aniketp@gmail.com
- π Issues: GitHub Issues
- π Documentation: Read the Docs
- π¬ Discussions: GitHub Discussions
Made with β€οΈ by the Django JQGrid team