Open Tradon processes inside Tryton
This commit is contained in:
@@ -34,6 +34,7 @@ from . import (
|
||||
backtoback,
|
||||
service,
|
||||
invoice,
|
||||
process_documentation,
|
||||
routes,
|
||||
)
|
||||
|
||||
@@ -60,6 +61,7 @@ def register():
|
||||
lc.LCMessage,
|
||||
lc.CreateLCStart,
|
||||
global_reporting.GRConfiguration,
|
||||
process_documentation.ProcessDocumentation,
|
||||
pricing.ImportPricesStart,
|
||||
pricing.ImportPricesResult,
|
||||
module='purchase_trade', type_='model')
|
||||
|
||||
238
modules/purchase_trade/process_documentation.py
Normal file
238
modules/purchase_trade/process_documentation.py
Normal file
@@ -0,0 +1,238 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import base64
|
||||
import html
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
|
||||
|
||||
|
||||
DOCUMENTATION_ROOT = Path(__file__).with_name('process_documentation')
|
||||
MARKDOWN_LINK = re.compile(r'(!?)\[([^\]]*)\]\(([^)]+)\)')
|
||||
|
||||
|
||||
class ProcessDocumentation(ModelSingleton, ModelSQL, ModelView):
|
||||
"Process Documentation"
|
||||
__name__ = 'purchase_trade.process.documentation'
|
||||
|
||||
content = fields.Function(fields.Text('Content'), 'get_content')
|
||||
|
||||
def get_content(self, name):
|
||||
pages = _documentation_pages()
|
||||
return HTML_TEMPLATE % {
|
||||
'initial': pages.get('README.md', ''),
|
||||
'pages': json.dumps(pages).replace('</', '<\\/'),
|
||||
}
|
||||
|
||||
|
||||
def _documentation_pages():
|
||||
pages = {}
|
||||
for path in sorted(DOCUMENTATION_ROOT.rglob('*.md')):
|
||||
relative_name = _relative_name(path)
|
||||
markdown = path.read_text(encoding='utf-8')
|
||||
pages[relative_name] = _markdown_to_html(markdown, relative_name)
|
||||
return pages
|
||||
|
||||
|
||||
def _relative_name(path):
|
||||
return path.relative_to(DOCUMENTATION_ROOT.resolve()).as_posix()
|
||||
|
||||
|
||||
def _resolve_link(current_file, target):
|
||||
if target.startswith(('#', 'http://', 'https://', 'mailto:')):
|
||||
return target
|
||||
target = unquote(target).replace('\\', '/')
|
||||
current_dir = posixpath.dirname(current_file)
|
||||
return posixpath.normpath(posixpath.join(current_dir, target)).lstrip('/')
|
||||
|
||||
|
||||
def _asset_data_uri(filename):
|
||||
path = (DOCUMENTATION_ROOT / Path(*filename.split('/'))).resolve()
|
||||
root = DOCUMENTATION_ROOT.resolve()
|
||||
common_path = os.path.commonpath([str(root), str(path)])
|
||||
if path != root and common_path == str(root):
|
||||
if path.exists() and path.is_file():
|
||||
mimetype = mimetypes.guess_type(str(path))[0]
|
||||
mimetype = mimetype or 'application/octet-stream'
|
||||
data = base64.b64encode(path.read_bytes()).decode('ascii')
|
||||
return 'data:%s;base64,%s' % (mimetype, data)
|
||||
|
||||
|
||||
def _inline(markdown, current_file):
|
||||
parts = []
|
||||
last = 0
|
||||
for match in MARKDOWN_LINK.finditer(markdown):
|
||||
parts.append(html.escape(markdown[last:match.start()]))
|
||||
is_image, label, target = match.groups()
|
||||
resolved = _resolve_link(current_file, target)
|
||||
label = html.escape(label)
|
||||
|
||||
if is_image:
|
||||
source = _asset_data_uri(resolved)
|
||||
if source:
|
||||
parts.append('<img src="%s" alt="%s"/>' % (
|
||||
html.escape(source, quote=True), label))
|
||||
else:
|
||||
parts.append(label)
|
||||
elif resolved.endswith('.md'):
|
||||
parts.append('<a href="#" data-doc="%s">%s</a>' % (
|
||||
html.escape(resolved, quote=True), label))
|
||||
elif resolved.startswith(('#', 'http://', 'https://', 'mailto:')):
|
||||
parts.append('<a href="%s">%s</a>' % (
|
||||
html.escape(resolved, quote=True), label))
|
||||
else:
|
||||
parts.append(html.escape(match.group(0)))
|
||||
last = match.end()
|
||||
parts.append(html.escape(markdown[last:]))
|
||||
return ''.join(parts)
|
||||
|
||||
|
||||
def _markdown_to_html(markdown, current_file):
|
||||
body = []
|
||||
table = []
|
||||
in_list = False
|
||||
|
||||
def close_list():
|
||||
nonlocal in_list
|
||||
if in_list:
|
||||
body.append('</ul>')
|
||||
in_list = False
|
||||
|
||||
def flush_table():
|
||||
nonlocal table
|
||||
if table:
|
||||
close_list()
|
||||
body.append(_render_table(table, current_file))
|
||||
table = []
|
||||
|
||||
for line in markdown.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('|') and stripped.endswith('|'):
|
||||
table.append(stripped)
|
||||
continue
|
||||
|
||||
flush_table()
|
||||
if not stripped:
|
||||
close_list()
|
||||
continue
|
||||
|
||||
if stripped.startswith('#'):
|
||||
close_list()
|
||||
level = len(stripped) - len(stripped.lstrip('#'))
|
||||
level = min(level, 6)
|
||||
title = stripped[level:].strip()
|
||||
body.append('<h%d>%s</h%d>' % (
|
||||
level, _inline(title, current_file), level))
|
||||
elif stripped.startswith('- '):
|
||||
if not in_list:
|
||||
body.append('<ul>')
|
||||
in_list = True
|
||||
body.append('<li>%s</li>' % _inline(
|
||||
stripped[2:].strip(), current_file))
|
||||
else:
|
||||
close_list()
|
||||
body.append('<p>%s</p>' % _inline(stripped, current_file))
|
||||
|
||||
flush_table()
|
||||
close_list()
|
||||
|
||||
if current_file != 'README.md':
|
||||
body.insert(
|
||||
0, '<p><a href="#" data-doc="README.md">Back to index</a></p>')
|
||||
return '\n'.join(body)
|
||||
|
||||
|
||||
def _render_table(lines, current_file):
|
||||
rows = [
|
||||
[cell.strip() for cell in line.strip('|').split('|')]
|
||||
for line in lines
|
||||
]
|
||||
header = rows[0] if rows else []
|
||||
data = rows[1:]
|
||||
if data and all(set(cell) <= {'-', ':'} for cell in data[0]):
|
||||
data = data[1:]
|
||||
|
||||
output = ['<table>', '<thead><tr>']
|
||||
for cell in header:
|
||||
output.append('<th>%s</th>' % _inline(cell, current_file))
|
||||
output.append('</tr></thead><tbody>')
|
||||
for row in data:
|
||||
output.append('<tr>')
|
||||
for cell in row:
|
||||
output.append('<td>%s</td>' % _inline(cell, current_file))
|
||||
output.append('</tr>')
|
||||
output.append('</tbody></table>')
|
||||
return ''.join(output)
|
||||
|
||||
|
||||
HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<style>
|
||||
body {
|
||||
color: #1f2933;
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
a {
|
||||
color: #0b63ce;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
width: 100%%;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #d7dde5;
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #f3f6f9;
|
||||
}
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main id="content">%(initial)s</main>
|
||||
<script>
|
||||
const pages = %(pages)s;
|
||||
const content = document.getElementById('content');
|
||||
|
||||
function showDocument(name) {
|
||||
content.innerHTML = pages[name] || pages['README.md'];
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(event) {
|
||||
const link = event.target.closest('a[data-doc]');
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
showDocument(link.dataset.doc);
|
||||
});
|
||||
|
||||
showDocument('README.md');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
@@ -3,9 +3,21 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.action.url" id="url_tradon_processes">
|
||||
<record model="ir.ui.view" id="process_documentation_view_form">
|
||||
<field name="model">purchase_trade.process.documentation</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">process_documentation_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_tradon_processes">
|
||||
<field name="name">Tradon Processes</field>
|
||||
<field name="url">/tradon/purchase_trade/processes</field>
|
||||
<field name="res_model">purchase_trade.process.documentation</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_tradon_processes_view_form">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="process_documentation_view_form"/>
|
||||
<field name="act_window" ref="act_tradon_processes"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
@@ -17,8 +29,16 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<menuitem
|
||||
name="Tradon Processes"
|
||||
parent="menu_help_processes"
|
||||
action="url_tradon_processes"
|
||||
action="act_tradon_processes"
|
||||
sequence="10"
|
||||
id="menu_tradon_processes"/>
|
||||
|
||||
<record model="ir.model.access" id="access_process_documentation">
|
||||
<field name="model">purchase_trade.process.documentation</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<form col="1">
|
||||
<field name="content" widget="html_viewer" height="900"/>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user