Административная панель Django очень удобна, но в ней не хватает хорошего редактора HTML, иными словами WYSIWYG 

В поиске есть инструкция по интеграции таких редакторов как tinymce или wymeditor, но первый очень громоздкий, а второй сбоит и глючит при вводе текста.

После сравнения редакторов  был выбран http://ckeditor.com/, ключевым фактором моего выбора была чистота кода, удобство настройки и русская версия.

В статье я покажу как его интегрировать в Django

Подключаем новый тип поля 

from sites.common.objects import TextField

Тип поля в моделе будет выглядеть так:

tag_full_descr = TextField.field( u"Полное описание", max_length=1024, null=True, blank=True)

Для возможности загрузки картинок на сервер добавьте в urls.py

from sites.common.objects import TextField

добавлям в обработчик урлов( файл urls.py)

,url(r'^uploader/admin_upload/$',TextField.upload)

также нужно создать файл '/js/ckeditor_init.js', он нужен для коректной работы редактора в InlineModelAdmin objects, и для инициализации редактора.

Файл ckeditor_init.js

$(document).ready(function() {
    $('.inline-related').live('DOMNodeInserted', function(){
        var textarea = $(this).find('textarea[ckeditor=ckeditor]' )
        enable_ckeditor( textarea )
    })
    function enable_ckeditor( obj ){
        obj.ckeditor({
              skin : 'v2'
             //,startupMode : 'source'
             ,startupOutlineBlocks : true
             ,filebrowserUploadUrl : '/uploader/admin_upload/?ref='+window.location.pathname
             ,toolbar :
                 [
                     { name: 'document', items : [ 'Maximize', 'Source','DocProps','Preview','RemoveFormat','-','Templates' ] },
                     { name: 'clipboard', items : [ 'Cut','Copy','Paste','PasteText','PasteFromWord','-','Undo','Redo' ] },
                     { name: 'links', items : [ 'Link','Unlink','Image','Table','HorizontalRule' ] },
                     '/',
                     { name: 'basicstyles', items : [ 'Bold','Italic','Underline','Strike','Subscript','Superscript'] },

                     { name: 'styles', items : [ 'Styles','Format','Font','FontSize' ] },
                     { name: 'colors', items : [ 'TextColor','BGColor' ] },
                     { name: 'paragraph', items : [ 'NumberedList','BulletedList','-','Outdent','Indent','-','Blockquote',
                     '-','JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock' ] },

                 ]
            })
    }
    $( 'textarea[ckeditor=ckeditor]' ).each( function(index){
        var obj = $(this)
        if (obj.attr('id').indexOf('__prefix__') == -1){
            enable_ckeditor( obj )
        }
    })
})


Файл TextField.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

from django import forms
from django.utils.safestring import mark_safe
from django.contrib.admin.widgets import AdminTextareaWidget
from django.contrib.admin.options import FORMFIELD_FOR_DBFIELD_DEFAULTS
from django.http import HttpResponse
from text import text ### используется функция translify, для транслитерации русских букв

import commonFunction ### используется getExceptionError(), для получения текста исключения
import commonException ### используется класс ошибок, вы можете использовать свой

from django.conf import settings
from django.db import models
from sites.common import ImageWorkshop ###используется 2 функции из библиотеки PIL( open и save )
import os





class field(models.TextField):
    pass



def upload( request, **kargs ):
    object_upload = request.GET['ref'].split('/')[-3] ### обычно в админке путь похожий на http://foothold.ru/admin/article_menu/article/23/, мы берем article, чтоб имя изображения боле более читабельным

    try:
        file_to_upload = request.FILES.get('upload')
        if file_to_upload == None:
            raise e_no_file
        try:
            valid_image_types = settings._valid_image_types #### с настройках можно указать валидные типы для загрузки
        except AttributeError:
            valid_image_types = ['image/png', 'image/gif', 'image/jpg', 'image/jpeg', 'image/pjpeg', 'image/x-png']
        if file_to_upload.content_type not in valid_image_types:
            raise e_bad_content_type( type = file_to_upload.content_type )

        original_path = "{0}/images/{1}".format(settings.DOCUMENT_ROOT,object_upload)

        if not os.path.exists( original_path ):
            os.makedirs( original_path )

        file_name_wo_ext, extension = os.path.splitext( file_to_upload.name )
        file_name_wo_ext = text.translify( file_name_wo_ext )
        extension = text.translify( extension )


        index = 0
        while True: #### если такой файл уже существует мы к имени добавим индекс
            if index > 0:
                file_name = "{0}-{1}.{2}".format(file_name_wo_ext, index, extension.strip('.'))
            else:
                file_name = "{0}.{1}".format(file_name_wo_ext, extension.strip('.'))
            index = index + 1
            full_file_name = original_path + '/' + file_name
            url_file_name = "/images/{0}/{1}".format(object_upload,file_name)
            if not os.path.exists( full_file_name ):
                break

        image_file = ImageWorkshop.open(file_to_upload)
        image_file = ImageWorkshop.scale_and_crop( image_file, ( 1000, 9000 ), {} ) ##### уменьшаем картинку чтоб пользователи не грузили большие файлы
        ImageWorkshop.save( image_file, full_file_name, quality=90)
        result ="""""".format(request.GET['CKEditorFuncNum'], url_file_name )
    except:
        error = commonFunction.getExceptionError()
        result ="""""".format(request.GET['CKEditorFuncNum'], '',error.replace('\n', ' ') )
    return HttpResponse(result)


class upload_exception( commonException.exception):
    pass

class e_no_file( upload_exception ):
    _message = u"Нет файла для загрузки"
class e_bad_content_type( upload_exception ):
    _message = u"Тип файлов {type} не могут быть загружен"




class textFieldTextarea(forms.Textarea):
    def render(self, name, value, attrs=None):
        #print self
        #print type(name), type(value)
        if value is not None:
            try:
                value = value.raw
            except AttributeError:
                pass
        return super(textFieldTextarea, self).render(name, value, attrs)


class textFieldWidget(textFieldTextarea):

    def _media(self):
        return forms.Media(
            js=( '/js/jquery.min.js'
                ,'/js/ckeditor/ckeditor.js'
                ,'/js/ckeditor/adapters/jquery.js'
                ,'/js/ckeditor_init.js'
                )
            )
    media = property(_media)

    def render(self, name, value, attrs=None):
        attrs['ckeditor'] = "ckeditor"
        html = super(textFieldWidget, self).render(name, value, attrs)
        return mark_safe(html)


class AdmintextFieldWidget(textFieldWidget, AdminTextareaWidget):
    pass


FORMFIELD_FOR_DBFIELD_DEFAULTS[field] = {'widget': AdmintextFieldWidget}