Django集成Ueditor富文本编辑器及少量功能定制详解

时间:2022-12-25 20:36:51


写在最开始:

本文适合已经对Python+Django1.7有一定了解的人,如果连Django视图向模板传入变量都不会的话,建议还是去学一些基础的东西再集成富文本编辑器比较好。

最近可能会写一两篇关于Django的文章,如果有兴趣,请移驾。

另外,经过一段时间的文献检索,发现Django恶心的地方在于旧版和新版的目录结构是不一样的(函数、参数变化什么的就不用说了,肯定是要改变的嘛),所以网上经常看到用1.5之前的例子写的东西,不适合用。


由于最近突然奇想想做一个wiki网站,而采用的网站框架是python的Django,所以在这方面做了一些小小的尝试。

关于富文本编辑器,这里有很多选择,几乎是html前端和后端分离的开源编辑器都是可以集成的Django里面的——因为限制实在比较少。

因为某种特别的原因,使用Ueditor比较多,而且也觉得这个富文本编辑器非常好用,功能非常强大,所以就选择了这个编辑器了。

基于对开发团队的敬仰,先附上编辑器的官网:

http://ueditor.baidu.com/website/index.html

另外,如果有兴趣,可以进入到这个页面进行查看,现在已经不能通过连接方式进入了(^_^):

http://ueditor.baidu.com/website/index.html

还有另外,Ueditor是有个第三方插件的,叫做DjangoUeditor,具体可以看页面介绍:

http://ueditor.baidu.com/website/thirdproject.html

不过需要安装,看说明文档,说配置很简单,照葫芦画瓢弄死活弄不好(很有可能是由于Django的版本问题,目录变更了),所以放弃使用了。

前台的功能大部分不需要写后台代码支撑(大赞一个),就是图片上传、文件上传、涂鸦这三个功能需要写后台支持。


一、大体集成思路。

集成方式是抽取Ueditor的前端(js+css+img),然后通过Django的views方式书写后端处理方法,最后通过urls载入这些方法,反馈到模板展现给用户。


二、集成步骤。

1. 下载任何一个版本的Ueditor,选择自己喜欢的编码版本,这里以1.4.3ASP(UTF-8)版为例。

2. 解压到任意目录之后,把整个目录(Ueditor)放到  your_project/you_app/ 下。

3. 参考ueditor/index.html里面的内容,可以添加ueditor到你的页面(假设页面名称为Ueditor.html)。

<!--ueditor配置-->
<script type="text/javascript" charset="utf-8" src="/static/ueditor/ueditor.config.js"></script>
<script type="text/javascript" charset="utf-8" src="/static/ueditor/ueditor.all.min.js"></script>
<script type="text/javascript" charset="utf-8" src="/static/ueditor/lang/zh-cn/zh-cn.js"></script>
<!--这里的toolbar在django中传入参数,也可以使用默认的toolbar,具体可以参考index.html-->
<input id="toolbar" type="text" value="{{toolbar}}" style="display:none" />
<script type="text/javascript" charset="utf-8">
<span style="white-space:pre"> </span>var editor = UE.getEditor('container',{
<span style="white-space:pre"> </span>toolbars: eval('('+document.getElementById('toolbar').value+')') //用eval转换为对象,使之展现为json数据格式
<span style="white-space:pre"> </span>});
</script>
<!--实例化Ueditor--><script id="container" name="content" charset="utf-8" type="text/plain" style="height:200px;"></script>

4. 把toolbar设置成如下内容后传入模板中,并测试页面显示效果,直到可用之后才进行后面的步骤。

