Django База [2023]: Добавляем редактор CKEditor 5 в Django 4.1 ✍️ #35
В этой статье мы установим CKEditor 5. Он предоставляет богатый набор функций для редактирования текста, таких как форматирование, вставка изображений, ссылок и многое другое в Django.
Если вы хотите выразить благодарность автору сайта, статей и курса по Django, вы можете сделать это по ссылке ниже:
Установка django-ckeditor-5
CKEditor устанавливаем с помощью слудующей команды в терминале: pip install django-ckeditor-5
.
Результат установки:
(venv) PS C:\Users\Razilator\Desktop\Base\backend> pip install django-ckeditor-5
Collecting django-ckeditor-5
Downloading django_ckeditor_5-0.2.4-py3-none-any.whl (1.9 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.9/1.9 MB 4.7 MB/s eta 0:00:00
Requirement already satisfied: Django>=2.2 in c:\users\razilator\desktop\base\venv\lib\site-packages (from django-ckeditor-5) (4.1.5)
Requirement already satisfied: Pillow in c:\users\razilator\desktop\base\venv\lib\site-packages (from django-ckeditor-5) (9.4.0)
Requirement already satisfied: asgiref<4,>=3.5.2 in c:\users\razilator\desktop\base\venv\lib\site-packages (from Django>=2.2->django-ckeditor-5) (3.6.0)
Requirement already satisfied: sqlparse>=0.2.2 in c:\users\razilator\desktop\base\venv\lib\site-packages (from Django>=2.2->django-ckeditor-5) (0.4.3)
Requirement already satisfied: tzdata in c:\users\razilator\desktop\base\venv\lib\site-packages (from Django>=2.2->django-ckeditor-5) (2022.7)
Installing collected packages: django-ckeditor-5
Successfully installed django-ckeditor-5-0.2.4
[notice] A new release of pip available: 22.3 -> 23.0.1
[notice] To update, run: python.exe -m pip install --upgrade pip
После установки добавляем в конфигурационный файл settings.py наше приложение:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'modules.blog.apps.BlogConfig',
'modules.system.apps.SystemConfig',
'mptt',
'debug_toolbar',
'taggit',
'captcha',
'django_ckeditor_5',
]
Примечание: у нас также должны правильно установлены настройки для media и статических файлов, делали мы это в этом уроке.
Добавление конфигурации CKEditor 5
Далее в наш конфигурационный файл необходимо добавить настройки для CKEditor, в конец файла settings.py добавляем настройки по умолчанию:
customColorPalette = [
{
'color': 'hsl(4, 90%, 58%)',
'label': 'Red'
},
{
'color': 'hsl(340, 82%, 52%)',
'label': 'Pink'
},
{
'color': 'hsl(291, 64%, 42%)',
'label': 'Purple'
},
{
'color': 'hsl(262, 52%, 47%)',
'label': 'Deep Purple'
},
{
'color': 'hsl(231, 48%, 48%)',
'label': 'Indigo'
},
{
'color': 'hsl(207, 90%, 54%)',
'label': 'Blue'
},
]
CKEDITOR_5_CUSTOM_CSS = 'path_to.css' # optional
CKEDITOR_5_FILE_STORAGE = "path_to_storage.CustomStorage" # optional
CKEDITOR_5_CONFIGS = {
'default': {
'toolbar': ['heading', '|', 'bold', 'italic', 'link',
'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', ],
},
'extends': {
'blockToolbar': [
'paragraph', 'heading1', 'heading2', 'heading3',
'|',
'bulletedList', 'numberedList',
'|',
'blockQuote',
],
'toolbar': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough',
'code','subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage',
'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', '|',
'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat',
'insertTable',],
'image': {
'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft',
'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'],
'styles': [
'full',
'side',
'alignLeft',
'alignRight',
'alignCenter',
]
},
'table': {
'contentToolbar': [ 'tableColumn', 'tableRow', 'mergeTableCells',
'tableProperties', 'tableCellProperties' ],
'tableProperties': {
'borderColors': customColorPalette,
'backgroundColors': customColorPalette
},
'tableCellProperties': {
'borderColors': customColorPalette,
'backgroundColors': customColorPalette
}
},
'heading' : {
'options': [
{ 'model': 'paragraph', 'title': 'Paragraph', 'class': 'ck-heading_paragraph' },
{ 'model': 'heading1', 'view': 'h1', 'title': 'Heading 1', 'class': 'ck-heading_heading1' },
{ 'model': 'heading2', 'view': 'h2', 'title': 'Heading 2', 'class': 'ck-heading_heading2' },
{ 'model': 'heading3', 'view': 'h3', 'title': 'Heading 3', 'class': 'ck-heading_heading3' }
]
}
},
'list': {
'properties': {
'styles': 'true',
'startIndex': 'true',
'reversed': 'true',
}
}
}
Обработка CKEditor 5 в urls.py
Далее нам нужно обработать представления CKEditor, в главном файле urls.py добавьте следующую строку подключения:
"""backend URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
urlpatterns = [
path('ckeditor5/', include('django_ckeditor_5.urls')),
path('admin/', admin.site.urls),
path('', include('modules.blog.urls')),
path('', include('modules.system.urls')),
]
if settings.DEBUG:
urlpatterns = [path('__debug__/', include('debug_toolbar.urls'))] + urlpatterns
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Изменение модели Article для работы редактора
Теперь переходим к модели Article, нам необходимо изменить TextField у полей full_description, short_description:
from django.db import models
from django_ckeditor_5.fields import CKEditor5Field
class Article(models.Model):
"""
Модель постов для сайта
"""
# Другие поля и функции...
short_description = CKEditor5Field(max_length=500, verbose_name='Краткое описание', config_name='extends')
full_description = CKEditor5Field(verbose_name='Полное описание', config_name='extends')
# Другие поля и функции...
Проведем миграции:
(venv) PS C:\Users\Razilator\Desktop\Base\backend> py manage.py makemigrations
Migrations for 'blog':
modules\blog\migrations\0005_alter_article_full_description_and_more.py
- Alter field full_description on article
- Alter field short_description on article
(venv) PS C:\Users\Razilator\Desktop\Base\backend> py manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions, sites, system, taggit
Running migrations:
Applying blog.0005_alter_article_full_description_and_more... OK
Давайте проверим редактор в админ-панеле:
Добаление редактора при добавлении и обновлении статей на сайте
Теперь необходимо обновить наши формы, для этого в файле forms.py нашего приложения blog проведем изменения:
from django import forms
from .models import Article, Comment
class ArticleCreateForm(forms.ModelForm):
"""
Форма добавления статей на сайте
"""
class Meta:
model = Article
fields = ('title', 'slug', 'category', 'short_description', 'full_description', 'thumbnail', 'status')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы под Bootstrap
"""
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({'class': 'form-control','autocomplete': 'off'})
self.fields['short_description'].widget.attrs.update({'class': 'form-control django_ckeditor_5'})
self.fields['full_description'].widget.attrs.update({'class': 'form-control django_ckeditor_5'})
self.fields['short_description'].required = False
self.fields['full_description'].required = False
class ArticleUpdateForm(ArticleCreateForm):
"""
Форма обновления статьи на сайте
"""
class Meta:
model = Article
fields = ArticleCreateForm.Meta.fields + ('updater', 'fixed')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы под Bootstrap
"""
super().__init__(*args, **kwargs)
self.fields['fixed'].widget.attrs.update({'class': 'form-check-input'})
self.fields['short_description'].widget.attrs.update({'class': 'form-control django_ckeditor_5'})
self.fields['full_description'].widget.attrs.update({'class': 'form-control django_ckeditor_5'})
self.fields['short_description'].required = False
self.fields['full_description'].required = False
Далее добавим {{ form.media }}
в HTML разметку articles_create.html, articles_update.html:
{% extends 'main.html' %}
{% block content %}
<div class="card mb-3 border-0 nth-shadow">
<div class="card-body">
<div class="card-title nth-card-title">
<h4>Добавление статьи</h4>
</div>
<form method="post" action="{% url 'articles_create' %}" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{{ form.as_p }}
<div class="d-grid gap-2 d-md-block mt-2">
<button type="submit" class="btn btn-dark">Добавить статью</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% extends 'main.html' %}
{% block content %}
<div class="card mb-3 border-0 nth-shadow">
<div class="card-body">
<div class="card-title nth-card-title">
<h4>Обновление статьи: {{ article.title }}</h4>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{{ form.as_p }}
<div class="d-grid gap-2 d-md-block mt-2">
<button type="submit" class="btn btn-dark">Обновить статью</button>
</div>
</form>
</div>
</div>
{% endblock %}
Корректировка шаблонов полной и краткой статьи
Для правильного отображения контента применяемого от редактора, необходимо в файле articles_detail.html, а также в articles_list.html к полям {{ article.full_description }}
и {{ article.short_description }}
добавить тег safe:
{% extends 'main.html' %}
{% load mptt_tags %}
{% block content %}
<div class="card mb-3 border-0 shadow-sm">
<div class="row">
<div class="col-4">
<img src="{{ article.thumbnail.url }}" class="card-img-top" alt="{{ article.title }}" />
</div>
<div class="col-8">
<div class="card-body">
<h5>{{ article.title }}</h5>
<p class="card-text">{{ article.full_description|safe }}</p>
Категория: <a href="{% url 'articles_by_category' article.category.slug %}">{{ article.category.title }}</a> / Добавил: {{ article.author.username }} / <small>{{ article.time_create }}</small>
</div>
</div>
</div>
{% if article.tags.all %}
<div class="card-footer border-0">
Теги записи: {% for tag in article.tags.all %} <a href="{% url 'articles_by_tags' tag.slug %}">{{ tag }}</a>, {% endfor %}
</div>
{% endif %}
</div>
<div class="card border-0">
<div class="card-body">
<h5 class="card-title">
Комментарии
</h5>
{% include 'blog/comments/comments_list.html' %}
</div>
</div>
{% endblock %}
{% block sidebar %}
<div class="card mb-2 border-0">
<div class="card-body">
<div class="card-title">
Похожие статьи
</div>
<div class="card-text">
<ul class="similar-articles">
{% for sim_article in similar_articles %}
<li><a href="{{ sim_article.get_absolute_url }}">{{ sim_article.title }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}
И соответственно в articles_list.html:
{% extends 'main.html' %}
{% block content %}
{% for article in articles %}
<div class="card mb-3">
<div class="row">
<div class="col-4">
<img src="{{ article.thumbnail.url }}" class="card-img-top" alt="{{ article.title }}">
</div>
<div class="col-8">
<div class="card-body">
<h5 class="card-title"><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h5>
<p class="card-text">{{ article.short_description|safe }}</p>
</hr>
Категория: <a href="{% url 'articles_by_category' article.category.slug %}">{{ article.category.title }}</a>
/ Добавил: {{ article.author.username }}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
Проверка редактора в деле
Дополнительно (может быть важно)
По умолчанию CKEditor сохраняет изображения в папку media. Выглядит это вот так:
Я считаю, что файлы редактора должны храниться в другой папке, например uploads и сохраняться по датам. Давайте настроим это:
Воспользуемся нашей папкой services, которую мы делали в этом уроке, и в файле utils.py мы добавим следующий код для кастомного расположения изображений:
import os
from django.core.files.storage import FileSystemStorage
from backend import settings
from urllib.parse import urljoin
from datetime import datetime
class CkeditorCustomStorage(FileSystemStorage):
"""
Кастомное расположение для медиа файлов редактора
"""
def get_folder_name(self):
return datetime.now().strftime('%Y/%m/%d')
def get_valid_name(self, name):
return name
def _save(self, name, content):
folder_name = self.get_folder_name()
name = os.path.join(folder_name, self.get_valid_name(name))
return super()._save(name, content)
location = os.path.join(settings.MEDIA_ROOT, 'uploads/')
base_url = urljoin(settings.MEDIA_URL, 'uploads/')
В примере выше метод get_folder_name
возвращает имя папки в формате год/месяц/день. Метод get_valid_name
позволяет модифицировать имя файла, если это необходимо. Метод _save
определяет новый путь к файлу, который включает дату и вызывает оригинальный метод _save
родительского класса.
Указываем в конфигурационном файле settings.py следующее свойство в самом конце:
CKEDITOR_5_FILE_STORAGE = 'modules.services.utils.CkeditorCustomStorage'
Попробуем добавить изображение в редакторе: