Skip to main content

Rich-text fields for Django using Prosemirror - a powerful, schema-driven rich text editor.

Project description

1 django-prosemirror

Version:

0.7.0

Source:

https://github.com/maykinmedia/django_prosemirror

Keywords:

Django, Prosemirror, rich-text, editor, document, JSON, WYSIWYG, content editor, text editor, markdown, html

PythonVersion:

3.11+

Build status Code quality checks Ruff Coverage status Documentation Status

python-versions django-versions pypi-version

Rich-text fields for Django using Prosemirror - a powerful, schema-driven rich text editor.

2 Features

  • Rich-text editing: Full-featured Prosemirror editor integration with Django (admin) forms

  • Bidirectional conversion: Seamless conversion between HTML and ProseMirror document format

  • Configurable schemas: Fine-grained control over allowed content (headings, links, images, tables, etc.)

  • Native ProseMirror storage: Documents are stored in their native Prosemirror format, preserving structure and enabling programmatic manipulation without HTML parsing

3 Installation

3.1 Requirements

  • Python 3.11 or above

  • Django 4.2 or newer

3.2 Install

pip install maykin-django-prosemirror

Add to your Django settings:

INSTALLED_APPS = [
    # ... your other apps
    'django_prosemirror',
]

4 Usage

4.1 Model Field

Use ProseMirrorModelField in your Django models:

from django.db import models
from django_prosemirror.fields import ProseMirrorModelField
from django_prosemirror.schema import NodeType, MarkType

class BlogPost(models.Model):
    title = models.CharField(max_length=200)

    # Full-featured rich text content (uses default configuration allowing all node
    # and mark types)
    content = ProseMirrorModelField()

    # Limited schema - only headings and paragraphs with bold text
    summary = ProseMirrorModelField(
        allowed_node_types=[NodeType.PARAGRAPH, NodeType.HEADING],
        allowed_mark_types=[MarkType.STRONG],
        null=True,
        blank=True
    )

    # Default document
    content_with_prompt = ProseMirrorModelField(
        default=lambda: {
            "type": "doc",
            "content": [
                {
                    "type": "paragraph",
                    "content": [{"type": "text", "text": "Start writing..."}]
                }
            ]
        }
    )

4.2 Accessing Content

Accessing a field returns a ProsemirrorFieldDocument. It holds the raw ProseMirror document structure and lets you read the content as HTML or as the underlying document dict:

post = BlogPost.objects.get(pk=1)

post.content.html  # "<h1>Heading</h1><p>Paragraph content...</p>"
post.content.doc   # {"type": "doc", "content": [...]}

Use safe_html to render content in templates — Django’s auto-escaping means html would print the tags as literal text:

{{ post.content.safe_html }}

4.3 Setting field values

Fields accept a ProseMirror document dict, an HTML string, None (nullable fields only), or a ProsemirrorFieldDocument. Any other type raises a ValidationError.

# Dict — the native ProseMirror document format
Article.objects.create(content={
    "type": "doc",
    "content": [
        {
            "type": "heading",
            "attrs": {"level": 1},
            "content": [{"type": "text", "text": "My Heading"}]
        },
        {
            "type": "paragraph",
            "content": [
                {"type": "text", "text": "Some "},
                {"type": "text", "marks": [{"type": "strong"}], "text": "bold"},
                {"type": "text", "text": " text."}
            ]
        }
    ]
})

# HTML string — converted to a doc automatically using the field's schema
Article.objects.create(content="<h1>My Heading</h1><p>Some <strong>bold</strong> text.</p>")

# Plain text (no tags) is also accepted — wrapped in a paragraph
Article.objects.create(content="Just plain text")

# Direct assignment on an existing instance works the same way
article = Article.objects.get(pk=1)
article.content = "<p>Replaced via HTML</p>"
article.save()

# Unsupported types raise ValidationError
article.content = ["not", "valid"]  # raises ValidationError

Regardless of input type, reading the field always returns a ProsemirrorFieldDocument:

article = Article.objects.get(pk=1)
article.content.html  # "<h1>My Heading</h1><p>Some <strong>bold</strong> text.</p>"
article.content.doc   # {"type": "doc", "content": [...]}

If you already hold a reference to the ProsemirrorFieldDocument object (e.g. it was passed into a function), you can mutate it in place via the .html or .doc setters — the change is synced back to the model automatically:

def update_content(doc, new_html):
    doc.html = new_html  # syncs back to the model instance

update_content(post.content, "<p>Updated</p>")
post.save()

ProsemirrorFieldDocument is falsy when the document is None or its content list is empty, and truthy otherwise:

if post.content:
    render(post.content.safe_html)

Two convenience methods are also available for clearing the content:

post.content.clear()    # reset to an empty document
post.content.nullify()  # set to None (field must allow null)

Both update the in-memory model instance only — you must call post.save() to persist the change to the database.

4.4 Form Field

Use ProsemirrorFormField in Django forms:

from django import forms
from django_prosemirror.fields import ProsemirrorFormField
from django_prosemirror.schema import NodeType, MarkType

class BlogPostForm(forms.Form):
    title = forms.CharField(max_length=200)

    # Full-featured editor (uses default configuration)
    content = ProsemirrorFormField()

    # Limited to headings and paragraphs with basic formatting
    summary = ProsemirrorFormField(
        allowed_node_types=[NodeType.PARAGRAPH, NodeType.HEADING],
        allowed_mark_types=[MarkType.STRONG, MarkType.ITALIC],
        required=False
    )

4.5 Schema Configuration

Control exactly what content types are allowed using node and mark types:

from django_prosemirror.schema import NodeType, MarkType

# Available node types
NodeType.PARAGRAPH         # Paragraphs (required)
NodeType.HEADING           # Headings (h1-h6)
NodeType.BLOCKQUOTE        # Quote blocks
NodeType.HORIZONTAL_RULE   # Horizontal rules
NodeType.CODE_BLOCK        # Code blocks
NodeType.FILER_IMAGE       # Images (requires django-filer)
NodeType.HARD_BREAK        # Line breaks
NodeType.BULLET_LIST       # Bullet lists
NodeType.ORDERED_LIST      # Numbered lists
NodeType.LIST_ITEM         # List items
NodeType.TABLE             # Tables
NodeType.TABLE_ROW         # Table rows
NodeType.TABLE_CELL        # Table data cells
NodeType.TABLE_HEADER      # Table header cells

# Available mark types
MarkType.STRONG            # Bold text
MarkType.ITALIC            # Italic text (em)
MarkType.UNDERLINE         # Underlined text
MarkType.STRIKETHROUGH     # Strikethrough text
MarkType.CODE              # Inline code
MarkType.LINK              # Links

# Custom configurations
BASIC_FORMATTING = {
    'allowed_node_types': [NodeType.PARAGRAPH, NodeType.HEADING],
    'allowed_mark_types': [MarkType.STRONG, MarkType.ITALIC, MarkType.LINK],
}

BLOG_EDITOR = {
    'allowed_node_types': [
        NodeType.PARAGRAPH,
        NodeType.HEADING,
        NodeType.BLOCKQUOTE,
        NodeType.IMAGE,
        NodeType.BULLET_LIST,
        NodeType.ORDERED_LIST,
        NodeType.LIST_ITEM,
    ],
    'allowed_mark_types': [
        MarkType.STRONG,
        MarkType.ITALIC,
        MarkType.LINK,
        MarkType.CODE,
    ],
}

TABLE_EDITOR = {
    'allowed_node_types': [
        NodeType.PARAGRAPH,
        NodeType.HEADING,
        NodeType.TABLE,
        NodeType.TABLE_ROW,
        NodeType.TABLE_CELL,
        NodeType.TABLE_HEADER,
    ],
    'allowed_mark_types': [MarkType.STRONG, MarkType.ITALIC],
}

# Use in fields
class DocumentModel(models.Model):
    blog_content = ProseMirrorModelField(**BLOG_EDITOR)
    table_content = ProseMirrorModelField(**TABLE_EDITOR)

4.6 Default Values

Always use callables for default values returning valid ProseMirror documents:

from django_prosemirror.constants import get_empty_doc

class Article(models.Model):
    # ✅ Correct: Using a callable
    content = ProseMirrorModelField(default=get_empty_doc)

    # ❌ Wrong: Static dict (validation error)
    # content = ProseMirrorModelField(
    #     default={"type": "doc", "content": []}
    # )

4.7 Django Admin Integration

The field works automatically with Django admin:

from django.contrib import admin
from .models import BlogPost

@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
    fields = ['title', 'content', 'summary']
    readonly_fields = ['summary']  # Read-only fields render as HTML

    # Editable fields: Render the full ProseMirror rich-text editor
    # Read-only fields: Render as formatted HTML output

4.8 Frontend Integration

Required Assets: The ProseMirror form fields require both CSS and JavaScript assets to function. These assets are mandatory for any template that renders ProseMirror form fields - without them, the rich text editor will not work.

{% load django_prosemirror %}
<!DOCTYPE html>
<html>
<head>
    {% include_django_prosemirror_css %}
    {% include_django_prosemirror_js_defer %}
</head>
<body>
    {{ form.as_p }}
</body>
</html>

Note: These assets are only required for form rendering (editing). Displaying saved content using {{ post.content.html }} in templates does not require these assets.

5 Data migrations

Corrupt ProseMirror field data can end up in the database whenever code bypasses the field’s validation — for example, a bulk_create or update() call that writes raw values directly, a third-party library that writes to the column without going through the descriptor, or a data import that predates the field being introduced. django_prosemirror provides helpers in django_prosemirror.migration_utils to audit and repair affected rows safely. All helpers bypass the field descriptor via .values() and .update(), so they work even when corrupt rows would otherwise raise ValidationError on normal access.

5.1 Auditing

Use the iter_* functions to inspect data without modifying it:

from django_prosemirror.migration_utils import (
    iter_corrupt_prosemirror_rows,
    iter_schema_invalid_prosemirror_rows,
)

# Wrong type or shape (strings, lists, numbers, malformed dicts)
for pk, raw in iter_corrupt_prosemirror_rows(MyModel, "body"):
    print(f"pk={pk}: corrupt value {raw!r}")

# Correct shape but fails schema validation for that field
for pk, raw in iter_schema_invalid_prosemirror_rows(MyModel, "body"):
    print(f"pk={pk}: schema-invalid doc {raw!r}")

These two functions cover disjoint sets: a row will appear in at most one of them.

5.2 Repairing

Three repair functions are available, each returning a list[RepairRecord] for logging:

from django_prosemirror.migration_utils import (
    repair_prosemirror_html_strings,
    nullify_corrupt_prosemirror_rows,
    clear_corrupt_prosemirror_rows,
)

# Convert corrupt HTML strings to proper doc dicts
records = repair_prosemirror_html_strings(MyModel, "body")

# Set corrupt values to NULL (nullable fields only — raises ValueError otherwise)
records = nullify_corrupt_prosemirror_rows(MyModel, "body")

# Set corrupt values to an empty doc (works on any field)
records = clear_corrupt_prosemirror_rows(MyModel, "body")

for r in records:
    print(f"pk={r.pk}: {r.original!r}{r.repaired!r}")

5.3 Example data migration

A common case is a field that previously stored raw HTML strings. Use repair_prosemirror_html_strings to convert them in place:

from django_prosemirror.migration_utils import repair_prosemirror_html_strings

def migrate(apps, schema_editor):
    MyModel = apps.get_model("myapp", "MyModel")
    records = repair_prosemirror_html_strings(MyModel, "body")
    for r in records:
        print(f"Repaired pk={r.pk}")
    print(f"{len(records)} row(s) repaired")

