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

9 篇文章 0 订阅
9 篇文章 0 订阅


写在最开始:

本文适合已经对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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值