Создание сайта на Django: Урок 38, добавление WYSIWYG редактора Сkeditor 5
avatar
7 | (offline)
❤️‍🔥Notehunter Developer
Добавлено:
Категория: Руководства «Django»
Комментариев: 0

Установка 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

Теперь посмотрим на самом сайте как выглядит редактор:

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

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

Комментарии к статье 0
Комментариев нет
Форма добавления комментария (необходима регистрация)