class Migration(migrations.Migration):
    dependencies = [("myapp", "0001_initial")]
    operations = [migrations.RunPython(migrate, migrations.RunPython.noop)]

The schema used for conversion is derived automatically from the field definition on the historical model, so no manual schema configuration is needed.

For non-string corrupt values (integers, lists, malformed dicts) there is no automatic conversion. If you know what shape the corrupt data takes you can fix it yourself using iter_corrupt_prosemirror_rows:

from django_prosemirror.migration_utils import iter_corrupt_prosemirror_rows

def migrate(apps, schema_editor):
    MyModel = apps.get_model("myapp", "MyModel")
    for pk, raw in iter_corrupt_prosemirror_rows(MyModel, "body"):
        fixed = my_conversion(raw)  # your own logic here
        MyModel.objects.filter(pk=pk).update(body=fixed)

class Migration(migrations.Migration):
    dependencies = [("myapp", "0001_initial")]
    operations = [migrations.RunPython(migrate, migrations.RunPython.noop)]

If the values are genuinely unrecoverable, fall back to clear_corrupt_prosemirror_rows to reset them to an empty document, or nullify_corrupt_prosemirror_rows if the field allows null:

from django_prosemirror.migration_utils import (
    clear_corrupt_prosemirror_rows,
    nullify_corrupt_prosemirror_rows,
)

def migrate(apps, schema_editor):
    MyModel = apps.get_model("myapp", "MyModel")
    # For non-nullable fields
    clear_corrupt_prosemirror_rows(MyModel, "body")
    # For nullable fields
    nullify_corrupt_prosemirror_rows(MyModel, "summary")

class Migration(migrations.Migration):
    dependencies = [("myapp", "0001_initial")]
    operations = [migrations.RunPython(migrate, migrations.RunPython.noop)]

6 Local development

Requirements for development:

  • Node.js (for building frontend assets)

  • All runtime requirements listed above

Setup for development:

python -mvirtualenv .venv
source .venv/bin/activate

# Install Python package in development mode
pip install -e .[tests,coverage,docs,release]

# Install Node.js dependencies
npm install

# Build frontend assets (when making changes to JavaScript)
./build.sh

When running management commands via django-admin, make sure to add the root directory to the python path (or use python -m django <command>):

export PYTHONPATH=. DJANGO_SETTINGS_MODULE=testapp.settings
django-admin migrate
django-admin createsuperuser  # optional
django-admin runserver

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

maykin_django_prosemirror-0.7.0.tar.gz (166.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

maykin_django_prosemirror-0.7.0-py3-none-any.whl (157.2 kB view details)

Uploaded Python 3

File details

Details for the file maykin_django_prosemirror-0.7.0.tar.gz.

File metadata

File hashes

Hashes for maykin_django_prosemirror-0.7.0.tar.gz
Algorithm Hash digest
SHA256 a394c938bed4fc27727a02b3a490ff011b220e08bc7bce6aa4e0e99bb197e19b
MD5 3c88b7ca1d872a9c66b459e650c413da
BLAKE2b-256 d30972c755a96ebf5a8dbe7e60625afecd6509dbb28cf288ea57dca0f811ca00

See more details on using hashes here.

Provenance

The following attestation bundles were made for maykin_django_prosemirror-0.7.0.tar.gz:

Publisher: ci.yml on maykinmedia/django-prosemirror

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file maykin_django_prosemirror-0.7.0-py3-none-any.whl.

File metadata

File hashes

Hashes for maykin_django_prosemirror-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9e5985bec7020d61901809b54ef150bb1985a8a2af0722345ee252b163a72220
MD5 0f86bbf0119d4752164bc22d2eb3e7a3
BLAKE2b-256 7c142513d9fd190952b98b845d751b8f07faf475f3a85b2d28ce385c245b3697

See more details on using hashes here.

Provenance

The following attestation bundles were made for maykin_django_prosemirror-0.7.0-py3-none-any.whl:

Publisher: ci.yml on maykinmedia/django-prosemirror

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page