diff --git a/CHANGES.md b/CHANGES.md index 13bf23f..b10d43f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,17 @@ ## 版本更新记录 -### v0.5.5 +### v0.5.5 2020-07-20 - 禁用编辑器页面的列表目录和下拉目录语法解析 - Chrome扩展添加鼠标选择控制选项 - 优化文档样式 +- 添加文集导入功能 +- 优化文集目录加载速度 +- 编辑器支持音视频外链和视频网站外链 +- 编辑器添加字符统计功能 +- 优化文档页面分享样式 +- 优化文档编辑器排版布局 +- 新增图片上传的URL链接插入 ### v0.5.4 diff --git a/MrDoc/settings.py b/MrDoc/settings.py index 458e152..9c5a2b9 100644 --- a/MrDoc/settings.py +++ b/MrDoc/settings.py @@ -40,7 +40,7 @@ SECRET_KEY = '5&71mt9@^58zdg*_!t(x6g14q*@84d%ptr%%s6e0l50zs0we3d' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = CONFIG.getboolean('site','debug') -VERSIONS = '0.5.4' +VERSIONS = '0.5.5' ALLOWED_HOSTS = ['*'] diff --git a/app_doc/import_utils.py b/app_doc/import_utils.py new file mode 100644 index 0000000..a00ab79 --- /dev/null +++ b/app_doc/import_utils.py @@ -0,0 +1,147 @@ +# coding:utf-8 +# @文件: import_utils.py +# @创建者:州的先生 +# #日期:2020/6/17 +# 博客地址:zmister.com +# 文集导入相关方法 + +import shutil +import os +import time +import re +from app_doc.models import Doc,Project,Image +from app_doc.util_upload_img import upload_generation_dir +from django.db import transaction +from django.conf import settings +from loguru import logger + +# 导入Zip文集 +class ImportZipProject(): + # 读取 Zip 压缩包 + def read_zip(self,zip_file_path,create_user): + # 导入流程: + # 1、解压zip压缩包文件到temp文件夹 + # 2、遍历temp文件夹内的解压后的.md文件 + # 3、读取.md文件的文本内容 + # 4、如果里面匹配到相对路径的静态文件,从指定文件夹里面读取 + # 5、上传图片,写入数据库,修改.md文件里面的url路径 + + # 新建一个临时文件夹,用于存放解压的文件 + self.temp_dir = zip_file_path[:-3] + os.mkdir(self.temp_dir) + # 解压 zip 文件到指定临时文件夹 + shutil.unpack_archive(zip_file_path, extract_dir=self.temp_dir) + + # 处理文件夹和文件名的中文乱码 + for root, dirs, files in os.walk(self.temp_dir): + for dir in dirs: + try: + new_dir = dir.encode('cp437').decode('gbk') + except: + new_dir = dir.encode('utf-8').decode('utf-8') + # print(new_dir) + os.rename(os.path.join(root, dir), os.path.join(root, new_dir)) + + for file in files: + try: + new_file = file.encode('cp437').decode('gbk') + except: + new_file = file.encode('utf-8').decode('utf-8') + # print(root, new_file) + os.rename(os.path.join(root, file), os.path.join(root, new_file)) + + # 开启事务 + with transaction.atomic(): + save_id = transaction.savepoint() + try: + # 新建文集 + project = Project.objects.create( + name=zip_file_path[:-4].split('/')[-1], + intro='', + role=1, + create_user=create_user + ) + # 遍历临时文件夹中的所有文件和文件夹 + for f in os.listdir(self.temp_dir): + # 获取 .md 文件 + if f.endswith('.md'): + # print(f) + # 读取 .md 文件文本内容 + with open(os.path.join(self.temp_dir,f),'r',encoding='utf-8') as md_file: + md_content = md_file.read() + md_content = self.operat_md_media(md_content,create_user) + # 新建文档 + doc = Doc.objects.create( + name = f[:-3], + pre_content = md_content, + top_doc = project.id, + status = 0, + create_user = create_user + ) + except: + logger.exception("解析导入文件异常") + # 回滚事务 + transaction.savepoint_rollback(save_id) + + transaction.savepoint_commit(save_id) + try: + shutil.rmtree(self.temp_dir) + os.remove(zip_file_path) + return project.id + except: + logger.exception("删除临时文件异常") + return None + + + # 处理MD内容中的静态文件 + def operat_md_media(self,md_content,create_user): + # 查找MD内容中的静态文件 + pattern = r"\!\[.*?\]\(.*?\)" + media_list = re.findall(pattern, md_content) + # print(media_list) + # 存在静态文件,进行遍历 + if len(media_list) > 0: + for media in media_list: + media_filename = media.split("(")[-1].split(")")[0] # 媒体文件的文件名 + # 存在本地图片路径 + if media_filename.startswith("./"): + # 获取文件后缀 + file_suffix = media_filename.split('.')[-1] + if file_suffix.lower() not in settings.ALLOWED_IMG: + continue + # 判断本地图片路径是否存在 + temp_media_file_path = os.path.join(self.temp_dir,media_filename[2:]) + if os.path.exists(temp_media_file_path): + # 如果存在,上传本地图片 + print(media_filename) + dir_name = upload_generation_dir() # 获取当月文件夹名称 + + # 复制文件到媒体文件夹 + copy2_filename = dir_name + '/' + str(time.time()) + '.' + file_suffix + new_media_file_path = shutil.copy2( + temp_media_file_path, + settings.MEDIA_ROOT + copy2_filename + ) + + # 替换MD内容的静态文件链接 + new_media_filename = new_media_file_path.split(settings.MEDIA_ROOT,1)[-1] + new_media_filename = '/media' + new_media_filename + + # 图片数据写入数据库 + Image.objects.create( + user=create_user, + file_path=new_media_filename, + file_name=str(time.time())+'.'+file_suffix, + remark='本地上传', + ) + md_content = md_content.replace(media_filename, new_media_filename) + else: + pass + return md_content + # 不存在静态文件,直接返回MD内容 + else: + return md_content + +if __name__ == '__main__': + imp = ImportZipProject() + imp.read_zip(r"D:\Python XlsxWriter模块中文文档_2020-06-16.zip") \ No newline at end of file diff --git a/app_doc/import_views.py b/app_doc/import_views.py new file mode 100644 index 0000000..4080f57 --- /dev/null +++ b/app_doc/import_views.py @@ -0,0 +1,118 @@ +# coding:utf-8 +# @文件: import_views.py +# @创建者:州的先生 +# #日期:2020/6/17 +# 博客地址:zmister.com +# 文集导入相关视图函数 + +from django.shortcuts import render,redirect +from django.http.response import JsonResponse,Http404,HttpResponseNotAllowed,HttpResponse +from django.http import HttpResponseForbidden +from django.contrib.auth.decorators import login_required # 登录需求装饰器 +from django.views.decorators.http import require_http_methods,require_GET,require_POST # 视图请求方法装饰器 +from django.core.paginator import Paginator,PageNotAnInteger,EmptyPage,InvalidPage # 后端分页 +from django.core.exceptions import PermissionDenied,ObjectDoesNotExist +from app_doc.models import Project,Doc,DocTemp +from django.contrib.auth.models import User +from django.db.models import Q +from django.db import transaction +from loguru import logger +import datetime +import traceback +import re +from app_doc.report_utils import * +from app_admin.decorators import check_headers,allow_report_file +import os.path +import json +from app_doc.import_utils import * + +# 导入文集 +@login_required() +@require_http_methods(['GET','POST']) +def import_project(request): + if request.method == 'GET': + return render(request,'app_doc/manage_project_import.html',locals()) + elif request.method == 'POST': + file_type = request.POST.get('type',None) + # 上传Zip压缩文件 + if file_type == 'zip': + import_file = request.FILES.get('import_file',None) + if import_file: + file_name = import_file.name + # 限制文件大小在50mb以内 + if import_file.size > 52428800: + return JsonResponse({'status': False, 'data': '文件大小超出限制'}) + # 限制文件格式为.zip + if file_name.endswith('.zip'): + if os.path.exists(os.path.join(settings.MEDIA_ROOT,'import_temp')) is False: + os.mkdir(os.path.join(settings.MEDIA_ROOT,'import_temp')) + temp_file_name = str(time.time())+'.zip' + temp_file_path = os.path.join(settings.MEDIA_ROOT,'import_temp/'+temp_file_name) + with open(temp_file_path,'wb+') as zip_file: + for chunk in import_file: + zip_file.write(chunk) + if os.path.exists(temp_file_path): + import_file = ImportZipProject() + project = import_file.read_zip(temp_file_path,request.user) + if project: + docs = Doc.objects.filter(top_doc=project).values_list('id','name') + doc_list = [doc for doc in docs] + return JsonResponse({'status':True,'data':doc_list,'id':project}) + else: + return JsonResponse({'status':False,'data':'上传失败'}) + else: + return JsonResponse({'status':False,'data':'上传失败'}) + else: + return JsonResponse({'status':False,'data':'仅支持.zip格式'}) + else: + return JsonResponse({'status':False,'data':'无有效文件'}) + else: + return JsonResponse({'status':False,'data':'参数错误'}) + + +# 文集文档排序 +@login_required() +@require_http_methods(['POST']) +def project_doc_sort(request): + project_id = request.POST.get('pid',None) # 文集ID + title = request.POST.get('title',None) # 文集名称 + desc = request.POST.get('desc',None) # 文集简介 + role = request.POST.get('role',1) # 文集权限 + sort_data = request.POST.get('sort_data','[]') # 文档排序列表 + doc_status = request.POST.get('status',0) # 文档状态 + # print(sort_data) + try: + sort_data = json.loads(sort_data) + except Exception: + return JsonResponse({'status':False,'data':'文档参数错误'}) + + try: + Project.objects.get(id=project_id,create_user=request.user) + except ObjectDoesNotExist: + return JsonResponse({'status':False,'data':'没有匹配的文集'}) + + # 修改文集信息 + Project.objects.filter(id=project_id).update( + name = title, + intro = desc, + role = role + ) + # 文档排序 + n = 10 + # 第一级文档 + for data in sort_data: + Doc.objects.filter(id=data['id']).update(sort = n,status=doc_status) + n += 10 + # 存在第二级文档 + if 'children' in data.keys(): + n1 = 10 + for c1 in data['children']: + Doc.objects.filter(id=c1['id']).update(sort = n1,parent_doc=data['id'],status=doc_status) + n1 += 10 + # 存在第三级文档 + if 'children' in c1.keys(): + n2 = 10 + for c2 in c1['children']: + Doc.objects.filter(id=c2['id']).update(sort=n2,parent_doc=c1['id'],status=doc_status) + + return JsonResponse({'status':True,'data':'ok'}) diff --git a/app_doc/urls.py b/app_doc/urls.py index ab2edfe..8725f4b 100644 --- a/app_doc/urls.py +++ b/app_doc/urls.py @@ -1,5 +1,5 @@ from django.urls import path,re_path -from app_doc import views,util_upload_img +from app_doc import views,util_upload_img,import_views urlpatterns = [ path('',views.project_list,name='pro_list'),# 文档首页 @@ -19,6 +19,8 @@ urlpatterns = [ path('check_viewcode/',views.check_viewcode,name='check_viewcode'),# 文集访问码验证 path('manage_project_colla//',views.manage_project_collaborator,name="manage_pro_colla"), # 管理文集协作 path('manage_pro_colla_self/',views.manage_pro_colla_self,name="manage_pro_colla_self"), # 我协作的文集 + path('manage_project_import/',import_views.import_project,name="import_project"), # 导入文集 + path('manage_project_doc_sort/',import_views.project_doc_sort,name='project_doc_sort'), # 导入文集文档排序 #################文档相关 path('project-/doc-/', views.doc, name='doc'), # 文档浏览页 path('create_doc/', views.create_doc, name="create_doc"), # 新建文档 diff --git a/app_doc/views.py b/app_doc/views.py index aa7ccdb..3ecbb6e 100644 --- a/app_doc/views.py +++ b/app_doc/views.py @@ -1298,47 +1298,52 @@ def get_pro_doc(request): def get_pro_doc_tree(request): pro_id = request.POST.get('pro_id', None) if pro_id: - # 获取一级文档 + # 查询存在上级文档的文档 + parent_id_list = Doc.objects.filter(top_doc=pro_id,status=1).exclude(parent_doc=0).values_list('parent_doc',flat=True) + # 获取存在上级文档的上级文档ID + # print(parent_id_list) doc_list = [] + # 获取一级文档 top_docs = Doc.objects.filter(top_doc=pro_id,parent_doc=0,status=1).values('id','name').order_by('sort') + # 遍历一级文档 for doc in top_docs: top_item = { 'id':doc['id'], 'field':doc['name'], 'title':doc['name'], - 'href':'/project-{}/doc-{}/'.format(pro_id,doc['id']), 'spread':True, 'level':1 } - # 获取二级文档 - sec_docs = Doc.objects.filter(top_doc=pro_id,parent_doc=doc['id'],status=1).values('id','name').order_by('sort') - if sec_docs.exists():# 二级文档 + # 如果一级文档存在下级文档,查询其二级文档 + if doc['id'] in parent_id_list: + # 获取二级文档 + sec_docs = Doc.objects.filter(top_doc=pro_id,parent_doc=doc['id'],status=1).values('id','name').order_by('sort') top_item['children'] = [] for doc in sec_docs: sec_item = { 'id': doc['id'], 'field': doc['name'], 'title': doc['name'], - 'href': '/project-{}/doc-{}/'.format(pro_id, doc['id']), 'level':2 } - # 获取三级文档 - thr_docs = Doc.objects.filter(top_doc=pro_id,parent_doc=doc['id'],status=1).values('id','name').order_by('sort') - if thr_docs.exists(): + # 如果二级文档存在下级文档,查询第三级文档 + if doc['id'] in parent_id_list: + # 获取三级文档 + thr_docs = Doc.objects.filter(top_doc=pro_id,parent_doc=doc['id'],status=1).values('id','name').order_by('sort') sec_item['children'] = [] for doc in thr_docs: item = { 'id': doc['id'], 'field': doc['name'], 'title': doc['name'], - 'href': '/project-{}/doc-{}/'.format(pro_id, doc['id']), 'level': 3 } sec_item['children'].append(item) - top_item['children'].append(sec_item) + top_item['children'].append(sec_item) else: top_item['children'].append(sec_item) doc_list.append(top_item) + # 如果一级文档没有下级文档,直接保存 else: doc_list.append(top_item) return JsonResponse({'status':True,'data':doc_list}) diff --git a/static/editor.md/editormd.js b/static/editor.md/editormd.js index f2ba887..df8dfd9 100644 --- a/static/editor.md/editormd.js +++ b/static/editor.md/editormd.js @@ -3478,6 +3478,94 @@ var editormdLogoReg = regexs.editormdLogo; var pageBreakReg = regexs.pageBreak; + // marked 解析图片 + markedRenderer.image = function(href,title,text) { + var attr = ""; + var begin = ""; + var end = ""; + // console.log(href,title,text) + if(/^=(.*?)/.test(text)){ + console.log(text) + switch(text){ + case '=video': + if(href.match(/^.+.(mp4|m4v|ogg|ogv|webm)$/)){ + return "" + } + break; + case '=audio': + if(href.match(/^.+.(mp3|wav|flac|m4a)$/)){ + return "" + } + break; + case '=video_iframe': + const youtubeMatch = href.match(/\/\/(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([\w|-]{11})(?:(?:[\?&]t=)(\S+))?/); + const youkuMatch = href.match(/\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/); + const qqMatch = href.match(/\/\/v\.qq\.com\/x\/cover\/.*\/([^\/]+)\.html\??.*/); + const coubMatch = href.match(/(?:www\.|\/\/)coub\.com\/view\/(\w+)/); + const facebookMatch = href.match(/(?:www\.|\/\/)facebook\.com\/([^\/]+)\/videos\/([0-9]+)/); + const dailymotionMatch = href.match(/.+dailymotion.com\/(video|hub)\/(\w+)\?/); + const bilibiliMatch = href.match(/(?:www\.|\/\/)bilibili\.com\/video\/(\w+)/); + const tedMatch = href.match(/(?:www\.|\/\/)ted\.com\/talks\/(\w+)/); + + if (youtubeMatch && youtubeMatch[1].length === 11) { + return `