新增本地文档批量导入指定文档功能

This commit is contained in:
zmister 2021-10-10 16:54:14 +08:00
parent 5989f06774
commit 3c4e0168af
7 changed files with 596 additions and 0 deletions

View File

@ -20,12 +20,15 @@ urlpatterns = [
path('manage_project_colla/<int:pro_id>/',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/',views_import.import_project,name="import_project"), # 导入文集
path('import_doc_to_project/', views_import.import_local_doc_to_project, name="import_doc_to_project"), # 导入本地文档到文集
path('manage_project_doc_sort/',views_import.project_doc_sort,name='project_doc_sort'), # 导入文集文档排序
path('manage_project_transfer/<int:pro_id>/',views.manage_project_transfer,name='manage_pro_transfer'), # 文集转让
path('manage_pro_doc_sort/<int:pro_id>/',views.manage_project_doc_sort,name='manage_pro_doc_sort'), # 文集排序
path('api/my_colla_list/', views.MyCollaList.as_view(), name="my_colla_list"), # 我的协作文集列表
path('api/import_local_doc/', views_import.ImportLocalDoc.as_view(), name="import_local_doc_api"), # 导入本地文档API
#################文档相关
path('project-<int:pro_id>/doc-<int:doc_id>/', views.doc, name='doc'), # 文档浏览页
path('doc/<int:doc_id>/', views.doc_id, name="doc_id"), # 文档浏览页(通过文档ID)
path('create_doc/', views.create_doc, name="create_doc"), # 新建文档
path('modify_doc/<int:doc_id>/', views.modify_doc, name="modify_doc"), # 修改文档
path('del_doc/',views.del_doc,name="del_doc"), # 删除文档

View File

@ -1071,6 +1071,95 @@ def doc(request,pro_id,doc_id):
return render(request,'404.html')
# 文档浏览页可通过文档ID 或文集ID+文档ID访问
@require_http_methods(['GET'])
def doc_id(request,doc_id):
try:
# 获取文档内容
try:
doc = Doc.objects.get(id=int(doc_id),status__in=[0,1]) # 文档信息
doc_tags = DocTag.objects.filter(doc=doc) # 文档标签信息
pro_id = doc.top_doc
if doc.status == 0 and doc.create_user != request.user:
raise ObjectDoesNotExist
elif doc.status == 0 and doc.create_user == request.user:
doc.name = _('【预览草稿】')+ doc.name
except ObjectDoesNotExist:
return render(request, '404.html')
# 获取文集信息
project = Project.objects.get(id=int(pro_id))
# 获取文集的文档目录
toc_list,toc_cnt = get_pro_toc(pro_id)
# 获取文集的协作用户信息
if request.user.is_authenticated:
colla_user = ProjectCollaborator.objects.filter(project=project,user=request.user)
if colla_user.exists():
colla_user_role = colla_user[0].role
colla_user = colla_user.count()
else:
colla_user = colla_user.count()
else:
colla_user = 0
# 获取文集收藏状态
if request.user.is_authenticated:
is_collect_pro = MyCollect.objects.filter(collect_type=2, collect_id=pro_id,
create_user=request.user).exists()
# 获取文档收藏状态
is_collect_doc = MyCollect.objects.filter(collect_type=1, collect_id=doc_id,
create_user=request.user).exists()
else:
is_collect_pro,is_collect_doc = False,False
# 私密文集且访问者非创建者、协作者 - 不能访问
if (project.role == 1) and (request.user != project.create_user) and (colla_user == 0):
return render(request, '404.html')
# 指定用户可见文集
elif project.role == 2:
user_list = project.role_value
if request.user.is_authenticated: # 认证用户判断是否在许可用户列表中
if (request.user.username not in user_list) and \
(request.user != project.create_user) and \
(colla_user == 0): # 访问者不在指定用户之中,也不是协作者
return render(request, '404.html')
else: # 游客直接返回404
return render(request, '404.html')
# 访问码可见
elif project.role == 3:
# 浏览用户不为创建者和协作者 - 需要访问码
if (request.user != project.create_user) and (colla_user == 0):
viewcode = project.role_value
viewcode_name = 'viewcode-{}'.format(project.id)
r_viewcode = request.COOKIES[
viewcode_name] if viewcode_name in request.COOKIES.keys() else 0 # 从cookie中获取访问码
if viewcode != r_viewcode: # cookie中的访问码不等于文集访问码跳转到访问码认证界面
return redirect('/check_viewcode/?to={}'.format(request.path))
# 获取文档内容
try:
doc = Doc.objects.get(id=int(doc_id),status__in=[0,1]) # 文档信息
doc_tags = DocTag.objects.filter(doc=doc) # 文档标签信息
if doc.status == 0 and doc.create_user != request.user:
raise ObjectDoesNotExist
elif doc.status == 0 and doc.create_user == request.user:
doc.name = _('【预览草稿】')+ doc.name
except ObjectDoesNotExist:
return render(request, '404.html')
# 获取文档分享信息
try:
doc_share = DocShare.objects.get(doc=doc)
is_share = True
except ObjectDoesNotExist:
is_share = False
return render(request,'app_doc/doc.html',locals())
except Exception as e:
logger.exception(_("文集浏览出错"))
return render(request,'404.html')
# 创建文档
@login_required()
@require_http_methods(['GET',"POST"])

View File

@ -18,10 +18,17 @@ from django.contrib.auth.models import User
from django.db.models import Q
from django.db import transaction
from django.utils.translation import gettext_lazy as _
from rest_framework.views import APIView # 视图
from rest_framework.response import Response # 响应
from rest_framework.pagination import PageNumberPagination # 分页
from rest_framework.authentication import SessionAuthentication # 认证
from rest_framework.permissions import IsAdminUser # 权限
from loguru import logger
from app_doc.report_utils import *
from app_admin.decorators import check_headers,allow_report_file
from app_doc.import_utils import *
from app_doc.views import get_pro_toc,html_filter,jsonXssFilter
from app_api.auth_app import AppAuth,AppMustAuth # 自定义认证
import datetime
import traceback
import re
@ -133,6 +140,144 @@ def import_project(request):
return JsonResponse({'status':False,'data':_('参数错误')})
# 导入本地文档到文集
@login_required()
@require_http_methods(['GET','POST'])
def import_local_doc_to_project(request):
if request.method == 'GET':
project_list = Project.objects.filter(create_user=request.user) # 自己创建的文集列表
colla_project_list = ProjectCollaborator.objects.filter(user=request.user) # 协作的文集列表
return render(request,'app_doc/manage/import_local_doc_to_project.html',locals())
# 导入文档到文集API
class ImportLocalDoc(APIView):
authentication_classes = [SessionAuthentication, AppMustAuth]
# 上传文件
def post(self,request):
project = request.data.get("project",'')
editor_mode = request.data.get("editor_mode",0)
file = request.data.get("local_doc",None)
try:
project = int(project)
editor_mode = int(editor_mode)
except:
resp = {
'code':5,
'data':'必须选择文集'
}
return Response(resp)
if file is None:
resp = {
'code':5,
'data':'文件未选择'
}
file_name = file.name
# Markdown 文件和 TXT 文件
if file_name.endswith('.md') or file_name.endswith(".txt"):
doc_content = file.read().decode('utf-8')
if editor_mode == 3:
doc_content_html = markdown.markdown(text=doc_content)
else:
doc_content_html = None
doc = Doc.objects.create(
name = html_filter(file_name[:-3]),
pre_content = doc_content,
content = doc_content_html,
top_doc = project,
editor_mode = 1 if editor_mode == 0 else editor_mode,
create_user = request.user,
status = 0
)
doc.save()
resp = {
'code':0,
'data':{
'doc_id':doc.id,
'doc_name':doc.name
}
}
# Word 文件
elif file_name.endswith('.docx'):
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()) + '.docx'
temp_file_path = os.path.join(settings.MEDIA_ROOT, 'import_temp/' + temp_file_name)
with open(temp_file_path, 'wb+') as docx_file:
for chunk in file:
docx_file.write(chunk)
if os.path.exists(temp_file_path):
docx_file_content = ImportDocxDoc(
docx_file_path=temp_file_path,
editor_mode=editor_mode,
create_user=request.user
).run()
if docx_file_content['status']:
doc = Doc.objects.create(
name=html_filter(file_name[:-5]),
pre_content=docx_file_content['data'],
content=docx_file_content['data'],
top_doc=project,
editor_mode=1 if editor_mode == 0 else editor_mode,
create_user=request.user,
status=0
)
doc.save()
resp = {
'code': 0,
'data': {
'doc_id': doc.id,
'doc_name': doc.name
}
}
else:
resp = {
'code':4,
'data': '{}读取失败'.format(file_name)
}
else:
resp = {
'code': 4,
'data': '{}上传失败'.format(file_name)
}
else:
resp = {
'code':5,
'data':'文件格式不支持'
}
return Response(resp)
# 发布文档
def put(self,request):
sort_data = request.data.get('sort_data', '[]') # 文档排序列表
try:
sort_data = json.loads(sort_data)
except Exception:
return JsonResponse({'code': 5, 'data': _('文档参数错误')})
# 文档排序
n = 10
# 第一级文档
for data in sort_data:
Doc.objects.filter(id=data['id']).update(sort=n, status=1)
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=1)
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=1)
return Response({'code':0,'data':'ok'})
# 文集文档排序
@login_required()
@require_http_methods(['POST'])

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1632207650207" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3385" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M1004.284179 311.581294v491.723781c0 50.435821-37.801393 88.288159-81.970945 88.288159h-81.970945v-44.118607h81.970945a43.099701 43.099701 0 0 0 44.118607-44.118607V343.116418H802.38806c-44.118607 0-81.970945-37.801393-81.970946-88.237214V59.402189h-371.900497a43.099701 43.099701 0 0 0-44.118607 44.118607h-37.750448C266.647562 53.084975 298.029851 15.283582 348.618507 15.283582h397.373135l252.179104 290.031443m-239.442786-208.060498v163.890946a43.099701 43.099701 0 0 0 43.6601 44.118607h138.72398z m37.801393 346.733533v491.774726c0 50.435821-37.801393 88.237214-81.970945 88.237214H140.55801C96.388458 1024 58.587065 986.147662 58.587065 935.711841V235.927562C58.587065 185.491741 96.388458 147.741294 140.55801 147.741294h397.373134l252.179105 289.980497m-239.442786-208.060497v163.890945a43.099701 43.099701 0 0 0 44.118607 44.118607h138.72398z m-447.961791 6.266268v699.784279a43.099701 43.099701 0 0 0 44.118607 44.118607h573.745671a43.099701 43.099701 0 0 0 44.118607-44.118607V475.523184h-170.208159c-44.118607 0-81.970945-37.852338-81.970945-88.288159V191.808955h-371.900498c-25.472637 0-37.801393 18.900697-37.801393 44.118607z m100.871641 264.915423h201.743284v44.118607h-201.743284z m0-132.457711h201.743284V412.656716h-201.743284z m453.922388 302.614925h-453.922388V626.626866h453.922388z m0 132.457711h-453.922388V759.084577h453.922388z m0 0" p-id="3386"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -57,6 +57,17 @@
color:red;
}
/* 表格列中的链接样式 */
.table-col-link{
color:#2D8CF0;
font-weight: 700;
}
.table-col-link:hover{
color:#2D8CF0;
text-decoration: underline;
font-weight: 700;
}
/* 覆盖layUI样式 */
.layui-layout-admin .layui-body{
/* background-color: #f0f2f5; */

View File

@ -0,0 +1,327 @@
{% extends 'app_doc/user/user_base.html' %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "导入文集" %}{% endblock %}
{% block content %}
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-row" style="padding-left:15px;">
<span class="layui-breadcrumb" lay-separator=">">
<a href="{% url 'import_project' %}">{% trans "导入文集" %}</a>
<a><cite>{% trans "导入本地文档到文集" %}</cite></a>
</span>
</div>
</div>
</div>
<!-- 导入本地文档到文集 -->
<div class="layui-row layui-col-space10">
<!-- 文集设置 -->
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-card-header" style="margin-bottom: 10px;">
<span style="font-size:18px;">{% trans "文档文集设置" %}</span>
</div>
<div>
<div class="layui-form" lay-filter="project-settings">
<div class="layui-form-item">
<label class="layui-form-label">选择文集</label>
<div class="layui-input-inline">
<select name="project" lay-verify="required">
<option value="">请选择一个文集(必选)</option>
<!-- 自己的文集 -->
<optgroup label="自有文集" id="self-project">
{% for p in project_list %}
{% if p.role == 1 %}
<option value="{{ p.id }}">[私密]《{{ p.name }}》</option>
{% elif p.role == 2 %}
<option value="{{ p.id }}" >[指定用户]《{{ p.name }}》</option>
{% elif p.role == 3 %}
<option value="{{ p.id }}" >[访问码]《{{ p.name }}》</option>
{% else %}
<option value="{{ p.id }}" >[公开]《{{ p.name }}》</option>
{% endif %}
{% endfor %}
</optgroup>
<!-- 协作的文集 -->
{% if colla_project_list.count > 0 %}
<optgroup label="协作文集">
{% for p in colla_project_list %}
<option value="{{ p.project.id }}">[协作]《{{ p.project.name }}》</option>
{% endfor %}
</optgroup>
{% endif %}
</select>
</div>
<div class="layui-form-mid layui-word-aux"><span style="color:red;">*必选</span></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文档模式</label>
<div class="layui-input-block">
<input type="radio" name="editor_mode" value="0" title="自动选择" checked>
<input type="radio" name="editor_mode" value="1" title="Markdown">
<input type="radio" name="editor_mode" value="3" title="富文本">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 文件上传 -->
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-card-header" style="margin-bottom: 10px;">
<span style="font-size:18px;">{% trans "文档上传" %}</span>
</div>
<div>
<button style="width: 100%;min-height: 113px;border-style:dashed;border-color: #999;cursor: pointer;" type="button" id="import-local-doc">
<i class="layui-icon layui-icon-addition" style="font-size: 40px;color:#999"></i><br>
点击上传本地文档,支持.md、.docx、.txt格式文件
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 文档排序 -->
<div class="layui-row">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-card-header" style="margin-bottom: 10px;">
<span style="font-size:18px;">{% trans "文档排序(文档导入后状态为草稿)" %}</span>
<button class="pear-btn pear-btn-primary pear-btn-sm" style="float: right;" id="save-sort-btn">{% trans "发布文档" %}</button>
</div>
<div>
<div id="nested" class="row">
<ul id="nestedDemo" class="list-group col nested-sortable"></ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_script %}
<script src="{% static 'jquery/3.1.1/jquery.min.js' %}"></script>
<script src="{% static 'layui/layui.js' %}"></script>
<script>
$.ajaxSetup({
headers: {"X-CSRFToken":'{{ csrf_token }}'},
});
var upload = layui.upload,form=layui.form;
// 上传文件按钮点击事件
var uploadInst = upload.render({
elem: '#import-local-doc', //绑定元素
url: "{% url 'import_local_doc_api' %}", //上传接口
field:"local_doc",
multiple:true,
accept:'file',
exts:'md|txt|docx',
before: function(obj){ //obj参数包含的信息跟 choose回调完全一致可参见上文。
this.data = form.val("project-settings");
layer.load(); //上传loading
},
done: function(res){
layer.closeAll();
//上传完毕回调
if(res.code == 0){
let doc_toc_str = '<li data-sortable-id="'
+ res.data['doc_id']
+ '" class="list-group-item"><i class="iconfont mrdoc-icon-wendang"></i> '
+ res.data['doc_name']
+ '&nbsp;<a class="table-col-link" target="_blank" href="/doc/' + res.data['doc_id'] + '/">预览</a>'
+ '<ul class="list-group nested-sortable"></ul></li>'
$("#nestedDemo").append(doc_toc_str);
docSort();// 动态添加元素之后,调用一次
}else{
layer.msg(res.data,{'icon':2})
}
},
error: function(){
layer.closeAll();
//请求异常回调
layer.msg("上传失败",{icon:2});
}
});
// 发布文档
$("#save-sort-btn").on('click',function(){
layer.load(1)
let sort_data = {
'sort_data':JSON.stringify(serialize(root)),
}
// console.log(sort_data)
$.ajax({
url:"{% url 'import_local_doc_api' %}",
type:'put',
data:sort_data,
success:function(r){
layer.closeAll();
if(r.code == 0){
layer.msg("发布成功",{icon:1},function(){
window.location.reload();
})
}else{
layer.msg(r.data,{icon:2})
}
},
error:function(){
layer.closeAll();
layer.msg("发布失败",{icon:2})
}
});
});
// 删除文档
</script>
<!-- 导入的文集文档排序模板div -->
<style>
.row {
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.list-group {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
padding-left: 0;
margin-bottom: 0;
}
.col {
-ms-flex-preferred-size: 0;
flex-basis: 0;
-ms-flex-positive: 1;
flex-grow: 1;
max-width: 100%;
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
}
.list-group-item:first-child {
border-top-left-radius: .25rem;
border-top-right-radius: .25rem;
}
.list-group-item {
position: relative;
display: block;
padding: .75rem 1.25rem;
margin-bottom: -1px;
background-color: #fff;
border: 1px solid rgba(0,0,0,.125);
}
.list-group-item:first-child {
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
}
.list-group-item:last-child {
margin-bottom: 0;
border-bottom-right-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.list-group-item:hover, .list-group-item:focus {
z-index: 1;
text-decoration: none;
}
.list-group-item.disabled, .list-group-item:disabled {
color: #6c757d;
pointer-events: none;
background-color: #fff;
}
.list-group-item.active {
z-index: 2;
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
</style>
<div id="import-project-sort" style="display: none;margin: 10px;" class="layui-form">
<div class="layui-row" style="padding-left: 14px;padding-bottom: 10px;">
<input class="layui-input" placeholder="请输入文集名称" name="project-name">
</div>
<div class="layui-row" style="padding-left: 14px;padding-bottom: 10px;">
<input class="layui-input" placeholder="请输入文集简介" name="project-desc">
</div>
<div class="layui-row" style="">
<div class="layui-form-item">
<label class="layui-form-label" style="width: auto;">文集状态</label>
<div class="layui-input-block">
<input type="radio" name="role" value="1" title="私密" checked>
<input type="radio" name="role" value="0" title="公开">
</div>
</div>
</div>
<div class="layui-row" style="padding-left: 14px;padding-bottom: 10px;">文档拖拽排序</div>
<div id="nested" class="row">
<ul id="nestedDemo" class="list-group col nested-sortable"></ul>
</div>
</div>
<!-- 文档拖拽排序 -->
<script src="{% static 'sortablejs/Sortable.js' %}"></script>
<script>
// 文档动态排序
function docSort(){
// Nested demo
var nestedSortables = [].slice.call(document.querySelectorAll('.nested-sortable'));
// Loop through each nested sortable element
for (var i = 0; i < nestedSortables.length; i++) {
new Sortable(nestedSortables[i], {
group: {
name:'docsort',
pull: function(event) {
var deep = event.el.parentNode.parentNode.parentNode.parentNode.className;
// if(deep == 'list-group nested-sortable') return false;
return true;
},
},
animation: 150,
fallbackOnBody: true,
invertSwap:true,
swapThreshold: 0.65,
});
}
};
const nestedQuery = '.nested-sortable';
const identifier = 'sortableId';
const root = document.getElementById('nestedDemo');
function serialize(sortable) {
var serialized = [];
var children = [].slice.call(sortable.children);
for (var i in children) {
var nested = children[i].querySelector(nestedQuery);
serialized.push({
id: children[i].dataset[identifier],
children: nested ? serialize(nested) : []
});
}
return serialized
}
function getLevel(){
console.log(serialize(root))
}
// 展开收起左边目录
$(function(){
$("body").on('click','.switch-toc',SwitchToc)
});
function SwitchToc(i){
console.log("点击了")
var $me = $(this);
$(this).next("ul").toggleClass("toc-close"); //切换展开收起样式
$(this).toggleClass("layui-icon-left layui-icon-down");//切换图标
};
form.render();
</script>
{% endblock %}

View File

@ -40,6 +40,26 @@
</div>
</div>
</div>
<!-- 导入本地文档到文集 -->
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-card-header" style="margin-bottom: 10px;">
<span style="font-size:18px;">{% trans "导入本地文档到文集" %}</span>
</div>
<div>
<a style="width: 142px;cursor: pointer;display: inline-block;" href="{% url 'import_doc_to_project' %}">
<div style="width: 70px;height: 70px;margin: 0 auto;">
<img src="{% static 'icon_img/file-doc.svg' %}">
</div>
<div style="text-align: center;">
<div style="color: #262626;font-size:14px;">本地文本文档</div>
<div style="color: #8c8c8c;font-size:12px;">支持Markdown、TXT、Word等格式文件</div>
</div>
</a>
</div>
</div>
</div>
{% endblock %}
{% block custom_script %}
<script src="{% static 'jquery/3.1.1/jquery.min.js' %}"></script>