django-admin-query-executor 1.2.0
Execute Django ORM queries directly from the admin interface
License: MIT
Requires Python: >=3.8
Keywords: django, admin, query, executor, orm, database, django-admin

# Django Admin Query Executor

A powerful Django admin extension that allows you to execute Django ORM queries directly from the admin interface. It supports complex queries including `Q()` objects, annotations, aggregations, and all standard Django ORM methods.

![Django Admin Query Executor](https://img.shields.io/badge/django-%3E%3D3.2-green.svg)
![Python Support](https://img.shields.io/badge/python-%3E%3D3.8-blue.svg)
![License](https://img.shields.io/badge/license-MIT-orange.svg)

<img width="1350" height="600" alt="Screenshot 2025-07-17 at 2 42 18 PM" src="https://github.com/user-attachments/assets/03e109d3-92db-4c9b-8df8-b0c1fedc0b32" />

## Features

- **Direct Query Execution**: Execute Django ORM queries directly from the admin changelist view
- **Seamless Integration**: Query results replace the standard admin list view
- **Full Django ORM Support**: Use `Q()`, `F()`, `Count()`, `Sum()`, `Avg()`, and other Django model functions
- **Custom Imports**: Add your own modules, functions, and classes to the query execution environment
- **Query History**: Automatically saves your recent queries for quick re-execution
- **Favorite Queries**: Save frequently used queries with custom names
- **Collapsible Interface**: Clean, collapsible UI that doesn't clutter your admin
- **Comprehensive Dark Mode Support**:
  - Automatically adapts to system preferences
  - Compatible with Django admin's built-in dark mode
  - Works with popular admin themes (Grappelli, Jazzmin, etc.)
  - Smooth transitions between light and dark themes
  - Accessible color contrasts in both modes
- **Security**: Queries execute in a restricted environment with whitelisted functions
- **Smart Result Detection**: Automatically handles both queryset and scalar results

## Installation

```bash
pip install django-admin-query-executor
```

## Quick Start

### Using in Django Admin (QueryExecutorMixin)

1. Add `django_admin_query_executor` to your `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    ...
    'django_admin_query_executor',
    ...
]
```

2. Add the `QueryExecutorMixin` to your `ModelAdmin` classes:

```python
from django.contrib import admin
from django_admin_query_executor import QueryExecutorMixin
from .models import MyModel

@admin.register(MyModel)
class MyModelAdmin(QueryExecutorMixin, admin.ModelAdmin):
    list_display = ['id', 'name', 'created_at']

    # Optional: Define custom example queries for this model
    query_examples = [
        ("Active items", "MyModel.objects.filter(is_active=True)"),
        ("Recent items", "MyModel.objects.filter(created_at__gte=timezone.now() - timedelta(days=7))"),
        ("Count by status", "MyModel.objects.values('status').annotate(count=Count('id'))"),
    ]
```

### Using as Standalone Function (execute_django_query)

You can also use the query executor outside the admin interface in your views, management commands, APIs, Celery tasks, or scripts:

```python
from django_admin_query_executor import execute_django_query
from myapp.models import Book

# Execute a query from a string
result = execute_django_query(
    "objects.filter(price__gte=20).count()",
    Book
)
print(f"Books priced $20 or more: {result}")

# With custom imports
custom_imports = {
    'json': 'json',
    'timedelta': ('datetime', 'timedelta'),
}
books = execute_django_query(
    "objects.filter(created_date__gte=now() - timedelta(days=30))",
    Book,
    custom_imports=custom_imports
)
```

**Function Signature:**
```python
execute_django_query(query_string, model, custom_imports=None)
```

**Parameters:**
- `query_string` (str): The Django ORM query string to execute
- `model` (Model): The Django model class to use as the base model
- `custom_imports` (dict, optional): Additional imports to make available in the query

**Returns:** QuerySet or other result (int, dict, etc.) depending on the query

**Raises:** `ValueError` if there's a syntax error or execution error in the query

**Use Cases:**
- Django management commands for running scheduled queries
- API endpoints that accept dynamic query strings
- Celery tasks for background query execution
- Admin actions with custom query logic
- Testing and debugging scripts

See `example_usage.py` for detailed examples including usage in management commands, views, APIs, and Celery tasks.

## Usage (Admin Interface)

1. Navigate to any model's admin changelist that uses `QueryExecutorMixin`
2. Click "Execute Django Query" to expand the query interface
3. Enter your Django ORM query (e.g., `MyModel.objects.filter(status='active')`)
4. Click "Execute Query"
5. The admin list updates to show your query results

### Query Examples

```python
# Filter queries
Book.objects.filter(author__name__icontains='Smith')
Book.objects.filter(Q(title__icontains='Django') | Q(title__icontains='Python'))

# Annotations and aggregations
Book.objects.annotate(review_count=Count('reviews')).filter(review_count__gt=10)
Book.objects.aggregate(avg_price=Avg('price'), total_books=Count('id'))

# Complex queries with joins
Author.objects.filter(books__published_date__year=2023).distinct()
Book.objects.select_related('author', 'publisher').filter(price__lt=50)

# Counting and existence checks
Book.objects.filter(category='Fiction').count()
Book.objects.filter(reviews__rating__gte=4).exists()

# Using custom imports (see Custom Imports section)
Book.objects.filter(title__in=json.loads('[\"Book1\", \"Book2\"]'))
Book.objects.filter(created_date__gte=datetime.now() - timedelta(days=30))
```

### Standalone Function Examples

```python
from django_admin_query_executor import execute_django_query
from myapp.models import Book, Author

# Basic filtering
active_books = execute_django_query("objects.filter(is_active=True)", Book)

# Count query
count = execute_django_query("objects.filter(price__gte=20).count()", Book)

# Annotations
stats = execute_django_query(
    "objects.aggregate(avg_price=Avg('price'), total=Count('id'))",
    Book
)

# Complex queries with Q objects
books = execute_django_query(
    "objects.filter(Q(category='Fiction') | Q(category='Mystery'))",
    Book
)

# With custom imports
result = execute_django_query(
    "objects.filter(published_date__gte=now() - timedelta(days=30))",
    Book,
    custom_imports={
        'timedelta': ('datetime', 'timedelta'),
        'now': ('django.utils.timezone', 'now'),
    }
)
```

## Configuration

### Custom Imports

You can add custom modules, functions, and classes to the query execution environment in two ways:

#### Method 1: Django Settings (Global)

Add a `QUERY_EXECUTOR_CUSTOM_IMPORTS` setting to your Django settings file:

```python
# settings.py
QUERY_EXECUTOR_CUSTOM_IMPORTS = {
    # Import entire modules
    'json': 'json',
    'datetime': 'datetime',
    'timedelta': 'datetime.timedelta',
    'timezone': 'django.utils.timezone',

    # Import specific functions/classes from modules
    'settings': ('django.conf', 'settings'),
    'Decimal': ('decimal', 'Decimal'),

    # Import your custom models and functions
    'Listing': 'numi.models.Listing',
    'Coin': 'numi.models.Coin',
    'custom_function': 'myapp.utils.custom_function',
}
```

#### Method 2: ModelAdmin Class Attribute (Per-Admin)

Override the `custom_imports` attribute in your ModelAdmin:

```python
@admin.register(PossibleMatch)
class PossibleMatchAdmin(QueryExecutorMixin, admin.ModelAdmin):
    # Custom imports specific to this admin
    custom_imports = {
        'json': 'json',
        'settings': ('django.conf', 'settings'),
        'Listing': 'numi.models.Listing',
        'Coin': 'numi.models.Coin',
        'timedelta': ('datetime', 'timedelta'),
        'now': ('django.utils.timezone', 'now'),
    }

    query_examples = [
        ("High confidence matches", "PossibleMatch.objects.filter(textual_score__gt=0.8, image_score__gt=0.8)"),
        ("Recent matches", "PossibleMatch.objects.filter(textual_match_date__gte=now() - timedelta(days=7))"),
        ("Using settings", "PossibleMatch.objects.filter(textual_score__gte=settings.TEXTUAL_SCORE_CUTOFF)"),
        ("With JSON data", "PossibleMatch.objects.filter(id__in=json.loads('[1, 2, 3]'))"),
    ]
```

#### Import Format Examples

```python
custom_imports = {
    # Simple module imports
    'json': 'json',                    # import json
    'os': 'os',                        # import os

    # Import specific items from modules
    'settings': ('django.conf', 'settings'),              # from django.conf import settings
    'timezone': ('django.utils', 'timezone'),             # from django.utils import timezone
    'timedelta': ('datetime', 'timedelta'),               # from datetime import timedelta
    'Decimal': ('decimal', 'Decimal'),                    # from decimal import Decimal

    # Import your custom models
    'User': ('django.contrib.auth.models', 'User'),      # from django.contrib.auth.models import User
    'MyModel': ('myapp.models', 'MyModel'),               # from myapp.models import MyModel

    # Import custom functions/utilities
    'my_function': ('myapp.utils', 'my_function'),        # from myapp.utils import my_function
    'calculate_score': ('myapp.scoring', 'calculate_score'),
}
```

### Custom Change List Templates

The mixin automatically overrides the ModelAdmin's `change_list_template` if the default template is in use. If your ModelAdmin uses a custom template, the template will need to extend `admin/query_executor_change_list.html`:

```
{% extends "admin/query_executor_change_list.html" %}
```

### Custom Example Queries

Define model-specific example queries by adding a `query_examples` attribute to your ModelAdmin:

```python
class BookAdmin(QueryExecutorMixin, admin.ModelAdmin):
    query_examples = [
        ("Bestsellers", "Book.objects.filter(is_bestseller=True)"),
        ("By price range", "Book.objects.filter(price__gte=20, price__lte=50)"),
        ("Review stats", "Book.objects.annotate(avg_rating=Avg('reviews__rating')).filter(avg_rating__gte=4.0)"),
        ("Using custom imports", "Book.objects.filter(created_date__gte=now() - timedelta(days=30))"),
    ]
```

### Customizing Query History

Control the number of queries saved in history:

```python
class BookAdmin(QueryExecutorMixin, admin.ModelAdmin):
    query_history_limit = 10  # Default is 5
```

## Supported Django ORM Features

### Query Methods
- `filter()`, `exclude()`, `get()`
- `order_by()`, `reverse()`, `distinct()`
- `values()`, `values_list()`
- `select_related()`, `prefetch_related()`
- `annotate()`, `aggregate()`
- `first()`, `last()`, `exists()`, `count()`

### Query Expressions
- `Q()` for complex queries
- `F()` for field references
- `Count()`, `Sum()`, `Avg()`, `Max()`, `Min()`
- `Case()`, `When()` for conditional expressions
- `Exists()`, `OuterRef()`, `Subquery()`

### Database Functions
- String functions: `Lower()`, `Upper()`, `Length()`, `Concat()`
- Date functions: `TruncDate()`, `Extract()`, `Now()`
- Type casting: `Cast()`, `Coalesce()`

## Dark Mode Support

The package includes comprehensive dark mode support that:

- **Auto-detects** your system's color scheme preference
- **Integrates seamlessly** with Django admin's native dark mode
- **Supports popular admin themes** including:
  - Django's built-in dark mode
  - Grappelli dark theme
  - Django Jazzmin dark mode
  - Django Admin Interface dark themes
- **Provides smooth transitions** when switching between themes
- **Ensures accessibility** with proper color contrasts
- **Includes custom CSS variables** for easy customization

### Customizing Dark Mode Colors

You can override the default colors by adding CSS variables to your admin CSS:

```css
.query-executor-container {
    --qe-bg-primary: #1a1a1a;
    --qe-text-primary: #ffffff;
    --qe-button-primary-bg: #007bff;
    /* See query_executor_dark_mode.css for all available variables */
}
```

## Security

The query executor runs in a restricted environment with:
- Whitelisted functions and classes only
- No access to private attributes or methods
- No direct database access beyond Django ORM
- No file system or network access
- Custom imports are loaded with error handling
- Failed imports are logged but don't break functionality

## Requirements

- Django >= 3.2
- Python >= 3.8

## License

MIT License - see LICENSE file for details

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## API Reference

### `execute_django_query(query_string, model, custom_imports=None)`

Standalone function for executing Django ORM queries from text strings outside the admin interface.

**Parameters:**
- `query_string` (str): Django ORM query string (e.g., `"objects.filter(status='active').count()"`)
- `model` (django.db.models.Model): The Django model class to query
- `custom_imports` (dict, optional): Additional modules/functions to make available
  - Format: `{alias: module_path}` or `{alias: (module_path, attribute)}`
  - Example: `{'json': 'json', 'MyModel': ('myapp.models', 'MyModel')}`

**Returns:**
- QuerySet, int, dict, or other result depending on the query

**Raises:**
- `ValueError`: If there's a syntax error or execution error in the query

**Security:**
- Queries execute in a restricted environment with whitelisted functions only
- No access to private attributes, file system, or network
- Custom imports are loaded with error handling

**Example:**
```python
from django_admin_query_executor import execute_django_query
from myapp.models import Book

# Simple usage
books = execute_django_query("objects.filter(price__lt=30)", Book)

# With custom imports
result = execute_django_query(
    "objects.filter(created__gte=now() - timedelta(days=7))",
    Book,
    custom_imports={
        'timedelta': ('datetime', 'timedelta'),
        'now': ('django.utils.timezone', 'now'),
    }
)
```

## Changelog

### 1.2.0 (TBD)
- Added standalone `execute_django_query()` function for use outside admin interface
- Function can be used in management commands, views, APIs, Celery tasks, and scripts
- Refactored internal implementation to share code between mixin and standalone function

### 1.1.0 (2025-01-17)
- Added custom imports support via Django settings and ModelAdmin attribute

### 1.0.0 (2025-07-17)
- Initial release
- Full Django ORM query support
- Query history and favorites
- Dark mode support
- Collapsible interface