写在最上面的废话:
知道很多人都不太有耐心看完一长篇文章,所以这里说明一下,到0x03章节后,所有代码块都只要复制即可。遇到错误或不会地方再回头仔细看文字性说明。
ueditor是非常好的富文本编辑器,但是官方提供的代码只有php、jsp、asp和源码,即,对于这几类语言的支持是比较完整的,包括上传功能。但是对于Django就不是那么友好了,除了提供常规的编辑之外,如果想要上传功能,那么就要费一翻周折了。
ueditor官方的第三方插件中提供了针对Django的上传插件(DjangoUeditor),但是,经过研究发现,该插件是基于Django1.x和python2(大家都知道python2已经被宣布死刑了),在Django2.2和python3.7中存在很多问题,并且因为是安装在site-packages中的,所以配置起来有点麻烦(如,项目静态文件配置等),也不能作为项目部署,在开发环境中配置好后,要在线上环境中重新配置一次。所以,基于这个插件这么多问题,经过了一下午的折腾,借用该插件中件的核心文件,将其创建为一个Django app,并且修复了版本错误,现在分享给大家,希望能帮到大家。
官方:https://ueditor.baidu.com/website/download.html
pip install ueditor==1.4
safeScan
--safeScan
----urls.py
----settings.py
--ueditorUpload
----views.py
----usettings.py
----utils.py
----urls.py
----apps.py
--static
----ueditor
------各种文件静态文件
----upload #最后将上传文件,保存到此
创建上传app
python manage.py startapp ueditorUpload
下面两个url中的配置一定要这么写,尤其是路径和views,否则不能与ueditor中的上传代码关联。
# ueditorUpload/urls.py
from django.urls import path
from ueditorUpload import views
app_name="ueditor"
urlpatterns = [
path("upload", views.get_ueditor_controller, name="ueditor_upload"),
]
# 项目/urls.py
from django.urls import path,include
urlpatterns = [
......
path('ueditor/',include('ueditorUpload.urls',namespace="ueditor")), # 如果你也配置了namespace,那么就需要在ueditorUpload apps.py中配置name
]
这里的核心代码就是ueditor官方提供的第三方插件的改版,大家在使用的时候,只需要完全复制即可。如果需要修改静态路径的话,可以自行修改静态路径常量即可。不过这里需要说明的是,虽然经过修改后可以完美支持上传了,但不免还存在错误的地方没有修改到,所以当你copy的时候,还会报各种错误,请留言,或者将自己的修改点留下,帮助更多人,谢谢。
#coding:utf-8
from importlib import import_module
from django.http import HttpResponse
from . import usettings as USettings
import os
import json
from django.views.decorators.csrf import csrf_exempt
import datetime,random
import urllib
def get_path_format_vars():
return {
"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)
}
#保存上传的文件
def save_upload_file(PostFile,FilePath):
try:
f = open(FilePath, 'wb')
for chunk in PostFile.chunks():
f.write(chunk)
except Exception as E:
f.close()
return u"写入文件错误:"+ E.message
f.close()
return u"SUCCESS"
@csrf_exempt
def get_ueditor_settings(request):
return HttpResponse(json.dumps(USettings.UEditorUploadSettings,ensure_ascii=False), content_type="application/javascript")
@csrf_exempt
def get_ueditor_controller(request):
"""获取ueditor的后端URL地址 """
action=request.GET.get("action","")
reponseAction={
"config":get_ueditor_settings,
"uploadimage":UploadFile,
"uploadscrawl":UploadFile,
"uploadvideo":UploadFile,
"uploadfile":UploadFile,
"catchimage":catcher_remote_image,
"listimage":list_files,
"listfile":list_files
}
return reponseAction[action](request)
@csrf_exempt
def list_files(request):
"""列出文件"""
if request.method!="GET":
return HttpResponse(json.dumps(u"{'state:'ERROR'}") ,content_type="application/javascript")
#取得动作
action=request.GET.get("action","listimage")
allowFiles={
"listfile":USettings.UEditorUploadSettings.get("fileManagerAllowFiles",[]),
"listimage":USettings.UEditorUploadSettings.get("imageManagerAllowFiles",[])
}
listSize={
"listfile":USettings.UEditorUploadSettings.get("fileManagerListSize",""),
"listimage":USettings.UEditorUploadSettings.get("imageManagerListSize","")
}
listpath={
"listfile":USettings.UEditorUploadSettings.get("fileManagerListPath",""),
"listimage":USettings.UEditorUploadSettings.get("imageManagerListPath","")
}
#取得参数
list_size=int(request.GET.get("size",listSize[action]))
list_start=int(request.GET.get("start",0))
files=[]
root_path=os.path.join(USettings.MEDIA_ROOT,listpath[action]).replace("\\","/")
files=get_files(root_path,root_path,allowFiles[action])
if (len(files)==0):
return_info={
"state":u"未找到匹配文件!",
"list":[],
"start":list_start,
"total":0
}
else:
return_info={
"state":"SUCCESS",
"list":files[list_start:list_start+list_size],
"start":list_start,
"total":len(files)
}
return HttpResponse(json.dumps(return_info),content_type="application/javascript")
def get_files(root_path,cur_path, allow_types=[]):
files = []
items = os.listdir(cur_path)
for item in items:
# item=unicode(item)
item_fullname = os.path.join(root_path,cur_path, item).replace("\\", "/")
if os.path.isdir(item_fullname):
files.extend(get_files(root_path,item_fullname, allow_types))
else:
ext = os.path.splitext(item_fullname)[1]
is_allow_list= (len(allow_types)==0) or (ext in allow_types)
if is_allow_list:
files.append({
"url":urllib.parse.urljoin(USettings.MEDIA_URL ,os.path.join(os.path.relpath(cur_path,root_path),item).replace("\\","/" )),
"mtime":os.path.getmtime(item_fullname)
})
return files
@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:
#取得上传的文件
file=request.FILES.get(UploadFieldName,None)
if file is None:return HttpResponse(json.dumps(u"{'state:'ERROR'}") ,content_type="application/javascript")
upload_file_name=file.name
upload_file_size=file.size
#取得上传的文件的原始名称
upload_original_name,upload_original_ext=os.path.splitext(upload_file_name)
#文件类型检验
upload_allow_type={
"uploadfile":"fileAllowFiles",
"uploadimage":"imageAllowFiles",
"uploadvideo":"videoAllowFiles"
}
if action in upload_allow_type:
# 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=int(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"
}
path_format_var=get_path_format_vars()
path_format_var.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],path_format_var)
#所有检测完成后写入文件
if state=="SUCCESS":
if action=="uploadscrawl":
state=save_scrawl_file(request, os.path.join(OutputPath,OutputFile))
else:
#保存到文件中,如果保存错误,需要返回ERROR
upload_module_name = USettings.UEditorUploadSettings.get("upload_module", None)
if upload_module_name:
mod = import_module(upload_module_name)
state = mod.upload(file, OutputPathFormat)
else:
state = save_upload_file(file, os.path.join(OutputPath, OutputFile))
#返回数据
return_info = {
'url': urllib.parse.urljoin(USettings.MEDIA_URL , OutputPathFormat) , # 保存后的文件名称
'original': upload_file_name, #原始文件名
'type': upload_original_ext,
'state': state, #上传状态,成功时返回SUCCESS,其他任何值将原样返回至图片上传框中
'size': upload_file_size
}
return HttpResponse(json.dumps(return_info,ensure_ascii=False),content_type="application/javascript")
@csrf_exempt
def catcher_remote_image(request):
"""远程抓图,当catchRemoteImageEnable:true时,
如果前端插入图片地址与当前web不在同一个域,则由本函数从远程下载图片到本地
"""
if not request.method=="POST":
return HttpResponse(json.dumps( u"{'state:'ERROR'}"),content_type="application/javascript")
state="SUCCESS"
allow_type= list(request.GET.get("catcherAllowFiles",USettings.UEditorUploadSettings.get("catcherAllowFiles","")))
max_size=int(request.GET.get("catcherMaxSize",USettings.UEditorUploadSettings.get("catcherMaxSize",0)))
remote_urls=request.POST.getlist("source[]",[])
catcher_infos=[]
path_format_var=get_path_format_vars()
for remote_url in remote_urls:
#取得上传的文件的原始名称
remote_file_name=os.path.basename(remote_url)
remote_original_name,remote_original_ext=os.path.splitext(remote_file_name)
#文件类型检验
if remote_original_ext in allow_type:
path_format_var.update({
"basename":remote_original_name,
"extname":remote_original_ext[1:],
"filename":remote_original_name
})
#计算保存的文件名
o_path_format,o_path,o_file=get_output_path(request,"catcherPathFormat",path_format_var)
o_filename=os.path.join(o_path,o_file).replace("\\","/")
#读取远程图片文件
try:
remote_image=urllib.request.urlopen(remote_url)
#将抓取到的文件写入文件
try:
f = open(o_filename, 'wb')
f.write(remote_image.read())
f.close()
state="SUCCESS"
except Exception as E:
state=u"写入抓取图片文件错误:%s" % E.message
except Exception as E:
state=u"抓取图片错误:%s" % E.message
catcher_infos.append({
"state":state,
"url":urllib.parse.urljoin(USettings.MEDIA_URL , o_path_format),
"size":os.path.getsize(o_filename),
"title":os.path.basename(o_file),
"original":remote_file_name,
"source":remote_url
})
return_info={
"state":"SUCCESS" if len(catcher_infos) >0 else "ERROR",
"list":catcher_infos
}
return HttpResponse(json.dumps(return_info,ensure_ascii=False),content_type="application/javascript")
def get_output_path(request,path_format,path_format_var):
#取得输出文件的路径
OutputPathFormat=(request.GET.get(path_format,USettings.UEditorSettings["defaultPathFormat"]) % path_format_var).replace("\\","/")
#分解OutputPathFormat
OutputPath,OutputFile=os.path.split(OutputPathFormat) #80-80_20190910173915_792.jpg
OutputPath=os.path.join(USettings.MEDIA_ROOT,OutputPath)
if not OutputFile:#如果OutputFile为空说明传入的OutputPathFormat没有包含文件名,因此需要用默认的文件名
OutputFile=USettings.UEditorSettings["defaultPathFormat"] % path_format_var
OutputPathFormat=os.path.join(OutputPathFormat,OutputFile)
if not os.path.exists(OutputPath):
os.makedirs(OutputPath)
return ( OutputPathFormat,OutputPath,OutputFile)
#涂鸦功能上传处理
@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.write(base64.decodebytes(content.encode("utf-8")))
f.close()
state="SUCCESS"
except Exception as E:
state="写入图片文件错误:%s" % E.message
return state
#coding: utf-8
#文件大小类
class FileSize(object):
SIZE_UNIT={"Byte":1,"KB":1024,"MB":1048576,"GB":1073741824,"TB":1099511627776}
def __init__(self,size):
self._size=size # 官方插件self.size和size方法名称重复,会导致setter重复执行,导致RuntimeError: maximum recursion depth exceeded
@staticmethod
def Format(size):
import re
if isinstance(size,int):
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=int(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=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
usettings.py这个名称,是为了与全局里的其它配置文件名称区分开,在其它地方引用时,也要修改。这个文件里有两个设置静态路径的常量,MEDIA_ROOT = STATIC_ROOT + os.sep + "upload"
和MEDIA_URL = STATIC_URL + "upload/"
,这个常量可以在项目全局配置文件settings.py中设置,然后引用,这样可以保证全局一致,你也可以写死在这里。全局配置文件settings.py中这两个参数为:# STATIC_ROOT = os.path.join(BASE_DIR,"static") # linux 用这个,不能用/static/,这样会导入到根目录
,STATIC_ROOT = os.path.join(BASE_DIR,"d:\\workspace\\safeScan\\static") # windows 用这个,用上面那个会报错:(staticfiles.E002) The STATICFILES_DIRS setting should not contain the STATI
,STATIC_URL = '/static/'
#coding:utf-8
from django.conf import settings as gSettings #全局设置
from safeScan.settings import STATIC_URL,STATIC_ROOT
import os
MEDIA_ROOT = STATIC_ROOT + os.sep + "upload"
MEDIA_URL = STATIC_URL + "upload/"
#工具栏样式,可以添加任意多的模式
TOOLBARS_SETTINGS={
"besttome":[['source','undo', 'redo','bold', 'italic', 'underline','forecolor', 'backcolor','superscript','subscript',"justifyleft","justifycenter","justifyright","insertorderedlist","insertunorderedlist","blockquote",'formatmatch',"removeformat",'autotypeset','inserttable',"pasteplain","wordimage","searchreplace","map","preview","fullscreen"], ['insertcode','paragraph',"fontfamily","fontsize",'link', 'unlink','insertimage','insertvideo','attachment','emotion',"date","time"]],
"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,
"defaultPathFormat":"%(basename)s_%(datetime)s_%(rnd)s.%(extname)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 "config" in UEditorSettings:UEditorSettings.update(UserSettings["config"])
# if UserSettings.has_key("upload"):UEditorUploadSettings.update(UserSettings["upload"])
if "upload" in UserSettings:UEditorUploadSettings.update(UserSettings["upload"])
#读取用户Settings文件并覆盖默认配置
UpdateUserSettings()
#取得配置项参数
def GetUeditorSettings(key,default=None):
if UEditorSettings.has_key(key):
return UEditorSettings[key]
else:
return default
在我的线上环境中,有一个小小的bug,当使用单一图片上传时,需要在上传完图片后面加【空格】或【回车】等其它字符,否则保存文章时,图片失败,具体原因。【就是少了
】
<body class="view" contenteditable="true" spellcheck="false" style="overflow-y: hidden; height: 297px; cursor: text;">
<p>
<img src="/static/upload/658-170_20190911164838_254.png" title="" _src="/static/upload/658-170_20190911164838_254.png" alt="658-170.png">​
</p>
</body>
虽然这个配置视频和音乐都可以上传,但该插件使用的都是flash方式,在当前浏览器下,基本不要想flash了。