Server-side processing for DataTables and Editor with MongoDB
Project description
mongo-datatables
Server-side processing for jQuery DataTables with MongoDB.
Translates DataTables Ajax requests into MongoDB aggregation pipelines, handling pagination, sorting, filtering, search, SearchPanes, SearchBuilder, and Editor (full CRUD).
Links
- mongo-datatables.com — project homepage
- docs.mongo-datatables.com — full documentation with configuration details, examples, and API reference
- Vinyl Archives — full-featured showcase app with CRUD, cover art, SearchPanes, SearchBuilder, and Editor
- Flask Demo · Django Demo · FastAPI Demo — minimal starter apps showing framework integration
Installation
pip install mongo-datatables
# or
uv add mongo-datatables
Quick Start
from pymongo import MongoClient
from mongo_datatables import DataTables, DataField
db = MongoClient("mongodb://localhost:27017/")["mydb"]
data_fields = [
DataField('title', 'string'),
DataField('artist', 'string'),
DataField('year', 'number'),
DataField('genre', 'string'),
]
# args is the DataTables Ajax request body (a dict)
result = DataTables(db, 'albums', args, data_fields).get_rows()
db is any PyMongo Database object. args is the JSON body from a DataTables Ajax POST.
get_rows() returns the standard server-side response:
{
"draw": 1,
"recordsTotal": 1000000,
"recordsFiltered": 4821,
"data": [...]
}
Framework Examples
Flask
@app.route('/api/data', methods=['POST'])
def data():
args = request.get_json()
dt = DataTables(db, 'albums', args, data_fields)
return jsonify(dt.get_rows())
FastAPI
@app.post('/api/data')
async def data(request: Request):
args = await request.json()
dt = DataTables(db, 'albums', args, data_fields)
return JSONResponse(dt.get_rows())
Django
class DataView(View):
def post(self, request):
args = json.loads(request.body)
dt = DataTables(db, 'albums', args, data_fields)
return JsonResponse(dt.get_rows())
Litestar
@post('/api/data')
async def data(request: Request) -> dict:
args = await request.json()
return DataTables(db, 'albums', args, data_fields).get_rows()
Quart
@app.route('/api/data', methods=['POST'])
async def data():
args = await request.get_json()
dt = DataTables(db, 'albums', args, data_fields)
return jsonify(dt.get_rows())
DataField
DataField(name, data_type, alias=None) maps a MongoDB field to a DataTables column.
DataField('title', 'string') # basic field
DataField('release_date', 'date') # date comparison
DataField('track_count', 'number') # numeric comparison
DataField('PublisherInfo.label', 'string', 'label') # nested + alias
DataField('_id', 'objectid') # string in response
Valid types:
| Type | Search behaviour | Uses index? | Operators |
|---|---|---|---|
keyword |
Exact equality match | Yes (regular index) | — |
string |
Case-insensitive regex (substring) | No | — |
number |
Exact equality or numeric comparison | Yes (regular index) | > >= < <= = |
date |
Date comparison (ISO YYYY-MM-DD) |
Yes (regular index) | > >= < <= = |
array |
Regex against array elements | No | — |
objectid |
Serialized as string in response | — | — |
boolean, object, null |
Treated as string (regex) | No | — |
Use keyword for categorical/code fields (country codes, status values, tags) where exact matching is always intended and index performance matters. Use string for free-text fields where substring and partial matching is useful.
# country:US → {"country_code": "US"} — uses index
DataField('country_code', 'keyword')
# name:york → regex, finds "New York", "Yorkshire"
DataField('name', 'string')
# year:>1990 → {"year": {"$gt": 1990}} — uses index
DataField('year', 'number')
# released:>=2020-01-01 — uses index
DataField('released', 'date')
# serialized as string in response
DataField('_id', 'objectid')
# nested field with UI alias
DataField('PublisherInfo.label', 'string', 'label')
The alias is the name DataTables uses for the column (columns[i][data]). Defaults to the last segment of the field path (PublisherInfo.label → label).
Search
Search is where this library earns its keep. The global search box supports several modes:
Text index search (fast)
When a MongoDB text index exists, global search uses $text — fast even on multi-million-row collections:
db.albums.create_index([
("title", "text"),
("artist", "text"),
("genre", "text"),
])
Without a text index, the library falls back to per-column regex (much slower on large collections).
Phrase search
Wrap in quotes for exact phrase matching:
"Dark Side of the Moon"
Multi-word AND search
With search[smart]=true (DataTables default), each word must match at least one searchable column:
pink floyd 1973 → all three terms must appear across the row
Colon syntax — field-specific search
Target a specific field without needing a separate input:
artist:Bowie → artist contains "Bowie" (regex)
artist:"David Bowie" → exact phrase in artist field
country_code:US → equals "US" (keyword, uses index)
year:1972 → equals 1972 (number, uses index)
year:>1990 → greater than
year:>=1990 year:<2000 → combine conditions (ANDed)
release_date:>2020-01-01 → date comparison
Column search with ranges
Per-column search supports pipe-delimited min|max for numbers and dates:
1990|2000 → 1990 ≤ year ≤ 2000
2020-01-01|2020-12-31
Regex mode
Set search[regex]=true to treat the search value as a raw MongoDB regex:
^Dark → starts with "Dark"
(Floyd|Bowie) → matches either
User-supplied regex patterns are validated for safety before execution to prevent ReDoS (Regular Expression Denial of Service) attacks. Patterns exceeding 200 characters or containing unsafe constructs (quantifiers on groups, lookahead/behind, stacked quantifiers, very large bounded repetitions) are rejected with a descriptive error.
Case sensitivity
Case-insensitive by default. Pass search[caseInsensitive]=false for case-sensitive matching.
Per-column override via columns[i][search][caseInsensitive].
SearchPanes
No server-side configuration needed — call get_searchpanes_options() to populate panes on page load:
@app.route('/searchpanes', methods=['POST'])
def searchpanes():
dt = DataTables(db, 'albums', request.get_json(), data_fields)
return jsonify(dt.get_searchpanes_options())
SearchBuilder
Full server-side support with nested AND/OR criteria trees. Works automatically — no extra configuration needed.
Sorting
Multi-column sorting, ColReorder (order[i][name] name-based ordering), and orderData column redirect are all supported.
Custom Filters
Scope all queries to a subset of the collection by passing extra filter criteria as keyword arguments:
DataTables(
db, 'albums', args, data_fields,
status='active', label='Merge Records',
)
Editor
Full CRUD support for DataTables Editor.
from mongo_datatables import Editor
@app.route('/editor', methods=['POST'])
def editor():
data = request.get_json()
result = Editor(
db, 'albums', data,
doc_id=request.args.get('id'),
data_fields=data_fields,
).process()
return jsonify(result)
Editor also handles action=search for autocomplete and tags field types:
@app.route('/editor/search', methods=['POST'])
def editor_search():
data = request.get_json()
editor = Editor(db, 'albums', data, data_fields=data_fields)
return jsonify(editor.search())
Optional Editor parameters:
| Parameter | Description |
|---|---|
validators |
dict mapping field names to callable(value) -> str|None |
hooks |
pre_create, pre_edit, pre_remove callables — return falsy to cancel |
options |
dict or zero-arg callable for select/radio/checkbox field options |
dependent_handlers |
dict mapping field names to callables for dependent field Ajax |
file_fields + storage_adapter |
file upload support (subclass StorageAdapter) |
row_class, row_data, row_attr |
per-row metadata (static value or callable) |
File Uploads
File uploads are validated for security before storage:
- Magic bytes: verified against declared MIME type (JPEG, PNG, GIF, WebP, PDF, plain text, CSV)
- Filename safety: rejects path traversal, null bytes, URL-encoded characters, Windows-reserved characters, and blocked executable extensions
- Size limits: per-type limits (10 MB images, 25 MB PDF, 5 MB text/CSV) with 50 MB global cap
- Virus scanning: optional integration point for antivirus/malware detection
from mongo_datatables.editor.validators.upload_security import validate_upload_data
# Validate before storage
validate_upload_data({
"filename": "report.pdf",
"content_type": "application/pdf",
"data": file_bytes,
}, scanner=optional_scanner_instance)
To plug in a virus scanner at the Editor level, pass it via virus_scanner. The scanner must implement scan(filename: str, data: bytes) -> bool — returning False rejects the file:
editor = Editor(mongo, "files", request_args, data_fields, virus_scanner=my_scanner)
Performance & Indexes
For large collections, indexes are critical — the library uses aggregation pipelines on every request.
Text index
db.albums.create_index([
("title", "text"),
("artist", "text"),
("genre", "text"),
])
With a text index, global search runs in ~100–300ms on multi-million-row collections. Without one, regex fallback can take 5–10+ seconds.
MongoDB allows only one text index per collection, but it can cover multiple fields.
To force regex search even when a text index exists (for substring matching):
DataTables(db, 'albums', args, data_fields, use_text_index=False)
Regular indexes
Create indexes for fields used in sorting, column search, or custom filters:
db.albums.create_index("year")
db.albums.create_index("artist")
db.albums.create_index([("artist", 1), ("year", -1)]) # compound
Advanced
pipeline_stages — inject aggregation stages ($lookup, $addFields, $unwind) before the $match, useful for computed or joined fields.
allow_disk_use=True — pass allowDiskUse to aggregation pipelines when complex filters exceed MongoDB's 100 MB in-memory limit.
get_export_data() — returns all matching rows without pagination for CSV/Excel export.
row_id, row_class, row_data, row_attr — per-row DT_Row* metadata, accepts a static value or a callable receiving the raw document.
See the full documentation for details.
Development
Run tests:
uv run pytest tests/
Run with coverage:
uv run pytest --cov=mongo_datatables tests/ \
--cov-report=term --cov-report=html
License
Released under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mongo_datatables-2.2.0.tar.gz.
File metadata
- Download URL: mongo_datatables-2.2.0.tar.gz
- Upload date:
- Size: 53.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9db140ed4f63348b718a29e72c9a8795d5f560116d580653f337c9b3b2fb5534
|
|
| MD5 |
8f8a861a4a3682c90d593222de133b62
|
|
| BLAKE2b-256 |
cde6f878931692bce08139873357e3a5b21ca8bbca3e4c8a8b17d0e424820640
|
File details
Details for the file mongo_datatables-2.2.0-py3-none-any.whl.
File metadata
- Download URL: mongo_datatables-2.2.0-py3-none-any.whl
- Upload date:
- Size: 64.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ac30729eb8b5a3e813b320de52fd985b1a930760fb709b6631a2e7a896719fb
|
|
| MD5 |
627dbd5eac0a712bce737045c978a43c
|
|
| BLAKE2b-256 |
5e6f86e117d81a54d35ca53ac084c34db0560489985926d4662efab6f09ff5ab
|