return render_to_response('Ueditor.html',{
'toolbar':[['fullscreen', 'source', '|', 'undo', 'redo', '|', 'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|','rowspacingtop', 'rowspacingbottom', '|', 'lineheight', 'pagebreak', 'template', 'background', '|', 'preview', 'searchreplace', 'help' , '|','paragraph', 'fontfamily', 'fontsize', '|','directionalityltr', 'directionalityrtl', 'indent', '|','justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 'touppercase', 'tolowercase', '|','link', 'unlink', 'anchor', '|', 'imagenone', 'imageleft', 'imageright', 'imagecenter', '|', 'insertimage', 'attachment', 'scrawl', 'emotion', 'map', 'gmap', 'insertframe', 'insertcode', '|','horizontal', 'date', 'time', 'spechars', 'wordimage', '|','inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols']]
})


5. 在app下面增加Ueditor目录(纯粹方便管理),并增加settings.py、utils.py、views.py

写在前面:以下内容基本都是抄袭(^_^),目前还不懂得用python来处理json

settings.py:

#coding:utf-8
from django.conf import settings as gSettings #全局设置

#工具栏样式,可以添加任意多的模式
TOOLBARS_SETTINGS={
"besttome":[['fullscreen', 'source', '|', 'undo', 'redo', '|', 'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|','rowspacingtop', 'rowspacingbottom', '|', 'lineheight', 'pagebreak', 'template', 'background', '|', 'preview', 'searchreplace', 'help' , '|','paragraph', 'fontfamily', 'fontsize', '|','directionalityltr', 'directionalityrtl', 'indent', '|','justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 'touppercase', 'tolowercase', '|','link', 'unlink', 'anchor', '|', 'imagenone', 'imageleft', 'imageright', 'imagecenter', '|', 'insertimage', 'attachment', 'scrawl', 'emotion', 'map', 'gmap', 'insertframe', 'insertcode', '|','horizontal', 'date', 'time', 'spechars', 'wordimage', '|','inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols']],
"mini":[['source','|','undo', 'redo', '|','bold', 'italic', 'underline','formatmatch','autotypeset', '|', 'forecolor', 'backcolor','|', 'link', 'unlink','|','simpleupload','attachment']],
"normal":[['source','|','undo', 'redo', '|','bold', 'italic', 'underline','removeformat', 'formatmatch','autotypeset', '|', 'forecolor', 'backcolor','|', 'link', 'unlink','|','simpleupload', 'emotion','attachment', '|','inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols']]
}

#默认的Ueditor设置,请参见ueditor.config.js
UEditorSettings={
"toolbars":TOOLBARS_SETTINGS["normal"],
"autoFloatEnabled":False,
#"defaultFileFormat":"%(basename)s_%(datetime)s_%(rnd)s.%(extname)s", #默认保存上传文件的命名方式
"defaultFileFormat":"%(datetime)s_%(rnd)s.%(extname)s", #默认保存上传文件的命名方式(时间_随机数.扩展名),由于apache常常出现字符不兼容问题,建议不要用中文的字样为好。
"defaultSubFolderFormat":"%(year)s%(month)s",
}
#请参阅php文件夹里面的config.json进行配置
UEditorUploadSettings={
#上传图片配置项
"imageActionName": "uploadimage", #执行上传图片的action名称
"imageMaxSize": 10485760, #上传大小限制,单位B,10M
"imageFieldName": "upfile", #* 提交的图片表单名称 */
"imageUrlPrefix":"",
"imagePathFormat":"",
"imageAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], #上传图片格式显示

#涂鸦图片上传配置项 */
"scrawlActionName": "uploadscrawl", #执行上传涂鸦的action名称 */
"scrawlFieldName": "upfile", #提交的图片表单名称 */
"scrawlMaxSize": 10485760, #上传大小限制,单位B 10M
"scrawlUrlPrefix":"",
"scrawlPathFormat":"",

#截图工具上传 */
"snapscreenActionName": "uploadimage", #执行上传截图的action名称 */
"snapscreenPathFormat":"",
"snapscreenUrlPrefix":"",

#抓取远程图片配置 */
"catcherLocalDomain": ["127.0.0.1", "localhost", "img.baidu.com"],
"catcherPathFormat":"",
"catcherActionName": "catchimage", #执行抓取远程图片的action名称 */
"catcherFieldName": "source", #提交的图片列表表单名称 */
"catcherMaxSize": 10485760, #上传大小限制,单位B */
"catcherAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], #抓取图片格式显示 */
"catcherUrlPrefix":"",
#上传视频配置 */
"videoActionName": "uploadvideo", #执行上传视频的action名称 */
"videoPathFormat":"",
"videoFieldName": "upfile", # 提交的视频表单名称 */
"videoMaxSize": 102400000, #上传大小限制,单位B,默认100MB */
"videoUrlPrefix":"",
"videoAllowFiles": [
".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid"], #上传视频格式显示 */

#上传文件配置 */
"fileActionName": "uploadfile", #controller里,执行上传视频的action名称 */
"filePathFormat":"",
"fileFieldName": "upfile",#提交的文件表单名称 */
"fileMaxSize": 204800000, #上传大小限制,单位B,200MB */
"fileUrlPrefix": "",#文件访问路径前缀 */
"fileAllowFiles": [
".png", ".jpg", ".jpeg", ".gif", ".bmp",
".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid",
".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"
], #上传文件格式显示 */

#列出指定目录下的图片 */
"imageManagerActionName": "listimage", #执行图片管理的action名称 */
"imageManagerListPath":"",
"imageManagerListSize": 30, #每次列出文件数量 */
"imageManagerAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], #列出的文件类型 */
"imageManagerUrlPrefix": "",#图片访问路径前缀 */

#列出指定目录下的文件 */
"fileManagerActionName": "listfile", #执行文件管理的action名称 */
"fileManagerListPath":"",
"fileManagerUrlPrefix": "",
"fileManagerListSize": 30, #每次列出文件数量 */
"fileManagerAllowFiles": [
".png", ".jpg", ".jpeg", ".gif", ".bmp",".tif",".psd"
".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid",
".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml",
".exe",".com",".dll",".msi"
] #列出的文件类型 */
}


#更新配置:从用户配置文件settings.py重新读入配置UEDITOR_SETTINGS,覆盖默认
def UpdateUserSettings():
UserSettings=getattr(gSettings,"UEDITOR_SETTINGS",{}).copy()
if UserSettings.has_key("config"):UEditorSettings.update(UserSettings["config"])
if UserSettings.has_key("upload"):UEditorUploadSettings.update(UserSettings["upload"])

#读取用户Settings文件并覆盖默认配置
UpdateUserSettings()

utils.py:

#coding: utf-8

#文件大小类
class FileSize():
SIZE_UNIT={"Byte":1,"KB":1024,"MB":1048576,"GB":1073741824,"TB":1099511627776L}
def __init__(self,size):
self.size=long(FileSize.Format(size))

@staticmethod
def Format(size):
import re
if isinstance(size,int) or isinstance(size,long):
return size
else:
if not isinstance(size,str):
return 0
else:
oSize=size.lstrip().upper().replace(" ","")
pattern=re.compile(r"(\d*\.?(?=\d)\d*)(byte|kb|mb|gb|tb)",re.I)
match=pattern.match(oSize)
if match:
m_size, m_unit=match.groups()
if m_size.find(".")==-1:
m_size=long(m_size)
else:
m_size=float(m_size)
if m_unit!="BYTE":
return m_size*FileSize.SIZE_UNIT[m_unit]
else:
return m_size
else:
return 0

#返回字节为单位的值
@property
def size(self):
return self.size
@size.setter
def size(self,newsize):
try:
self.size=long(newsize)
except:
self.size=0

#返回带单位的自动值
@property
def FriendValue(self):
if self.size<FileSize.SIZE_UNIT["KB"]:
unit="Byte"
elif self.size<FileSize.SIZE_UNIT["MB"]:
unit="KB"
elif self.size<FileSize.SIZE_UNIT["GB"]:
unit="MB"
elif self.size<FileSize.SIZE_UNIT["TB"]:
unit="GB"
else:
unit="TB"

if (self.size % FileSize.SIZE_UNIT[unit])==0:
return "%s%s" % ((self.size / FileSize.SIZE_UNIT[unit]),unit)
else:
return "%0.2f%s" % (round(float(self.size) /float(FileSize.SIZE_UNIT[unit]) ,2),unit)

def __str__(self):
return self.FriendValue

#相加
def __add__(self, other):
if isinstance(other,FileSize):
return FileSize(other.size+self.size)
else:
return FileSize(FileSize(other).size+self.size)
def __sub__(self, other):
if isinstance(other,FileSize):
return FileSize(self.size-other.size)
else:
return FileSize(self.size-FileSize(other).size)
def __gt__(self, other):
if isinstance(other,FileSize):
if self.size>other.size:
return True
else:
return False
else:
if self.size>FileSize(other).size:
return True
else:
return False
def __lt__(self, other):
if isinstance(other,FileSize):
if other.size>self.size:
return True
else:
return False
else:
if FileSize(other).size > self.size:
return True
else:
return False
def __ge__(self, other):
if isinstance(other,FileSize):
if self.size>=other.size:
return True
else:
return False
else:
if self.size>=FileSize(other).size:
return True
else:
return False
def __le__(self, other):
if isinstance(other,FileSize):
if other.size>=self.size:
return True
else:
return False
else:
if FileSize(other).size >= self.size:
return True
else:
return False

views.py:

# -*- coding: utf-8 -*-
from csvt import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
import settings as USettings
import os
import json
import urllib
import datetime,random

def get_output_path(request,path_format,fileformatdict):
#取得输出文件的路径
OutputPathFormat=(request.GET.get(path_format,USettings.UEditorSettings["defaultFileFormat"]) % fileformatdict).replace("\\","/")

#分解OutputPathFormat
OutputFile=os.path.split(OutputPathFormat)[1]
if not OutputFile:#如果OutputFile为空说明传入的OutputPathFormat没有包含文件名,因此需要用默认的文件名
OutputFile=USettings.UEditorSettings["defaultFileFormat"] % fileformatdict
OutputPathFormat=os.path.join(OutputPathFormat,OutputFile)
#
subfolder = USettings.UEditorSettings['defaultSubFolderFormat'] % fileformatdict + '/'
OutputPath = settings.MEDIA_ROOT + subfolder
OutputPathFormat = subfolder + OutputPathFormat
if not os.path.exists(OutputPath):
os.makedirs(OutputPath)
return ( OutputPathFormat,OutputPath,OutputFile)

#保存上传的文件
def save_upload_file(PostFile,FilePath):
try:
print FilePath
f = open(FilePath, 'wb')
for chunk in PostFile.chunks():
f.write(chunk)
except Exception,E:
f.close()
return u"写入文件错误:"+ E.message
f.close()
return u"SUCCESS"

#涂鸦功能上传处理
@csrf_exempt
def save_scrawl_file(request,filename):
import base64
try:
content=request.POST.get(USettings.UEditorUploadSettings.get("scrawlFieldName","upfile"))
f = open(filename, 'wb')
f.write(base64.decodestring(content))
f.close()
state="SUCCESS"
except Exception,E:
state="写入图片文件错误:%s" % E.message
return state

@csrf_exempt
def UploadFile(request):
"""上传文件"""
if not request.method=="POST":
return HttpResponse(json.dumps(u"{'state:'ERROR'}"),content_type="application/javascript")

state="SUCCESS"
action=request.GET.get("action")

#上传文件
upload_field_name={
"uploadfile":"fileFieldName","uploadimage":"imageFieldName",
"uploadscrawl":"scrawlFieldName","catchimage":"catcherFieldName",
"uploadvideo":"videoFieldName",
}
UploadFieldName=request.GET.get(upload_field_name[action],USettings.UEditorUploadSettings.get(action,"upfile"))

#上传涂鸦,涂鸦是采用base64编码上传的,需要单独处理
if action=="uploadscrawl":
upload_file_name="scrawl.png"
upload_file_size=0
else:
#取得上传的文件
req_file=request.FILES.get(UploadFieldName,None)
if req_file is None:
return HttpResponse(json.dumps(u"{'state:'ERROR'}") ,content_type="application/javascript")
upload_file_name=req_file.name
upload_file_size=req_file.size

#取得上传的文件的原始名称
upload_original_name,upload_original_ext=os.path.splitext(upload_file_name)

#文件类型检验
upload_allow_type={
"uploadfile":"fileAllowFiles",
"uploadimage":"imageAllowFiles",
"uploadvideo":"videoAllowFiles"
}

if upload_allow_type.has_key(action):
allow_type= list(request.GET.get(upload_allow_type[action],USettings.UEditorUploadSettings.get(upload_allow_type[action],"")))
if not upload_original_ext.lower() in allow_type:
state=u"服务器不允许上传%s类型的文件。" % upload_original_ext

#大小检验
upload_max_size={
"uploadfile":"filwMaxSize",
"uploadimage":"imageMaxSize",
"uploadscrawl":"scrawlMaxSize",
"uploadvideo":"videoMaxSize"
}
max_size=long(request.GET.get(upload_max_size[action],USettings.UEditorUploadSettings.get(upload_max_size[action],0)))
if max_size!=0:
from utils import FileSize
MF=FileSize(max_size)
if upload_file_size>MF.size:
state=u"上传文件大小不允许超过%s。" % MF.FriendValue

#检测保存路径是否存在,如果不存在则需要创建
upload_path_format={
"uploadfile":"filePathFormat",
"uploadimage":"imagePathFormat",
"uploadscrawl":"scrawlPathFormat",
"uploadvideo":"videoPathFormat"
}

FileFormatDict = {
"year":datetime.datetime.now().strftime("%Y"),
"month":datetime.datetime.now().strftime("%m"),
"day":datetime.datetime.now().strftime("%d"),
"date": datetime.datetime.now().strftime("%Y%m%d"),
"time":datetime.datetime.now().strftime("%H%M%S"),
"datetime":datetime.datetime.now().strftime("%Y%m%d%H%M%S"),
"rnd":random.randrange(100,999)
}
fileformat = FileFormatDict
fileformat.update({
"basename":upload_original_name,
"extname":upload_original_ext[1:],
"filename":upload_file_name,
})

#取得输出文件的路径
OutputPathFormat,OutputPath,OutputFile=get_output_path(request,upload_path_format[action],fileformat)

#所有检测完成后写入文件
if state=="SUCCESS":
if action=="uploadscrawl":
state=save_scrawl_file(request,os.path.join(OutputPath,OutputFile))
else:
#保存到文件中,如果保存错误,需要返回ERROR
state=save_upload_file(req_file,os.path.join(OutputPath,OutputFile))

#返回数据
return_info = {
'url': urllib.basejoin(settings.MEDIA_URL , OutputPathFormat) , # 保存后的文件名称
'original': upload_file_name, #原始文件名
#'original': 'aa',
'type': upload_original_ext,
'state': state, #上传状态,成功时返回SUCCESS,其他任何值将原样返回至图片上传框中
'size': upload_file_size
}
return HttpResponse(json.dumps(return_info,ensure_ascii=False),content_type="application/javascript")

def uecontroller(req):
if req.GET['action'] in 'uploadimage|uploadfile|uploadscrawl':
return UploadFile(req)
elif req.GET['action'] == 'config':
return HttpResponse(json.dumps(USettings.UEditorUploadSettings,ensure_ascii=False), content_type="application/javascript")

6. 上述内容注意几个地方:

1)settings.MEDIA_ROOT,这个在Django1.7之后貌似就不再推荐使用了,所以默认用django-admin生成的settings里面是没有自带这个参数的设置的,这个在我的项目中,是这样设置的:

MEDIA_ROOT = os.path.join(BASE_DIR, 'upload/')

并且设置上传路径(在django1.7中)是和your_project、your_app目录平级的upload文件夹(具体参考后面的目录结构)。

2)csvt字样统统替换成你自己的your_project名字。

3)上面的内容实现了图片上传、附件上传、涂鸦三个功能(实测都能用)。

7. 在your_project下的urls.py增加如下内容:

url(r'^uecontroller/$','wiki.Ueditor.views.uecontroller'),  #文章编辑器Ueditor的控制中心(注意对应路径为your_app.Ueditor.views.<span style="font-family: Arial, Helvetica, sans-serif;">uecontroller</span><span style="font-family: Arial, Helvetica, sans-serif;">)</span>
url(r'^upload/(?P<path>.*)$','django.views.static.serve',{'document_root':BASE_DIR+'/upload'}), #上传根目录,就是settings.MEDIA_ROOT的内容
当然,你需要先from settings import BASE_DIR。

8. Enjoy it……


如有任何疑问,欢迎提出。

最后附上目录结构,由于目录内容太多,只显示一部分,剩下的大家自己yy:

顶层目录
│ db.sqlite3
│ manage.py

├─csvt /*备注:这是你的项目,就是多次提到的your_project*/
│ settings.py
│ urls.py
│ wsgi.py
│ __init__.py

├─upload
│ ├─201412
│ └─headimages

└─wiki /*备注:这是你的app,就是前面提到的your_app*/
│ admin.py
│ forms.py
│ models.py
│ views.py
│ __init__.py

├─static
│ ├─admin
│ │ ├─css
│ │ ├─img
│ │ └─js
│ ├─css
│ ├─images
│ ├─js
│ └─ueditor
│ │ index.html
│ │ ueditor.all.js
│ │ ueditor.all.min.js
│ │ ueditor.config.js
│ │ ueditor.parse.js
│ │ ueditor.parse.min.js
│ │
│ ├─dialogs
│ ├─lang
│ │ └─zh-cn
│ ├─themes
│ └─third-party
├─templates
│ 404.html
│ Ueditor.html

└─Ueditor
settings.py
utils.py
views.py
__init__.py


-------------------------------------------------------------------------------------------------------------------------

参考文献:

http://www.yihaomen.com/article/python/238.htm

http://www.phperz.com/article/14/1027/14177.html