Установка Django Сkeditor 5
В терминале вводим следующую команду: pip install django-ckeditor-5
Результат установки:
(venv) PS C:\Users\Razilator\Desktop\Courses\App\backend> pip install django-ckeditor-5
Collecting django-ckeditor-5
Downloading django_ckeditor_5-0.2.0-py3-none-any.whl (1.9 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.9/1.9 MB 3.8 MB/s eta 0:00:00
Requirement already satisfied: Django>=2.2 in c:\users\razilator\desktop\courses\app\venv\lib\site-packages (from django-ckeditor-5) (4.1)
Requirement already satisfied: Pillow in c:\users\razilator\desktop\courses\app\venv\lib\site-packages (from django-ckeditor-5) (9.2.0)
Requirement already satisfied: asgiref<4,>=3.5.2 in c:\users\razilator\desktop\courses\app\venv\lib\site-packages (from Django>=2.2->django-ckeditor-5) (3.5.2)
Requirement already satisfied: tzdata in c:\users\razilator\desktop\courses\app\venv\lib\site-packages (from Django>=2.2->django-ckeditor-5) (2022.2)
Requirement already satisfied: sqlparse>=0.2.2 in c:\users\razilator\desktop\courses\app\venv\lib\site-packages (from Django>=2.2->django-ckeditor-5) (0.4.2)
Installing collected packages: django-ckeditor-5
Successfully installed django-ckeditor-5-0.2.0
Следующим шагом нам необходимо добавить Ckeditor 5 в установленные приложения в файле settings.py
backend/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',
'mptt',
'debug_toolbar',
'taggit',
'captcha',
'django_ckeditor_5',
'modules.blog.apps.BlogConfig',
'modules.system.apps.SystemConfig',
]
Отлично. Не забываем, настроить папки для статики в settings.py (делали в 12 уроке)
STATIC_URL = '/static/'
STATIC_ROOT = (BASE_DIR / 'static')
MEDIA_ROOT = (BASE_DIR / 'media')
MEDIA_URL = '/media/'
Также добавим в settings.py некоторые конфигурации для Ckeditor 5, в самый низ файла вставлю следующее
backend/settings.py
# Ckeditor Settings
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_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',
}
}
}
Добавим ссылку подключения в основной urls.py
backend/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from backend import settings
urlpatterns = [
path('ckeditor5/', include('django_ckeditor_5.urls')),
path('admin/', admin.site.urls),
path('s/', include('modules.system.urls')),
path('', include('modules.blog.urls')),
]
if settings.DEBUG:
urlpatterns = [path('__debug__/', include('debug_toolbar.urls'))] + urlpatterns
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Теперь перейдем к модели статьи и изменим два поля: short_description, full_description
modules/blog/models/articles.py
from django_ckeditor_5.fields import CKEditor5Field
class Article(AbstractBaseMeta):
title = models.CharField(verbose_name='Заголовок', max_length=255)
slug = models.SlugField(verbose_name='URL', max_length=255, blank=True)
category = TreeForeignKey('Category', on_delete=models.PROTECT, related_name='articles', verbose_name='Категория')
short_description = CKEditor5Field(max_length=300, verbose_name='Краткое описание', config_name='extends')
full_description = CKEditor5Field(verbose_name='Описание', config_name='extends')
author = models.ForeignKey(
User, verbose_name='Автор материала',
on_delete=models.PROTECT,
related_name='article_author'
)
updated_by = models.ForeignKey(User,
verbose_name='Автор обновления',
on_delete=models.PROTECT,
related_name='article_updated_by',
blank=True,
null=True
)
Пояснения:
- В модели меняем с models.TextField на импортированный CKEditor5Field
- Добавляем config_name='extends', который мы добавили в settings.py, его можно спокойно кастомизировать.
Проведем миграции:
(venv) PS C:\Users\Razilator\Desktop\Courses\App\backend> python manage.py makemigrations
Migrations for 'blog':
modules\blog\migrations\0006_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\Courses\App\backend> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions, sites, system, taggit
Running migrations:
Applying blog.0006_alter_article_full_description_and_more... OK
Отлично. Но это ещё не все, я бы хотел сохранять загружаемый изображения из Ckeditor 5 в определенное место. Давайте это настроим в файле utils.py из модуля system.
modules/system/services/utils.py
import os
from django.core.files.storage import FileSystemStorage
from backend import settings
from urllib.parse import urljoin
class CkeditorCustomStorage(FileSystemStorage):
"""
Кастомное расположение для медиа файлов редактора
"""
location = os.path.join(settings.MEDIA_ROOT, 'uploads/images/')
base_url = urljoin(settings.MEDIA_URL, 'uploads/images/')
А теперь добавим в settings.py новую настройку, я добавил после тех конфигов, что мы добавляли в начале урока в самый низ
backend/settings.py
CKEDITOR_5_FILE_STORAGE = 'modules.system.services.utils.CkeditorCustomStorage'
Посмотрим результат работы в админ панели:

