How CKEditor 5 is Configured for Rich Text Editing in Yappuccino
Yappuccino integrates CKEditor 5 through django-ckeditor-5 by layering configuration across Django settings, model fields, form widgets, and a custom upload endpoint that supports both local filesystem and AWS S3 storage backends.
The Yappuccino blog platform implements a production-ready rich text editing solution using CKEditor 5 within its Django architecture. This article examines the multi-layered configuration spanning settings, models, forms, and custom upload handlers that enable seamless content authoring with image support.
Architecture Overview
The CKEditor 5 integration in jafarbekyusupov/yappuccino follows a five-layer architecture that separates concerns between configuration, data persistence, UI rendering, file handling, and security:
- Settings layer (
blogpost/settings.py): Defines global editor configurations, storage backends, and upload paths - Model layer (
blog/models.py): Persists HTML content usingCKEditor5Field - Form layer (
blog/forms.py): Renders the editor widget viaCKEditor5Widget - Upload layer (
blog/ckeditor_views.py,blog/patch_ckeditor.py): Handles image uploads with storage abstraction - Permission layer (
blog/ckeditor_upload_permissions.py): Controls upload access rights
Settings Configuration
The core CKEditor 5 configuration resides in blogpost/settings.py, where the CKEDITOR_5_CONFIGS dictionary defines multiple editor profiles for different use cases.
Global Configuration
# blogpost/settings.py
CKEDITOR_5_FILE_STORAGE = DEFAULT_FILE_STORAGE # S3 in production, FileSystemStorage locally
CKEDITOR_5_UPLOAD_PATH = "uploads/"
CKEDITOR_RESTRICT_BY_USER = False # Uploads are not isolated per-user
CKEDITOR_BROWSE_SHOW_DIRS = True
CKEDITOR_5_CONFIGS = {
'default': {
'toolbar': [
'heading', '|', 'bold', 'italic', 'link',
'bulletedList', 'numberedList', 'blockQuote',
'imageUpload',
],
'uploadUrl': '/ckeditor5/upload/',
'image': {
'toolbar': [
'imageTextAlternative', '|',
'imageStyle:alignLeft', 'imageStyle:full',
'imageStyle:alignRight'
],
'styles': ['full', 'alignLeft', 'alignRight'],
'upload': {'types': ['jpeg', 'png', 'gif', 'jpg']},
},
},
'basic': {
'toolbar': ['bold', 'italic', 'link', 'bulletedList', 'numberedList', 'imageUpload'],
'uploadUrl': '/ckeditor5/upload/',
},
}
Storage Backend Selection
The configuration dynamically switches storage backends based on the DEBUG environment variable. In production, DEFAULT_FILE_STORAGE points to 'storages.backends.s3boto3.S3Boto3Storage', writing files to s3://<bucket>/uploads/. In development, the system falls back to Django's FileSystemStorage, storing uploads under <project>/media/uploads/.
Database Models
Rich text content is persisted using CKEditor5Field from the django-ckeditor-5 package. The field declaration references a specific configuration name from CKEDITOR_5_CONFIGS, allowing different models to use different toolbar configurations.
Post and SimplePost Implementation
# blog/models.py
from django_ckeditor_5.fields import CKEditor5Field
class Post(models.Model):
# … additional fields …
content = CKEditor5Field('Content', config_name='default')
# …
class SimplePost(models.Model):
# … additional fields …
content = CKEditor5Field('Content', config_name='basic')
# …
The Post model uses the full-featured default configuration with headings and image alignment tools, while SimplePost employs the streamlined basic configuration for lighter editing requirements. The stored value is standard HTML generated by the CKEditor 5 JavaScript runtime.
Form Integration
Forms instantiate the editor interface using CKEditor5Widget, which loads the required JavaScript bundles and maps the image upload button to the custom endpoint.
Widget Configuration
# blog/forms.py
from django_ckeditor_5.widgets import CKEditor5Widget
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content']
widgets = {
'content': CKEditor5Widget(
attrs={'placeholder': 'Write your article...'},
config_name='default',
),
}
class SimplePostForm(forms.ModelForm):
class Meta:
model = SimplePost
fields = ['title', 'content']
widgets = {
'content': CKEditor5Widget(config_name='basic')
}
The widget automatically injects the CKEditor 5 CSS and JavaScript resources into the template when {{ form.media }} is rendered.
Image Upload Handling
Yappuccino overrides the default upload behavior to ensure consistent storage backend usage and logging across both explicit views and internal library calls.
Custom Upload Endpoint
The ckeditor_upload view in blog/ckeditor_views.py handles multipart file uploads with validation:
# blog/ckeditor_views.py
@csrf_exempt
@login_required
def ckeditor_upload(request):
# 1. Verify POST method and file presence
# 2. Enforce file size < 5 MB and MIME type whitelist
# 3. Generate unique filename using UUID + original extension
# 4. Save via default_storage.save() to respect configured backend (S3 or local)
# 5. Return JSON response: {"url": "<file_url>", "uploaded": "1", "fileName": "<filename>"}
This implementation logs the upload path and active storage backend, facilitating debugging when switching between local development and S3 production environments.
Monkey-Patching for Consistency
To ensure all upload paths respect the custom storage configuration, blog/patch_ckeditor.py replaces the upstream django_ckeditor_5.views.upload_file function with the same logic used in the custom view. This guarantees that any internal library calls or future package updates maintain the desired permission and storage handling.
Security and Permissions
Upload security is managed through a decorator-based permission system located in blog/ckeditor_upload_permissions.py.
Current Permission Model
The user_has_upload_permission decorator currently acts as a pass-through, allowing any authenticated user to upload images. However, the structure is designed for extension:
# blog/ckeditor_upload_permissions.py
def user_has_upload_permission(view_func):
"""
Placeholder for upload permission logic.
Extend to enforce staff-only uploads or user-specific quotas.
"""
return view_func
The @login_required decorator on the ckeditor_upload view ensures only authenticated sessions can access the endpoint, while the whitelist in CKEDITOR_5_CONFIGS restricts acceptable file types to jpeg, png, gif, and jpg.
Summary
- Configuration hierarchy: CKEditor 5 behavior is controlled through
CKEDITOR_5_CONFIGSinblogpost/settings.py, supporting multiple toolbar profiles (default,basic, or custom) - Storage abstraction: The
CKEDITOR_5_FILE_STORAGEsetting enables seamless switching between local filesystem storage and AWS S3 without code changes - Model integration:
CKEditor5Fieldstores HTML content in database models, withconfig_nameparameter selecting the appropriate editor profile - Custom upload pipeline: The
ckeditor_uploadview inblog/ckeditor_views.pyhandles file validation, unique naming, and storage backend abstraction, whilepatch_ckeditor.pyensures consistency across all upload entry points - Extensible security: The permission decorator pattern in
blog/ckeditor_upload_permissions.pyprovides a hook for implementing role-based upload restrictions
Frequently Asked Questions
How do I add a custom toolbar configuration for specific models?
Define a new configuration dictionary in blogpost/settings.py under CKEDITOR_5_CONFIGS, then reference it in your model field:
# settings.py
CKEDITOR_5_CONFIGS['custom'] = {
'toolbar': ['heading', 'bold', 'italic', 'link'],
'uploadUrl': '/ckeditor5/upload/',
}
# models.py
content = CKEditor5Field('Content', config_name='custom')
Can I restrict image uploads to staff users only?
Yes. Modify the user_has_upload_permission decorator in blog/ckeditor_upload_permissions.py to check for request.user.is_staff or specific group membership, then apply it to the ckeditor_upload view in blog/ckeditor_views.py.
Where are uploaded images stored in production versus development?
In production (DEBUG=False), images are stored in the AWS S3 bucket configured in DEFAULT_FILE_STORAGE under the uploads/ path. In development, files are saved to the local filesystem at <project_root>/media/uploads/ due to the fallback to FileSystemStorage.
How does the system handle filename collisions during upload?
The ckeditor_upload view in blog/ckeditor_views.py generates unique filenames by combining a UUID with the original file extension, preventing collisions regardless of how many users upload files with identical names.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →