Django Eddytor
A Django wrapper for the Eddytor rich text editor, bringing Notion-style editing to your Django applications with a clean, Pythonic API.
✨ Features
- 🎨 Notion-style Editor - Modern, intuitive editing experience
- ⚡ Slash Commands - Quick access to formatting and blocks (
/heading,/table,/image, etc.) - 🤖 AI Rewrite Integration - Connect your AI API for content enhancement
- 📊 Rich Content Blocks - Tables, charts, callouts, code blocks, and more
- 🖼️ Media Support - Images and videos with Base64 or API upload
- 📑 Table of Contents - Auto-generated document navigation
- 🔗 Integrations - Import from Confluence and Azure DevOps
- 🎭 Theme Support - Automatic light/dark mode detection
- 📝 Django Forms Ready - Seamless integration with Django forms
- 🐍 Pythonic Configuration - No JavaScript needed, configure everything in Python
📦 Installation
1. Install the Package
# Via pip (when published)
pip install django-eddytor
# Or install from source
git clone [Official github source]
cd django-eddytor
pip install -e .
2. Add to Django Settings
# settings.py
INSTALLED_APPS = [
# ... other apps
'django_eddytor',
]
3. Configure Static Files
# settings.py
STATIC_URL = '/static/'
# For production deployment only
STATIC_ROOT = BASE_DIR / 'staticfiles'
4. Collect Static Files (Production Only)
# Only needed when deploying to production
python manage.py collectstatic
Note: During development with
DEBUG = True, Django automatically serves static files. You don't need to runcollectstatic.
🚀 Quick Start
Minimal Example
Create a form with Eddytor in just a few lines:
# forms.py
from django import forms
from django_eddytor.widgets import EddytorWidget
class ArticleForm(forms.Form):
content = forms.CharField(
widget=EddytorWidget(
config={
"placeholder": {
"content": "Start writing or type / for commands..."
}
}
)
)
# views.py
from django.shortcuts import render, redirect
from .forms import ArticleForm
def create_article(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
content = form.cleaned_data['content']
# Save your content here
return redirect('success')
else:
form = ArticleForm()
return render(request, 'article_form.html', {'form': form})
<!-- article_form.html -->
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Create Article</title>
</head>
<body>
<h1>Create New Article</h1>
<form method="post">
{% csrf_token %}
{{ form.content }}
<button type="submit">Save Article</button>
</form>
</body>
</html>
That's it! You now have a fully functional Notion-style editor. 🎉
📚 Configuration Guide
All configuration is done through Python dictionaries - no JavaScript required!
Title & Subtitle
Add a document title with an optional subtitle:
widget = EddytorWidget(
config={
"title": {
"enabled": True,
"initialValue": "My Document",
"placeholder": "Enter title here...",
"editable": True,
"className": "document-title",
"subtitle": {
"enabled": True,
"text": "Last updated today",
"className": "text-gray-500",
"editable": False
}
}
}
)
Options:
| Property | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | False |
Enable title field |
initialValue |
string | '' |
Pre-filled title text |
placeholder |
string | 'Untitled Document' |
Placeholder text |
editable |
boolean | True |
Allow editing |
className |
string | '' |
CSS class for styling |
subtitle.enabled |
boolean | False |
Show subtitle |
subtitle.text |
string | '' |
Subtitle content |
Theme Configuration
Automatic theme switching based on HTML attributes:
config = {
"themeConfig": {
"observeElement": "html", # Element to observe
"attribute": "class", # Attribute to watch
"darkValue": "dark", # Value indicating dark mode
"lightValue": "", # Value indicating light mode
"default": "light" # Default theme
}
}
Example with data attributes:
config = {
"themeConfig": {
"observeElement": "html",
"attribute": "data-theme",
"darkValue": "dark",
"lightValue": "light",
"default": "light"
}
}
Placeholder Text
Customize the empty editor placeholder:
config = {
"placeholder": {
"text": "Type / for commands or click + to open menu",
"className": "editor-placeholder",
"showForEmptyBlocks": True,
"showOnFocus": True
}
}
Form Integration
Generate a hidden textarea for form submission:
config = {
"textarea": {
"generate": True, # Auto-generate textarea
"name": "content" # Form field name
}
}
The widget automatically syncs the editor content with the textarea on form submission.
AI Rewrite
Enable AI-powered content enhancement:
config = {
"aiRewrite": {
"enabled": True,
"apiEndpoint": "/api/ai/rewrite/",
"apiMethod": "POST",
"headers": {
"Content-Type": "application/json"
},
"requestTransform": {
"tone": "professional",
"readingLevel": "grade-8",
"maxTokens": 500
},
"responseTransform": True # Use default response handler
}
}
Backend Implementation:
# views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
@csrf_exempt # Use proper authentication in production
def ai_rewrite(request):
if request.method != 'POST':
return JsonResponse({"error": "Method not allowed"}, status=405)
try:
data = json.loads(request.body)
content = data.get("text") or data.get("content")
tone = data.get("tone", "professional")
# Process with your AI service (OpenAI, Anthropic, etc.)
rewritten = your_ai_service.rewrite(
content,
tone=tone,
reading_level=data.get("readingLevel")
)
return JsonResponse({"rewritten_text": rewritten})
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
Expected API Response:
{
"rewritten_text": "<p>Your rewritten content here...</p>"
}
Image Upload
Base64 Mode (Default - Good for Development):
config = {
"image": {
"upload": {
"mode": "base64" # Stores images as inline data
}
}
}
API Upload Mode (Recommended for Production):
config = {
"image": {
"upload": {
"mode": "api",
"endpoint": "/api/upload/image/",
"field": "file",
"method": "POST",
"headers": {
"X-CSRFToken": "{{ csrf_token }}"
},
"responseKey": "url" # Key in response containing image URL
}
}
}
Backend Implementation:
# views.py
from django.core.files.storage import default_storage
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
@login_required
@require_POST
def upload_image(request):
file = request.FILES.get('file')
if not file:
return JsonResponse({"error": "No file provided"}, status=400)
# Validate file type
allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
if file.content_type not in allowed_types:
return JsonResponse({"error": "Invalid file type"}, status=400)
# Validate file size (5MB limit)
if file.size > 5 * 1024 * 1024:
return JsonResponse({"error": "File too large"}, status=400)
# Save file
filename = default_storage.save(f'images/{file.name}', file)
url = default_storage.url(filename)
return JsonResponse({"url": url})
URL Configuration:
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('api/upload/image/', views.upload_image, name='upload_image'),
]
Video Upload
URL Only (Default):
config = {
"video": {
"upload": {
"mode": "url" # User enters video URL
}
}
}
API Upload:
config = {
"video": {
"upload": {
"mode": "api",
"endpoint": "/api/upload/video/",
"method": "POST",
"responseKey": "url"
}
}
}
Both URL and Upload:
config = {
"video": {
"upload": {
"mode": "both", # Allow both URL input and file upload
"endpoint": "/api/upload/video/",
"method": "POST",
"responseKey": "url"
}
}
}
Table of Contents (TOC)
Enable automatic table of contents generation:
HTML Structure:
<!-- your_template.html -->
<div id="editor-wrapper">
<div class="toc-block" style="height: 100%;">
<div style="display: none;">
<a class="toc-item" href="#intro" target="_self">Introduction</a>
<a class="toc-item" href="#guide" target="_self">User Guide</a>
<a class="toc-item" href="#api" target="_self">API Reference</a>
</div>
<form method="post" style="height: 100%;">
{% csrf_token %}
{{ form.content }}
<button type="submit">Save</button>
</form>
</div>
</div>
Widget Configuration:
widget = EddytorWidget(
config={
"editorId": "editor-wrapper", # Must match parent container ID
"toc": {
"enable": True,
"toc_items": [
{"url": "#intro", "text": "Introduction"},
{"url": "#guide", "text": "User Guide"},
{"url": "#api", "text": "API Reference"}
]
}
}
)
Requirements:
- ✅ Parent container must have an
idattribute - ✅ Must have a sibling
.toc-blockelement - ✅ TOC items must have valid
hrefattributes - ✅ Use
/tocslash command to insert TOC in document
Confluence Import
Import content from Confluence pages:
config = {
"confluence": {
"enabled": True,
"dummyMode": False, # Set to True for testing without backend
"statusEndpoint": "/api/integrations/confluence/status/",
"importEndpoint": "/api/confluence/import/",
"buildImportRequest": {
"method": "POST",
"headers": {"Content-Type": "application/json"},
"bodyField": "pageUrl" # Field name for the Confluence URL
},
"importResponseTransform": True # Use default response handler
},
"settingsUrl": "/settings/integrations/" # Redirect for configuration
}
Backend Implementation:
# views.py
import requests
from django.http import JsonResponse
import json
def confluence_import(request):
if request.method != 'POST':
return JsonResponse({"error": "Method not allowed"}, status=405)
try:
data = json.loads(request.body)
page_url = data.get('pageUrl')
if not page_url:
return JsonResponse({"error": "No page URL provided"}, status=400)
# Fetch from Confluence API
# You'll need Confluence API credentials
confluence_response = requests.get(
page_url,
auth=('username', 'api_token'),
headers={'Accept': 'application/json'}
)
confluence_data = confluence_response.json()
html_content = confluence_data.get('body', {}).get('storage', {}).get('value', '')
title = confluence_data.get('title', 'Untitled')
return JsonResponse({
"html": html_content,
"title": title
})
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
Expected Response:
{
"html": "<p>Confluence page content...</p>",
"title": "Page Title"
}
Azure DevOps Import
Import work items from Azure DevOps queries:
config = {
"azureDevOps": {
"enabled": True,
"dummyMode": False,
"statusEndpoint": "/api/integrations/azuredevops/status/",
"importEndpoint": "/api/azuredevops/import/",
"buildQueryImportRequest": {
"method": "POST",
"headers": {"Content-Type": "application/json"},
"bodyField": "queryUrl" # Field name for the query URL
},
"queryImportResponseTransform": True
},
"settingsUrl": "/settings/integrations/"
}
Backend Implementation:
# views.py
def azure_import(request):
if request.method != 'POST':
return JsonResponse({"error": "Method not allowed"}, status=405)
try:
data = json.loads(request.body)
query_url = data.get('queryUrl')
if not query_url:
return JsonResponse({"error": "No query URL provided"}, status=400)
# Parse Azure DevOps URL
# Extract organization, project, query ID
# Fetch work items from Azure DevOps API
azure_response = requests.post(
f'https://dev.azure.com/{organization}/{project}/_apis/wit/wiql/{query_id}',
headers={
'Authorization': f'Basic {base64_encoded_pat}',
'Content-Type': 'application/json'
},
params={'api-version': '6.0'}
)
work_items = azure_response.json()
# Convert work items to HTML table
html_content = convert_work_items_to_html(work_items)
return JsonResponse({"html": html_content})
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
Expected Response:
{
"html": "<table>...work items...</table>"
}
Metadata
Release files for geteddytor-django 0.18.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| geteddytor_django-0.18.2.tar.gz | 837.2 kB | Without provenance | Details for geteddytor_django-0.18.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| geteddytor_django-0.18.2-py3-none-any.whl | Python 3 | none | any | Details for geteddytor_django-0.18.2-py3-none-any.whl |
Total release size: 1.7 MB
Release files / geteddytor_django-0.18.2.tar.gz
| Download URL | geteddytor_django-0.18.2.tar.gz |
|---|---|
| Size | 837.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
031032a012ba231472614a9c088106d7f2329206854283f7efd2b99d1b240915
|
|
BLAKE2b-256 checksum How to use checksums |
5a2be4cf0470f9264e853f0adc619548edc406be4395366d424b4d1fa42a93d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.0.1 CPython/3.14.0
|
Release files / geteddytor_django-0.18.2-py3-none-any.whl
| Download URL | geteddytor_django-0.18.2-py3-none-any.whl |
|---|---|
| Size | 834.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5e37e5a4b9ae384f6a0cc8d5a850ad275177c3bd9947768fdf3d3e6d52d5b1c3
|
|
BLAKE2b-256 checksum How to use checksums |
c635dd733795d6aa78ebabe4704dc12b9149e16a48d2fd2a4906e873ba5dd106
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.0.1 CPython/3.14.0
|
Security for geteddytor-django 0.18.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.