diff --git a/CHANGES.md b/CHANGES.md index 4579470..9407b30 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,16 @@ ## 版本更新记录 +### v.0.5.0 2020-05-02 + +- MrDoc正式中文取名:觅道文档; +- 文档编辑器添加Markdown折叠功能; +- 思维导图支持图形高度设置; +- 优化文集导出EPUB文件功能; +- 新增PDF文件导出功能; +- 新增一个广告位; +- 优化文集名称字符验证; + + ### v0.4.2 2020-04-20 - 添加思维导图功能的支持,可以在文档编辑器通过图标和`mindmap`标识代码块来创建脑图; diff --git a/MrDoc/settings.py b/MrDoc/settings.py index 974d434..2485796 100644 --- a/MrDoc/settings.py +++ b/MrDoc/settings.py @@ -1,3 +1,4 @@ +# coding:utf-8 """ Django settings for MrDoc project. @@ -25,7 +26,7 @@ SECRET_KEY = '5&71mt9@^58zdg*_!t(x6g14q*@84d%ptr%%s6e0l50zs0we3d' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False -VERSIONS = '0.4.2' +VERSIONS = '0.5.0' ALLOWED_HOSTS = ['*'] diff --git a/README.md b/README.md index 5da16f2..6729932 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## MrDoc - 记录文档,汇聚思想 +## 觅道文档MrDoc - 记录文档,汇聚思想 ![Mrdoc首页](./captrue/mrdoc-index.png) @@ -28,13 +28,13 @@ - 支持**思维导图**,以Markdown的语法创建思维导图 - 支持流程图、时序图的绘制; - 两栏式**文档阅读**页面、三级目录层级显示,文档阅读字体缩放,字体类型切换,页面社交分享,移动端阅读优化; - - 支持文集后台**导出打包**`markdown`文本格式`.md`文件、前台导出为`EPUB`等格式文件; + - 支持文集**导出打包**,包括`markdown`文本格式`.md`文件、`EPUB`电子书格式文件和PDF格式文件; - 基于文集进行**文档权限**控制,提供公开、私密、指定用户可见、访问码可见4种权限模式; - 支持基于账户的**`API`接口**,可以借助账户`token`通过`API`获取文集、上传图片和创建文档; - 支持**文集协作**功能,一个文集可以拥有一个创建者和多个协作者,可灵活选择协作权限; - 支持**文档历史版本**功能,可以查看和对比历史版本与现有版本的差异,恢复某个历史版本为当前版本; -当前版本为:**v0.4.2**,版本发布时间为**2020-04-20** +当前版本为:**v0.5.0**,版本发布时间为**2020-05-02** 完整更新记录详见:[CHANGES.md](./CHANGES.md) @@ -57,7 +57,10 @@ pip install -r requirements.txt 默认情况下,MrDoc使用Django的SQLite数据库,如果你使用Sqlite数据库,则无需另外配置数据库。 -如果有配置其他数据库的需求,请在/MrDoc/MrDoc目录下打开settings.py文件,在约80行的位置,将如下代码: +如果有配置其他数据库的需求,请首先按照Django官方的[数据库支持说明](https://docs.djangoproject.com/zh-hans/2.2/ref/databases/),安装特定数据库的Python绑定库, + +然后在/MrDoc/MrDoc目录下打开settings.py文件,在约80行的位置,将如下代码: + ```python DATABASES = { 'default': { @@ -66,7 +69,9 @@ DATABASES = { } } ``` + 按照自己数据库的信息,将其修改如下格式,下面以MySQL为例: + ```python DATABASES = { 'default': { @@ -79,6 +84,7 @@ DATABASES = { } } ``` + ### 3、初始化数据库 在安装完所需的第三方库并配置好数据库信息之后,我们需要对数据库进行初始化。 diff --git a/app_admin/views.py b/app_admin/views.py index 18534b5..8b58400 100644 --- a/app_admin/views.py +++ b/app_admin/views.py @@ -12,19 +12,25 @@ import json,datetime,hashlib,random from app_doc.models import * from app_admin.models import * from app_admin.utils import * +import traceback # 返回验证码图片 def check_code(request): - import io - from . import check_code as CheckCode - stream = io.BytesIO() - # img图片对象,code在图像中写的内容 - img, code = CheckCode.create_validate_code() - img.save(stream, "png") - # 图片页面中显示,立即把session中的CheckCode更改为目前的随机字符串值 - request.session["CheckCode"] = code - return HttpResponse(stream.getvalue()) + try: + import io + from . import check_code as CheckCode + stream = io.BytesIO() + # img图片对象,code在图像中写的内容 + img, code = CheckCode.create_validate_code() + img.save(stream, "png") + # 图片页面中显示,立即把session中的CheckCode更改为目前的随机字符串值 + request.session["CheckCode"] = code + return HttpResponse(stream.getvalue()) + except Exception as e: + if settings.DEBUG: + print(traceback.print_exc()) + return HttpResponse("请求异常:{}".format(repr(e))) # 登录视图 @@ -54,6 +60,8 @@ def log_in(request): errormsg = '用户名或密码错误!' return render(request, 'login.html', locals()) except Exception as e: + if settings.DEBUG: + print(traceback.print_exc()) return HttpResponse('请求出错') @@ -136,8 +144,8 @@ def log_out(request): try: logout(request) except Exception as e: - print(e) - # logger.error(e) + if settings.DEBUG: + print(traceback.print_exc()) return redirect(request.META['HTTP_REFERER']) @@ -165,7 +173,8 @@ def forget_pwd(request): errormsg = "验证码已过期" return render(request, 'forget_pwd.html', locals()) except Exception as e: - print(repr(e)) + if settings.DEBUG: + print(traceback.print_exc()) errormsg = "验证码错误" return render(request,'forget_pwd.html',locals()) @@ -514,7 +523,8 @@ def admin_setting(request): if types == 'basic': close_register = request.POST.get('close_register',None) # 禁止注册 static_code = request.POST.get('static_code',None) # 统计代码 - ad_code = request.POST.get('ad_code',None) # 广告代码 + ad_code = request.POST.get('ad_code',None) # 广告位1 + ad_code_2 = request.POST.get('ad_code_2',None) # 广告位2 beian_code = request.POST.get('beian_code',None) # 备案号 enbale_email = request.POST.get("enable_email",None) # 启用邮箱 enable_register_code = request.POST.get('enable_register_code',None) # 注册邀请码 @@ -534,6 +544,10 @@ def admin_setting(request): name = 'ad_code', defaults={'value':ad_code,'types':'basic'} ) + SysSetting.objects.update_or_create( + name='ad_code_2', + defaults={'value': ad_code_2, 'types': 'basic'} + ) # 更新备案号 SysSetting.objects.update_or_create( name='beian_code', diff --git a/app_doc/migrations/0020_projectreportfile.py b/app_doc/migrations/0020_projectreportfile.py new file mode 100644 index 0000000..d763c11 --- /dev/null +++ b/app_doc/migrations/0020_projectreportfile.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.12 on 2020-04-26 11:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('app_doc', '0019_dochistory_create_user'), + ] + + operations = [ + migrations.CreateModel( + name='ProjectReportFile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file_type', models.CharField(choices=[('epub', 'epub'), ('pdf', 'pdf'), ('docx', 'docx')], max_length=10, verbose_name='文件类型')), + ('file_name', models.CharField(max_length=100, verbose_name='文件名称')), + ('file_path', models.CharField(max_length=250, verbose_name='文件路径')), + ('create_time', models.DateTimeField(auto_now_add=True)), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app_doc.Project')), + ], + options={ + 'verbose_name': '附件管理', + 'verbose_name_plural': '附件管理', + }, + ), + ] diff --git a/app_doc/migrations/0021_projectreport_allow_pdf.py b/app_doc/migrations/0021_projectreport_allow_pdf.py new file mode 100644 index 0000000..80dd2d2 --- /dev/null +++ b/app_doc/migrations/0021_projectreport_allow_pdf.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.12 on 2020-05-01 10:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app_doc', '0020_projectreportfile'), + ] + + operations = [ + migrations.AddField( + model_name='projectreport', + name='allow_pdf', + field=models.IntegerField(default=0, verbose_name='前台导出PDF'), + ), + ] diff --git a/app_doc/models.py b/app_doc/models.py index 9c05ec2..fb4dcd4 100644 --- a/app_doc/models.py +++ b/app_doc/models.py @@ -106,6 +106,7 @@ class ProjectReport(models.Model): project = models.OneToOneField(Project,unique=True,on_delete=models.CASCADE) # 允许导出,默认为0-允许,1-不允许 allow_epub = models.IntegerField(default=0,verbose_name="前台导出EPUB") + allow_pdf = models.IntegerField(default=0, verbose_name="前台导出PDF") def __str__(self): return self.project.name @@ -114,6 +115,22 @@ class ProjectReport(models.Model): verbose_name = '文集导出' verbose_name_plural = verbose_name + +# 文集导出文集模型 +class ProjectReportFile(models.Model): + project = models.ForeignKey(Project, on_delete=models.CASCADE) # 外键关联文集 + file_type = models.CharField(choices=(('epub', 'epub'), ('pdf', 'pdf'), ('docx', 'docx')), verbose_name='文件类型',max_length=10) + file_name = models.CharField(max_length=100, verbose_name='文件名称') + file_path = models.CharField(max_length=250, verbose_name='文件路径') + create_time = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.file_name + + class Meta: + verbose_name = '附件管理' + verbose_name_plural = verbose_name + # 图片分组模型 class ImageGroup(models.Model): user = models.ForeignKey(User,on_delete=models.CASCADE) diff --git a/app_doc/report_utils.py b/app_doc/report_utils.py index 021e9b8..c284e02 100644 --- a/app_doc/report_utils.py +++ b/app_doc/report_utils.py @@ -21,6 +21,125 @@ django.setup() from app_doc.models import * import traceback import time +from pyppeteer import launch +import asyncio +# import PyPDF2 +# from pdfminer import high_level + +# 动态脑图转静态图片 +def genera_mindmap_img(html_path,img_path): + async def main(): + browser = await launch(headless=True,handleSIGINT=False,handleSIGTERM=False,handleSIGHUP=False) + page = await browser.newPage() + await page.goto('file://' + html_path, {'waitUntil': 'networkidle0'}) + element = await page.querySelector('.mindmap') + await element.screenshot({'type': 'jpeg', 'quality': 100, 'path': img_path}) + await browser.close() + + # asyncio.new_event_loop().run_until_complete(main()) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(main()) + finally: + loop.close() + + +# 公式转图片 +def genera_tex_img(html_path,img_path): + async def main(): + browser = await launch(headless=True,handleSIGINT=False,handleSIGTERM=False,handleSIGHUP=False) + page = await browser.newPage() + await page.goto('file://' + html_path, {'waitUntil': 'networkidle0'}) + element = await page.querySelector('.editormd-tex') + await element.screenshot({'type': 'jpeg', 'quality': 100, 'path': img_path}) + await browser.close() + + # asyncio.new_event_loop().run_until_complete(main()) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(main()) + finally: + loop.close() + +# 流程图转图片 +def genera_flowchart_img(html_path,img_path): + async def main(): + browser = await launch(headless=True,handleSIGINT=False,handleSIGTERM=False,handleSIGHUP=False) + page = await browser.newPage() + await page.goto('file://' + html_path, {'waitUntil': 'networkidle0'}) + element = await page.querySelector('.flowchart') + await element.screenshot({'type': 'jpeg', 'quality': 100, 'path': img_path}) + await browser.close() + + # asyncio.new_event_loop().run_until_complete(main()) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(main()) + finally: + loop.close() + +# 时序图转图片 +def genera_seque_img(html_path,img_path): + async def main(): + browser = await launch(headless=True,handleSIGINT=False,handleSIGTERM=False,handleSIGHUP=False) + page = await browser.newPage() + await page.goto('file://' + html_path, {'waitUntil': 'networkidle0'}) + element = await page.querySelector('.sequence-diagram') + await element.screenshot({'type': 'jpeg', 'quality': 100, 'path': img_path}) + await browser.close() + + # asyncio.new_event_loop().run_until_complete(main()) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(main()) + finally: + loop.close() + +# HTML转PDF +def html_to_pdf(html_path,pdf_path): + async def main(): + browser = await launch( + headless=True, + handleSIGINT=False, + handleSIGTERM=False, + handleSIGHUP=False, + ignoreHTTPSErrors = True, + ) + page = await browser.newPage() + await page.goto('file://' + html_path, {'waitUntil': 'networkidle0'}) + await page.pdf({ + 'path':pdf_path, + 'format':'A4', + 'displayHeaderFooter':True, + 'headerTemplate':'
', + 'footerTemplate':'
/
', + 'margin':{ + 'top':'1cm', + 'right':'1cm', + 'bottom':'1cm', + 'left':'1cm' + } + + }) + await browser.close() + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(main()) + except: + loop.run_until_complete(main()) + finally: + loop.close() # 导出MD文件压缩包 class ReportMD(): @@ -55,7 +174,7 @@ class ReportMD(): def work(self): # 读取指定文集的文档数据 data = Doc.objects.filter(top_doc=self.pro_id, parent_doc=0).order_by("sort") - # 遍历文档 + # 遍历一级文档 for d in data: md_name = d.name md_content = d.pre_content @@ -70,7 +189,6 @@ class ReportMD(): for d2 in data_2: md_name_2 = d2.name md_content_2 = d2.pre_content - md_content_2 = self.operat_md_media(md_content_2) # 新建MD文件 @@ -138,7 +256,7 @@ class ReportMD(): class ReportEPUB(): def __init__(self,project_id): self.project = Project.objects.get(id=project_id) - self.base_path = settings.MEDIA_ROOT + '/report/{}/'.format(project_id) + self.base_path = settings.MEDIA_ROOT + '/report_epub/{}/'.format(project_id) # 创建相关目录 if os.path.exists(self.base_path + '/OEBPS') is False: @@ -155,7 +273,7 @@ class ReportEPUB(): # 复制样式文件到相关目录 shutil.copyfile(settings.BASE_DIR+'/static/report_epub/style.css',self.base_path + '/OEBPS/Styles/style.css') shutil.copyfile(settings.BASE_DIR+'/static/katex/katex.min.css',self.base_path + '/OEBPS/Styles/katex.css') - shutil.copyfile(settings.BASE_DIR+'/static/editor.md/css/editormd.min.css/',self.base_path + '/OEBPS/Styles/editormd.css') + shutil.copyfile(settings.BASE_DIR+'/static/editor.md/css/editormd.min.css',self.base_path + '/OEBPS/Styles/editormd.css') # 复制封面图片到相关目录 shutil.copyfile(settings.BASE_DIR+'/static/report_epub/epub_cover1.jpg',self.base_path + '/OEBPS/Images/epub_cover1.jpg') @@ -164,8 +282,11 @@ class ReportEPUB(): # 使用BeautifulSoup解析拼接好的HTML文本 html_soup = BeautifulSoup(html_str, 'lxml') src_tag = html_soup.find_all(lambda tag: tag.has_attr("src")) # 查找所有包含src的标签 - code_tag = html_soup.find_all(name="code") - # print(src_tag) + mindmap_tag = html_soup.select('svg.mindmap') # 查找所有脑图的SVG标签 + tex_tag = html_soup.select('.editormd-tex') # 查找所有公式标签 + flowchart_tag = html_soup.select('.flowchart') # 查找所有流程图标签 + seque_tag = html_soup.select('.sequence-diagram') # 查找所有时序图标签 + code_tag = html_soup.find_all(name="code") # 查找code代码标签 # 添加css样式标签 style_link = html_soup.new_tag(name='link',href="../Styles/style.css",rel="stylesheet",type="text/css") @@ -177,6 +298,7 @@ class ReportEPUB(): # 添加xlm标签声明 # html_soup.html.insert_before('') + # 添加html标签的xmlns属性 html_soup.html['xmlns'] = "http://www.w3.org/1999/xhtml" @@ -195,6 +317,160 @@ class ReportEPUB(): except FileNotFoundError as e: pass + # 替换HTML文本中的脑图为静态图片 + for mindmap in mindmap_tag: + print('转换脑图') + html_str = ''' + + + + + + Markmap + + + + + + + {svg_content} + + + + '''.format(svg_content=mindmap) + # 脑图HTML文件路径 + temp_mindmap_html = settings.BASE_DIR +'/media/report_epub/mindmap_{}.html'.format(str(time.time())) + mindmap_img_filename = 'mindmap_{}.jpg'.format(str(time.time())) + mindmap_img_path = self.base_path + '/OEBPS/Images/' + mindmap_img_filename + with open(temp_mindmap_html,'w+',encoding='utf-8') as mindmap_html: + mindmap_html.write(html_str) + genera_mindmap_img(temp_mindmap_html,mindmap_img_path) + # 将图片标签设置进去 + mindmap.name = 'img' + mindmap['src'] = '../Images/' + mindmap_img_filename + mindmap.string = '' + os.remove(temp_mindmap_html) # 删除临时的HTML + + # 替换公式为静态图片 + for tex in tex_tag: + print('转换公式') + html_str = ''' + + + + + + + Markmap + + + + + + {content} + + + + + '''.format(content=tex) + # 公式HTML文件路径 + temp_tex_html = settings.BASE_DIR + '/media/report_epub/tex_{}.html'.format(str(time.time())) + tex_img_filename = 'tex_{}.jpg'.format(str(time.time())) + tex_img_path = self.base_path + '/OEBPS/Images/' + tex_img_filename + with open(temp_tex_html, 'w+', encoding='utf-8') as tex_html: + tex_html.write(html_str) + genera_tex_img(temp_tex_html, tex_img_path) + # 将图片标签添加进去 + # tex.name = 'img' + # tex['src'] = '../Images/' + tex_img_filename + tex.string = '' + tex_img_tag = html_soup.new_tag(name='img',src='../Images/' + tex_img_filename) + tex.insert(0,tex_img_tag) + os.remove(temp_tex_html) # 删除临时的HTML + + # 替换流程图为静态图片 + for flowchart in flowchart_tag: + print("转换流程图") + html_str = ''' + + + + + + + Markmap + + + + + + + {content} + + + + + '''.format(content=flowchart) + # 流程图HTML文件路径 + temp_flow_html = settings.BASE_DIR + '/media/report_epub/flow_{}.html'.format(str(time.time())) + flow_img_filename = 'flow_{}.jpg'.format(str(time.time())) + flow_img_path = self.base_path + '/OEBPS/Images/' + flow_img_filename + with open(temp_flow_html, 'w+', encoding='utf-8') as flow_html: + flow_html.write(html_str) + genera_flowchart_img(temp_flow_html, flow_img_path) + # 将图片标签添加进去 + flowchart.string = '' + flow_img_tag = html_soup.new_tag(name='img', src='../Images/' + flow_img_filename) + flowchart.insert(0, flow_img_tag) + os.remove(temp_flow_html) # 删除临时的HTML + + # 替换时序图为静态图片 + for seque in seque_tag: + print("转换时序图") + html_str = ''' + + + + + + Markmap + + + + + + + {content} + + + + + '''.format(content=seque) + # 时序图HTML文件路径 + temp_seque_html = settings.BASE_DIR + '/media/report_epub/seque_{}.html'.format(str(time.time())) + seque_img_filename = 'seque_{}.jpg'.format(str(time.time())) + seque_img_path = self.base_path + '/OEBPS/Images/' + seque_img_filename + with open(temp_seque_html, 'w+', encoding='utf-8') as seque_html: + seque_html.write(html_str) + genera_seque_img(temp_seque_html, seque_img_path) + # 将图片标签添加进去 + seque.string = '' + seque_img_tag = html_soup.new_tag(name='img', src='../Images/' + seque_img_filename) + seque.insert(0, seque_img_tag) + os.remove(temp_seque_html) # 删除临时的HTML + # 替换code标签的内容 # for code in code_tag: # code_str = code.get_text() @@ -521,11 +797,11 @@ class ReportEPUB(): def generate_epub(self): try: # 生成ZIP压缩文件 - zipfile_name = settings.MEDIA_ROOT + '/report/{}'.format(self.project.name)+'_'+str(int(time.time())) + zipfile_name = settings.MEDIA_ROOT + '/report_epub/{}'.format(self.project.name)+'_'+str(int(time.time())) zip_name = shutil.make_archive( base_name = zipfile_name, format='zip', - root_dir= settings.MEDIA_ROOT + '/report/{}'.format(self.project.id) + root_dir= settings.MEDIA_ROOT + '/report_epub/{}'.format(self.project.id) ) # print(zip_name) # 修改zip压缩文件后缀为EPUB @@ -550,6 +826,186 @@ class ReportEPUB(): epub_file = self.generate_epub() return epub_file + +# 导出PDF +class ReportPDF(): + def __init__(self,project_id): + # 查询文集信息 + self.pro_id = project_id + self.html_str = ''' + + + + + + + {title} + + + + + + + + + + + + + + + + + +
+ 本文档由觅道文档(MrDoc)生成 +
+
+