Отлично. Но это ещё не все, чтобы наш html код правильно форматировался в описании, нам нужно добавить тег safe в шаблоны вывода контента статьи.
templates/modules/blog/articles/article-detail.html
{% extends 'main.html' %}
{% load static %}
{% block content %}
<div class="card border-0 mb-3">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<figure>
<img src="{{ article.get_thumbnail }}" width="200" alt="{{ article.title }}">
</figure>
</div>
<div class="col-md-8">
<h5 class="card-title">
{{ article.title }}
</h5>
<small class="card-subtitle">
{{ article.created_at }} / {{ article.category }}
</small>
<div class="card-text">
{{ article.full_description|safe }}
</div>
<hr/>
Добавил: <img src="{{ article.author.profile.get_avatar }}" class="rounded-circle" width="26" height="26"/> {{ article.author }}
<hr/>
<strong>Теги записи</strong>: {% for tag in article.tags.all %} <a href="{% url 'article-list-by-tags' tag.slug %}">{{ tag }}</a>, {% endfor %}
<hr/>
{% if article.reason %}{{ article.reason }} / обновил: {{ article.updated_by }}{% endif %}
</div>
</div>
</div>
</div>
<div class="card border-0">
<div class="card-body">
<h5 class="card-title">
Комментарии
</h5>
{% include 'modules/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 %}
{% block script %}
<script src="{% static 'custom/js/article-detail.js' %}"></script>
{% endblock %}
Пояснение:
- Добавил safe к
{{ article.full_description }}
Тоже самое для списка статей:
templates/modules/blog/articles/article-list.html
{% extends 'main.html' %}
{% block content %}
{% for article in articles %}
<div class="card mb-3">
<div class="row">
<div class="col-md-4">
<figure class="mb-0">
<img src="{{ article.get_thumbnail }}" class="img-fluid h-100" alt="{{ article.title }}">
</figure>
</div>
<div class="col-md-8">
<div class="card-body">
<h4 class="card-title"><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h4>
<a class="card-subtitle" href="{% url 'article-by-cat' article.category.pk article.category.slug %}">#{{ article.category }}</a> / <time>{{ article.created_at }}</time>
<p class="card-text">
{{ article.short_description|safe }}
</p>
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
Если вы следовали многим урокам по формам и их стилизации, то при добавлении статьи на самом сайте вы сможете столкнуться с проблемой того, что не будет видно редактор.
Во первых, нам нужно добавить в html файл добавления статьи {{ form.media }}
templates/modules/blog/articles/article-create.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 'article-create-view' %}" 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 %}
Тоже самое и в обновление статьи
templates/modules/blog/articles/article-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>Обновление статьи: {{ 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 %}
И отредактировать форму добавления статьи (если вы стилизировали форму путем инициализации):
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control',
'autocomplete': 'off'
})
modules/blog/forms/articles.py
class ArticleCreateForm(forms.ModelForm):
"""
Форма добавления статей на сайте
"""
class Meta:
model = Article
fields = (
'title',
'slug',
'category',
'short_description',
'full_description',
'thumbnail',
'meta_title',
'meta_keywords',
'meta_description',
'is_published',
)
recaptcha = ReCaptchaField(widget=ReCaptchaV2Checkbox, public_key=settings.RECAPTCHA_PUBLIC_KEY,
private_key=settings.RECAPTCHA_PRIVATE_KEY, label='Капча')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы
"""
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control',
'autocomplete': 'off'
})
self.fields['meta_title'].widget.attrs.update({
'placeholder': 'Введите мета-название для поисковой системы'
})
self.fields['title'].widget.attrs.update({
'placeholder': 'Заголовок статьи'
})
self.fields['slug'].widget.attrs.update({
'placeholder': 'Ссылка статьи (необязательно)'
})
self.fields['meta_description'].widget.attrs.update({
'placeholder': 'Введите небольшое описание в 300 символов для поисковой системы'
})
self.fields['meta_keywords'].widget.attrs.update({
'placeholder': 'Введите ключевые слова через запятую для поиска'
})
self.fields['category'].empty_label = 'Выберите категорию'
self.fields['is_published'].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
Добавил следующий фрагмент кода:
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
Теперь посмотрим на самом сайте как выглядит редактор:

Проверю подгрузку изображений:


Отлично, все работает как нам и надо. На этом урок по добавлению редактора и его правильной настройки закончен.