{project_name}

+

作者:{author}

+

日期:{create_time}

+
\n +
+ +
+ + + + ''' + self.content_str = "" + + def work(self): + try: + project = Project.objects.get(pk=self.pro_id) + except: + return + # 拼接文档的HTML字符串 + data = Doc.objects.filter(top_doc=self.pro_id,parent_doc=0).order_by("sort") + toc_list = {'1':[],'2':[],'3':[]} + for d in data: + self.content_str += "

{}

\n\n".format(d.name) + self.content_str += d.pre_content + '\n' + toc_list['1'].append({'id':d.id,'name':d.name}) + # 获取第二级文档 + data_2 = Doc.objects.filter(parent_doc=d.id).order_by("sort") + for d2 in data_2: + self.content_str += "\n\n

{}

\n\n".format(d2.name) + self.content_str += d2.pre_content + '\n' + toc_list['2'].append({'id':d2.id,'name':d2.name,'parent':d.id}) + # 获取第三级文档 + data_3 = Doc.objects.filter(parent_doc=d2.id).order_by("sort") + for d3 in data_3: + # print(d3.name,d3.content) + self.content_str += "\n\n

{}

\n\n".format(d3.name) + self.content_str += d3.pre_content +'\n' + toc_list['3'].append({'id':d3.id,'name':d3.name,'parent':d2.id}) + + # 替换所有媒体文件链接 + self.content_str = self.content_str.replace('![](/media//','![](../../media/') + # print(self.html_str.format(pre_content=self.content_str)) + + # 创建写入临时HTML文件 + report_pdf_folder = settings.MEDIA_ROOT+'/report_pdf' + is_folder = os.path.exists(report_pdf_folder) + # 创建文件夹 + if is_folder is False: + os.mkdir(report_pdf_folder) + # 临时HTML和PDF文件名 + temp_file_name = '{}_{}'.format( + project.name, + str(datetime.datetime.today()).replace(' ', '-').replace(':', '-') + ) + # 临时HTML文件路径 + temp_file_path = report_pdf_folder + '/{0}.html'.format(temp_file_name) + # PDF文件路径 + report_file_path = report_pdf_folder + '/{0}.pdf'.format(temp_file_name) + # output_pdf_path = report_pdf_folder + '/{}_{}.pdf'.format( + # project.name, + # str(datetime.datetime.today()).replace(' ','-').replace(':','-') + # ) + # 写入HTML文件 + with open(temp_file_path, 'w', encoding='utf-8') as htmlfile: + htmlfile.write( + self.html_str.format( + title=project.name, + pre_content=self.content_str, + project_name=project.name, + author=project.create_user, + create_time=str(datetime.date.today()) + ) + ) + + # 执行HTML转PDF + html_to_pdf(temp_file_path,report_file_path) + # 处理PDF文件 + if os.path.exists(report_file_path): + # output = PyPDF2.PdfFileWriter() # 实例化一个PDF写入文件类,用于保存最后的PDF文件 + # tmp_pdf_file = open(report_file_path, 'rb') # 打开临时PDF + # input = PyPDF2.PdfFileReader(tmp_pdf_file) # 打开临时PDF文件 + # pdf_pages = input.getNumPages() # 获取临时PDF的页数 + # for p in range(pdf_pages): + # page = input.getPage(p) + # output.addPage(page) # 添加一页 + # page_content = high_level.extract_text(report_file_path, page_numbers=[p]) # 提取某页的文本 + # first_line_text = page_content.split('\n') # 获取某页的第一行文本 + # # 添加第一层级文档书签 + # for i1 in toc_list['1']: + # if i1['name'] in first_line_text: + # bookmark_1 = output.addBookmark(i1['name'], p, parent=None) # 添加书签 + # else: + # bookmark_1 = None + # # 添加第二层文档书签 + # for i2 in toc_list['2']: + # if i2['name'] in first_line_text: + # bookmark_2 = output.addBookmark(i2['name'], p, parent=bookmark_1) # 添加书签 + # # 添加第三层文档书签 + # for i3 in toc_list['3']: + # if i3['name'] in first_line_text: + # bookmark_3 = output.addBookmark(i3['name'], p, parent=bookmark_2) # 添加书签 + # + # output.setPageMode("/UseOutlines") # 默认打开书签 + # with open(output_pdf_path, 'wb') as output_pdf_file: + # output.write(output_pdf_file) + + # output_pdf_file.close() + + # 删除临时HTML文件和临时PDF文件 + # tmp_pdf_file.close() # 关闭临时PDF文件 + os.remove(temp_file_path) + # os.remove(report_file_path) + # print(report_file_path) + return report_file_path + else: + return False + + # 导出Docx class ReportDocx(): def __init__(self,project_id): @@ -697,7 +1153,12 @@ if __name__ == '__main__': # project_id=7 # ) # app.work() - app = ReportEPUB(project_id=20) + + # app = ReportEPUB(project_id=20) + # app.work() + + app = ReportPDF(project_id=20) app.work() + # app = ReportDocx(project_id=20) # app.work() \ No newline at end of file diff --git a/app_doc/templatetags/project_filter.py b/app_doc/templatetags/project_filter.py index 15bc760..dadff81 100644 --- a/app_doc/templatetags/project_filter.py +++ b/app_doc/templatetags/project_filter.py @@ -19,9 +19,9 @@ def get_new_doc(value): new_doc = '它还没有文档……' return new_doc -# 获取文集的开放导出状态 -@register.filter(name='get_report_status') -def get_report_status(value): +# 获取文集的EPUB开放导出状态 +@register.filter(name='report_status_epub') +def get_report_status_epub(value): try: project = Project.objects.get(id=int(value)) status = ProjectReport.objects.get(project=project).allow_epub @@ -30,6 +30,17 @@ def get_report_status(value): status = 0 return status +# 获取文集的PDF开放导出状态 +@register.filter(name='report_status_pdf') +def get_report_status_pdf(value): + try: + project = Project.objects.get(id=int(value)) + status = ProjectReport.objects.get(project=project).allow_pdf + except Exception as e: + # print(repr(e)) + status = 0 + return status + # 获取图片分组的图片数量 @register.filter(name='img_group_cnt') def get_img_group_cnt(value): diff --git a/app_doc/urls.py b/app_doc/urls.py index adc283c..80b2595 100644 --- a/app_doc/urls.py +++ b/app_doc/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ path('manage_project',views.manage_project,name="manage_project"), # 管理文集 path('del_project/',views.del_project,name='del_project'), # 删除文集 path('report_project_md/',views.report_md,name='report_md'), # 导出文集MD文件 + path('genera_project_file/',views.genera_project_file,name='genera_project_file'), # 个人中心生成文集文件(epub\docx\pdf等) path('report_project_file/',views.report_file,name='report_file'), # 导出文集文件(epub、docx等) path('modify_pro_role//',views.modify_project_role,name="modify_pro_role"),# 修改文集权限 path('modify_pro_download//', views.modify_project_download, name="modify_pro_download"), # 修改文集前台下载权限 diff --git a/app_doc/views.py b/app_doc/views.py index c3c5e16..066f448 100644 --- a/app_doc/views.py +++ b/app_doc/views.py @@ -9,6 +9,7 @@ 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 import datetime import traceback import re @@ -18,7 +19,7 @@ import os.path # 替换前端传来的非法字符 def validateTitle(title): - rstr = r"[\/\\\:\*\?\"\<\>\|]" # '/ \ : * ? " < > |' + rstr = r"[\/\\\:\*\?\"\<\>\|\[\]]" # '/ \ : * ? " < > |' new_title = re.sub(rstr, "_", title) # 替换为下划线 return new_title @@ -181,7 +182,7 @@ def create_project(request): role_list = ['0','1','2','3',0,1,2,3] if name != '': project = Project.objects.create( - name=name, + name=validateTitle(name), intro=desc[:100], create_user=request.user, role = int(role) if role in role_list else 0 @@ -214,9 +215,9 @@ def project_index(request,pro_id): # 获取问价文集前台下载权限 try: - allow_epub_download = ProjectReport.objects.get(project=project).allow_epub + allow_download = ProjectReport.objects.get(project=project) except ObjectDoesNotExist: - allow_epub_download = 0 + allow_download = False # 私密文集并且访问者非创建者非协作者 if (project.role == 1) and (request.user != project.create_user) and (colla_user == 0): @@ -266,7 +267,7 @@ def modify_project(request): if (request.user == project.create_user) or request.user.is_superuser: name = request.POST.get('name',None) content = request.POST.get('desc',None) - project.name = name + project.name = validateTitle(name) project.intro = content project.save() return JsonResponse({'status':True,'data':'修改成功'}) @@ -415,18 +416,30 @@ def modify_project_download(request,pro_id): if (pro.create_user != request.user) and (request.user.is_superuser is False): return render(request,'403.html') else: + project_files = ProjectReportFile.objects.filter(project=pro) if request.method == 'GET': return render(request,'app_doc/manage_project_download.html',locals()) elif request.method == 'POST': download_epub = request.POST.get('download_epub',None) + download_pdf = request.POST.get('download_pdf', None) # print("epub状态:",download_epub) + # EPUB下载权限 if download_epub == 'on': epub_status = 1 else: epub_status = 0 + # PDF下载权限 + if download_pdf == 'on': + pdf_status = 1 + else: + pdf_status = 0 + # 写入数据库 ProjectReport.objects.update_or_create( project = pro,defaults={'allow_epub':epub_status} ) + ProjectReport.objects.update_or_create( + project=pro, defaults={'allow_pdf': pdf_status} + ) return render(request,'app_doc/manage_project_download.html',locals()) @@ -526,7 +539,6 @@ def doc(request,pro_id,doc_id): # 私密文集且访问者非创建者、协作者 - 不能访问 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 @@ -842,6 +854,7 @@ def diff_doc(request,doc_id,his_id): print(traceback.print_exc()) return JsonResponse({'status':False,'data':'获取异常'}) + # 管理文档历史版本 @login_required() def manage_doc_history(request,doc_id): @@ -1106,7 +1119,6 @@ def get_pro_doc_tree(request): def handle_404(request): return render(request,'404.html') - # 导出文集MD文件 @login_required() def report_md(request): @@ -1133,43 +1145,252 @@ def report_md(request): else: return Http404 -# 导出文集文件 +# 生成文集文件 - 个人中心 - 文集管理 +@login_required() +def genera_project_file(request): + if request.method == 'POST': + report_type = request.POST.get('types',None) # 获取前端传入到导出文件类型参数 + # 导出EPUB文件 + + pro_id = request.POST.get('pro_id') + try: + project = Project.objects.get(id=int(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 project.role == 0: + allow_export = True + + # 私密文集 - 非创建者和协作者不可导出 + elif (project.role == 1): + if (request.user != project.create_user) and (colla_user == 0): + allow_export = False + else: + allow_export = True + + # 指定用户可见文集 - 指定用户、文集创建者和协作者可导出 + 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): # 访问者不在指定用户之中,也不是协作者 + allow_export = False + else: + allow_export = True + else: # 游客直接返回404 + allow_export = False + + # 访问码可见文集 - 文集创建者、协作者和通过验证即可导出 + 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中的访问码不等于文集访问码,不可导出 + allow_export = False + else: + allow_export = True + else: + allow_export = True + else: + allow_export = False + + # 允许被导出 + if allow_export: + # 导出EPUB + if report_type in ['epub']: + try: + report_project = ReportEPUB( + project_id=project.id + ).work() + # print(report_project) + report_file_path = report_project.split('media', maxsplit=1)[-1] # 导出文件的路径 + epub_file = '/media' + report_file_path + '.epub' # 文件相对路径 + # 查询文集是否存在导出文件 + report_cnt = ProjectReportFile.objects.filter(project=project,file_type='epub') + # 存在文件删除 + if report_cnt.count() != 0: + for r in report_cnt: + is_exist = os.path.exists(settings.BASE_DIR + r.file_path) + if is_exist: + os.remove(settings.BASE_DIR + r.file_path) + report_cnt.delete() # 删除数据库记录 + + # 创建数据库记录 + ProjectReportFile.objects.create( + project=project, + file_type='epub', + file_name=epub_file, + file_path=epub_file + ) + + return JsonResponse({'status': True, 'data': epub_file}) + except Exception as e: + if settings.DEBUG: + print(traceback.print_exc()) + return JsonResponse({'status': False, 'data': '生成出错'}) + # 导出PDF + elif report_type in ['pdf']: + try: + report_project = ReportPDF( + project_id=project.id + ).work() + if report_project is False: + return JsonResponse({'status':False,'data':'生成出错'}) + report_file_path = report_project.split('media', maxsplit=1)[-1] # 导出文件的路径 + pdf_file = '/media' + report_file_path # 文件相对路径 + # 查询文集是否存在导出文件 + report_cnt = ProjectReportFile.objects.filter(project=project, file_type='pdf') + # 存在文件删除 + if report_cnt.count() != 0: + for r in report_cnt: + is_exist = os.path.exists(settings.BASE_DIR + r.file_path) + if is_exist: + os.remove(settings.BASE_DIR + r.file_path) + report_cnt.delete() # 删除数据库记录 + + # 创建数据库记录 + ProjectReportFile.objects.create( + project=project, + file_type='pdf', + file_name=pdf_file, + file_path=pdf_file + ) + + return JsonResponse({'status': True, 'data': pdf_file}) + + except Exception as e: + if settings.DEBUG: + print(traceback.print_exc()) + return JsonResponse({'status': False, 'data': '生成出错'}) + else: + return JsonResponse({'status': False, 'data': '不支持的类型'}) + # 不允许被导出 + else: + return JsonResponse({'status':False,'data':'无权限导出'}) + + except ObjectDoesNotExist: + return JsonResponse({'status':False,'data':'文集不存在'}) + + except Exception as e: + if settings.DEBUG: + print(traceback.print_exc()) + return JsonResponse({'status':False,'data':'系统异常'}) + else: + return Http404 + + +# 获取文集前台导出文件 @allow_report_file def report_file(request): if request.method == 'POST': - report_type = request.POST.get('types',None) - if report_type in ['epub']: - pro_id = request.POST.get('pro_id') - try: - project = Project.objects.get(id=int(pro_id)) - # 公开的文集 - 可以直接导出 - if project.role == 0: - report_project = ReportEPUB( - project_id=project.id - ).work() - # print(report_project) - report_file_path = report_project.split('media',maxsplit=1)[-1] - epub_file = '/media' + report_file_path + '.epub' - return JsonResponse({'status':True,'data':epub_file}) - # 私密文集 - 拥有者可导出 - elif project.role == 1: - pass - # 指定用户可见文集 - 指定用户可导出 - elif project.role == 2: - pass - # 访问码可见文集 - 通过验证即可导出 - elif project.role == 3: - pass + report_type = request.POST.get('types',None) # 获取前端传入到导出文件类型参数 + + pro_id = request.POST.get('pro_id') + try: + project = Project.objects.get(id=int(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: - return JsonResponse({'status':False,'data':'不存在的文集权限'}) - except ObjectDoesNotExist: - return JsonResponse({'status':False,'data':'文集不存在'}) - except Exception as e: - if settings.DEBUG: - print(traceback.print_exc()) - return JsonResponse({'status':False,'data':'系统异常'}) - else: - return JsonResponse({'status':False,'data':'不支持的类型'}) + colla_user = colla_user.count() + else: + colla_user = 0 + + # 公开的文集 - 可以直接导出 + if project.role == 0: + allow_export = True + + # 私密文集 - 非创建者和协作者不可导出 + elif (project.role == 1): + if (request.user != project.create_user) and (colla_user == 0): + allow_export = False + else: + allow_export = True + + # 指定用户可见文集 - 指定用户、文集创建者和协作者可导出 + 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): # 访问者不在指定用户之中,也不是协作者 + allow_export = False + else: + allow_export = True + else: # 游客直接返回404 + allow_export = False + # 访问码可见文集 - 文集创建者、协作者和通过验证即可导出 + 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中的访问码不等于文集访问码,不可导出 + allow_export = False + else: + allow_export = True + else: + allow_export = True + else: + allow_export = False + # return JsonResponse({'status':False,'data':'不存在的文集权限'}) + if allow_export: + # 导出EPUB文件 + if report_type in ['epub']: + try: + try: + report_project = ProjectReportFile.objects.get(project=project,file_type='epub') + except ObjectDoesNotExist: + return JsonResponse({'status':False,'data':'无可用文件,请联系文集创建者'}) + # print(report_project) + return JsonResponse({'status': True, 'data': report_project.file_path}) + except Exception as e: + if settings.DEBUG: + print(traceback.print_exc()) + return JsonResponse({'status': False, 'data': '导出出错'}) + # 导出PDF + elif report_type in ['pdf']: + try: + try: + report_project = ProjectReportFile.objects.get(project=project,file_type='pdf') + except ObjectDoesNotExist: + return JsonResponse({'status':False,'data':'无可用文件,请联系文集创建者'}) + # print(report_project) + return JsonResponse({'status': True, 'data': report_project.file_path}) + except Exception as e: + if settings.DEBUG: + print(traceback.print_exc()) + return JsonResponse({'status': False, 'data': '导出出错'}) + else: + return JsonResponse({'status': False, 'data': '不支持的类型'}) + else: + return JsonResponse({'status':False,'data':'无权限导出'}) + except ObjectDoesNotExist: + return JsonResponse({'status':False,'data':'文集不存在'}) + except Exception as e: + if settings.DEBUG: + print(traceback.print_exc()) + return JsonResponse({'status':False,'data':'系统异常'}) + else: return Http404 diff --git a/captrue/mrdoc-index.png b/captrue/mrdoc-index.png index 64fc660..58b35ca 100644 Binary files a/captrue/mrdoc-index.png and b/captrue/mrdoc-index.png differ diff --git a/requirements.txt b/requirements.txt index 15c676d..422d9d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -beautifulsoup4==4.8.2 django==2.2.12 +beautifulsoup4==4.8.2 +lxml pillow==6.2.2 -pytz==2019.1 -sqlparse==0.3.0 +pyppeteer==0.0.25 \ No newline at end of file diff --git a/static/editor.md/editormd.js b/static/editor.md/editormd.js index 85a3670..2132de7 100644 --- a/static/editor.md/editormd.js +++ b/static/editor.md/editormd.js @@ -3672,7 +3672,15 @@ } // var map_id = lang.split('>')[1]; // console.log(map_id) - return ""+code+""; + var custom_height; + var h = lang.split('>')[1]; + if(h != undefined){ + custom_height = h; + }else{ + custom_height = 150; + } + + return ""+code+""; } else { @@ -3952,7 +3960,7 @@ taskList : false, // Github Flavored Markdown task lists emoji : false, flowChart : false, - mindMap : true, //百度脑图 + mindMap : true, //脑图 sequenceDiagram : false, previewCodeHighlight : true }; diff --git a/static/logo.jpg b/static/logo.jpg new file mode 100644 index 0000000..f7572dd Binary files /dev/null and b/static/logo.jpg differ diff --git a/static/report_epub/epub_cover1.jpg b/static/report_epub/epub_cover1.jpg index 4822112..fe83dea 100644 Binary files a/static/report_epub/epub_cover1.jpg and b/static/report_epub/epub_cover1.jpg differ diff --git a/template/403.html b/template/403.html index 25f27d2..b1ecb7e 100644 --- a/template/403.html +++ b/template/403.html @@ -3,7 +3,7 @@ - 拒绝访问 - MrDoc + 拒绝访问 - 觅道文档MrDoc diff --git a/template/404.html b/template/404.html index 5d322ca..c9f3bd6 100644 --- a/template/404.html +++ b/template/404.html @@ -3,7 +3,7 @@ - 404 页面未找到 - MrDoc + 404 页面未找到 - 觅道文档MrDoc diff --git a/template/app_admin/admin_base.html b/template/app_admin/admin_base.html index df76b3d..7cb2cb6 100644 --- a/template/app_admin/admin_base.html +++ b/template/app_admin/admin_base.html @@ -3,7 +3,7 @@ - {% block title %}{% endblock %} - 后台管理 - MrDoc + {% block title %}{% endblock %} - 后台管理 - 觅道文档MrDoc diff --git a/template/app_doc/manage_project.html b/template/app_doc/manage_project.html index cc63c10..ed3119a 100644 --- a/template/app_doc/manage_project.html +++ b/template/app_doc/manage_project.html @@ -53,10 +53,11 @@ {{ pro.id | get_doc_count }} {{ pro.create_time }} - {% if pro.id|get_report_status == 1 %} - 允许 + {% if pro.id|report_status_epub == 1 or pro.id|report_status_pdf == 1 %} + + 允许 {% else %} - 禁止 + 禁止 {% endif %} diff --git a/template/app_doc/manage_project_download.html b/template/app_doc/manage_project_download.html index 93df6d8..5f5798b 100644 --- a/template/app_doc/manage_project_download.html +++ b/template/app_doc/manage_project_download.html @@ -1,10 +1,10 @@ {% extends 'app_doc/manage_base.html' %} {% load staticfiles %} -{% block title %}文集下载状态管理{% endblock %} +{% block title %}文集下载状态管理 - {{pro.name}}{% endblock %} {% block content %} {% if enable_project_report %} {% else %} -
当前站点未启用前台文集下载功能,修改操作将在功能启用后生效!
+
站点管理员未启用文集导出下载文件功能,相关操作将在此功能启用后生效!
{% endif %}
@@ -33,22 +33,76 @@
+ + {% if enable_project_report %} + 生成或更新EPUB文件 + {% if project_files %} + {% for file in project_files %} + {% if file.file_type == 'epub' %} + | 下载文集EPUB文件 + {% else %} + + {% endif %} + {% endfor %} + {% else %} + | 未生成文集导出文件 + {% endif %} + {% endif %}
+
- + + + {% if enable_project_report %} + 生成或更新PDF文件 + {% if project_files %} + {% for file in project_files %} + {% if file.file_type == 'pdf' %} + | 下载文集PDF文件 + {% else %} + + {% endif %} + {% endfor %} + {% else %} + | 未生成文集导出文件 + {% endif %} + {% endif %}
+ +
+
+ +
+
+
+
注意:开启某类型文件下载后,请先点击“生成或更新XXX文件”,文集文档中如果包含公式、流程图、时序图、脑图等内容,将会延长生成时间,请耐心等待
+
{% endblock %} {% block custom_script %} {% endblock %} \ No newline at end of file diff --git a/template/app_doc/manage_project_role.html b/template/app_doc/manage_project_role.html index 9bc6b83..2da3f70 100644 --- a/template/app_doc/manage_project_role.html +++ b/template/app_doc/manage_project_role.html @@ -57,7 +57,7 @@
- +
diff --git a/template/app_doc/pro_list.html b/template/app_doc/pro_list.html index a7d436b..9716788 100644 --- a/template/app_doc/pro_list.html +++ b/template/app_doc/pro_list.html @@ -7,10 +7,10 @@ - - + + - MrDoc - 一个简单的文档写作系统 + 觅道文档(MrDoc) - 一个简单的在线文档系统 diff --git a/template/app_doc/project.html b/template/app_doc/project.html index 6aaa6cf..5c7acb6 100644 --- a/template/app_doc/project.html +++ b/template/app_doc/project.html @@ -74,10 +74,15 @@

文集下载:

- {% if allow_epub_download == 1 %} - EPUB电子书 + {% if allow_download %} + {% if allow_download.allow_epub == 1 %} + EPUB电子书 + {% endif %} + {% if allow_download.allow_pdf == 1 %} + PDF电子书 + {% endif %} {% else %} - 文集作者未开放此文集的任何格式下载! + 文集作者未开放此文集的任何格式下载! {% endif %} @@ -85,13 +90,15 @@