* changes exception and raise statements to python 3 style * changes except statement to python 3 style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * changes except and raise statement to python 3 compatible style * adds six.reraise to fix python 3 style raise statements with traceback * fixes indentation
283 lines
8.5 KiB
Python
283 lines
8.5 KiB
Python
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: GNU General Public License v3. See license.txt
|
|
|
|
from __future__ import unicode_literals, print_function
|
|
|
|
import frappe
|
|
import hashlib
|
|
|
|
from frappe.model.db_schema import DbManager
|
|
from frappe.installer import get_root_connection
|
|
from frappe.database import Database
|
|
import os
|
|
from markdown2 import markdown
|
|
from bs4 import BeautifulSoup
|
|
import jinja2.exceptions
|
|
|
|
def sync():
|
|
# make table
|
|
print('Syncing help database...')
|
|
help_db = HelpDatabase()
|
|
help_db.make_database()
|
|
help_db.connect()
|
|
help_db.make_table()
|
|
help_db.sync_pages()
|
|
help_db.build_index()
|
|
|
|
@frappe.whitelist()
|
|
def get_help(text):
|
|
return HelpDatabase().search(text)
|
|
|
|
@frappe.whitelist()
|
|
def get_help_content(path):
|
|
return HelpDatabase().get_content(path)
|
|
|
|
class HelpDatabase(object):
|
|
def __init__(self):
|
|
self.global_help_setup = frappe.conf.get('global_help_setup')
|
|
if self.global_help_setup:
|
|
bench_name = os.path.basename(os.path.abspath(frappe.get_app_path('frappe')).split('/apps/')[0])
|
|
self.help_db_name = hashlib.sha224(bench_name).hexdigest()[:15]
|
|
|
|
def make_database(self):
|
|
'''make database for global help setup'''
|
|
if not self.global_help_setup:
|
|
return
|
|
|
|
dbman = DbManager(get_root_connection())
|
|
dbman.drop_database(self.help_db_name)
|
|
|
|
# make database
|
|
if not self.help_db_name in dbman.get_database_list():
|
|
try:
|
|
dbman.create_user(self.help_db_name, self.help_db_name)
|
|
except Exception as e:
|
|
# user already exists
|
|
if e.args[0] != 1396: raise
|
|
dbman.create_database(self.help_db_name)
|
|
dbman.grant_all_privileges(self.help_db_name, self.help_db_name)
|
|
dbman.flush_privileges()
|
|
|
|
def connect(self):
|
|
if self.global_help_setup:
|
|
self.db = Database(user=self.help_db_name, password=self.help_db_name)
|
|
else:
|
|
self.db = frappe.db
|
|
|
|
def make_table(self):
|
|
if not 'help' in self.db.get_tables():
|
|
self.db.sql('''create table help(
|
|
path varchar(255),
|
|
content text,
|
|
title text,
|
|
intro text,
|
|
full_path text,
|
|
fulltext(title),
|
|
fulltext(content),
|
|
index (path))
|
|
COLLATE=utf8mb4_unicode_ci
|
|
ENGINE=MyISAM
|
|
CHARACTER SET=utf8mb4''')
|
|
|
|
def search(self, words):
|
|
self.connect()
|
|
return self.db.sql('''
|
|
select title, intro, path from help where title like %s union
|
|
select title, intro, path from help where match(content) against (%s) limit 10''', ('%'+words+'%', words))
|
|
|
|
def get_content(self, path):
|
|
self.connect()
|
|
query = '''select title, content from help
|
|
where path like "{path}%" order by path desc limit 1'''
|
|
result = None
|
|
|
|
if not path.endswith('index'):
|
|
result = self.db.sql(query.format(path=os.path.join(path, 'index')))
|
|
|
|
if not result:
|
|
result = self.db.sql(query.format(path=path))
|
|
|
|
return {'title':result[0][0], 'content':result[0][1]} if result else {}
|
|
|
|
def sync_pages(self):
|
|
self.db.sql('truncate help')
|
|
doc_contents = '<ol>'
|
|
apps = os.listdir('../apps') if self.global_help_setup else frappe.get_installed_apps()
|
|
|
|
for app in apps:
|
|
docs_folder = '../apps/{app}/{app}/docs/user'.format(app=app)
|
|
self.out_base_path = '../apps/{app}/{app}/docs'.format(app=app)
|
|
if os.path.exists(docs_folder):
|
|
app_name = getattr(frappe.get_module(app), '__title__', None) or app.title()
|
|
doc_contents += '<li><a data-path="/{app}/index">{app_name}</a></li>'.format(
|
|
app=app, app_name=app_name)
|
|
|
|
for basepath, folders, files in os.walk(docs_folder):
|
|
files = self.reorder_files(files)
|
|
for fname in files:
|
|
if fname.rsplit('.', 1)[-1] in ('md', 'html'):
|
|
fpath = os.path.join(basepath, fname)
|
|
with open(fpath, 'r') as f:
|
|
try:
|
|
content = frappe.render_template(unicode(f.read(), 'utf-8'),
|
|
{'docs_base_url': '/assets/{app}_docs'.format(app=app)})
|
|
|
|
relpath = self.get_out_path(fpath)
|
|
relpath = relpath.replace("user", app)
|
|
content = markdown(content)
|
|
title = self.make_title(basepath, fname, content)
|
|
intro = self.make_intro(content)
|
|
content = self.make_content(content, fpath, relpath)
|
|
self.db.sql('''insert into help(path, content, title, intro, full_path)
|
|
values (%s, %s, %s, %s, %s)''', (relpath, content, title, intro, fpath))
|
|
except jinja2.exceptions.TemplateSyntaxError:
|
|
print("Invalid Jinja Template for {0}. Skipping".format(fpath))
|
|
|
|
doc_contents += "</ol>"
|
|
self.db.sql('''insert into help(path, content, title, intro, full_path) values (%s, %s, %s, %s, %s)''',
|
|
('/documentation/index', doc_contents, 'Documentation', '', ''))
|
|
|
|
|
|
def make_title(self, basepath, filename, html):
|
|
if '<h1>' in html:
|
|
title = html.split("<h1>", 1)[1].split("</h1>", 1)[0]
|
|
elif 'index' in filename:
|
|
title = basepath.rsplit('/', 1)[-1].title().replace("-", " ")
|
|
else:
|
|
title = filename.rsplit('.', 1)[0].title().replace("-", " ")
|
|
return title
|
|
|
|
def make_intro(self, html):
|
|
intro = ""
|
|
if '<p>' in html:
|
|
intro = html.split('<p>', 1)[1].split('</p>', 1)[0]
|
|
if 'Duration' in html:
|
|
intro = "Help Video: " + intro
|
|
return intro
|
|
|
|
def make_content(self, html, path, relpath):
|
|
|
|
if '<h1>' in html:
|
|
html = html.split('</h1>', 1)[1]
|
|
|
|
if '{next}' in html:
|
|
html = html.replace('{next}', '')
|
|
|
|
target = path.split('/', 3)[-1]
|
|
app_name = path.split('/', 3)[2]
|
|
html += '''
|
|
<div class="page-container">
|
|
<div class="page-content">
|
|
<div class="edit-container text-center">
|
|
<i class="fa fa-smile text-muted"></i>
|
|
<a class="edit text-muted" href="https://github.com/frappe/{app_name}/blob/develop/{target}">
|
|
Improve this page
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>'''.format(app_name=app_name, target=target)
|
|
|
|
soup = BeautifulSoup(html, 'html.parser')
|
|
|
|
for link in soup.find_all('a'):
|
|
if link.has_attr('href'):
|
|
url = link['href']
|
|
if '/user' in url:
|
|
data_path = url[url.index('/user'):]
|
|
if '.' in data_path:
|
|
data_path = data_path[: data_path.rindex('.')]
|
|
if data_path:
|
|
link['data-path'] = data_path.replace("user", app_name)
|
|
|
|
parent = self.get_parent(relpath)
|
|
if parent:
|
|
parent_tag = soup.new_tag('a')
|
|
parent_tag.string = parent['title']
|
|
parent_tag['class'] = 'parent-link'
|
|
parent_tag['data-path'] = parent['path']
|
|
soup.find().insert_before(parent_tag)
|
|
|
|
return soup.prettify()
|
|
|
|
def build_index(self):
|
|
for data in self.db.sql('select path, full_path, content from help'):
|
|
self.make_index(data[0], data[1], data[2])
|
|
|
|
def make_index(self, original_path, full_path, content):
|
|
'''Make index from index.txt'''
|
|
if '{index}' in content:
|
|
path = os.path.dirname(full_path)
|
|
files = []
|
|
|
|
# get files from index.txt
|
|
index_path = os.path.join(path, "index.txt")
|
|
if os.path.exists(index_path):
|
|
with open(index_path, 'r') as f:
|
|
files = f.read().splitlines()
|
|
|
|
# files not in index.txt
|
|
for f in os.listdir(path):
|
|
if not os.path.isdir(os.path.join(path, f)):
|
|
name, extn = f.rsplit('.', 1)
|
|
if name not in files \
|
|
and name != 'index' and extn in ('md', 'html'):
|
|
files.append(name)
|
|
|
|
links_html = "<ol class='index-links'>"
|
|
for line in files:
|
|
fpath = os.path.join(os.path.dirname(original_path), line)
|
|
|
|
title = self.db.sql('select title from help where path like %s',
|
|
os.path.join(fpath, 'index') + '%')
|
|
if not title:
|
|
title = self.db.sql('select title from help where path like %s',
|
|
fpath + '%')
|
|
|
|
if title:
|
|
title = title[0][0]
|
|
links_html += "<li><a data-path='{fpath}'> {title} </a></li>".format(
|
|
fpath=fpath, title=title)
|
|
# else:
|
|
# bad entries in .txt files
|
|
# print fpath
|
|
|
|
links_html += "</ol>"
|
|
html = content.replace('{index}', links_html)
|
|
|
|
self.db.sql('update help set content=%s where path=%s', (html, original_path))
|
|
|
|
def get_out_path(self, path):
|
|
return '/' + os.path.relpath(path, self.out_base_path)
|
|
|
|
def get_parent(self, child_path):
|
|
if 'index' in child_path:
|
|
child_path = child_path[: child_path.rindex('index')]
|
|
if child_path[-1] == '/':
|
|
child_path = child_path[:-1]
|
|
child_path = child_path[: child_path.rindex('/')]
|
|
|
|
out = None
|
|
if child_path:
|
|
parent_path = child_path + "/index"
|
|
out = self.get_content(parent_path)
|
|
#if parent is documentation root
|
|
else:
|
|
parent_path = "/documentation/index"
|
|
out = {}
|
|
out['title'] = "Documentation"
|
|
|
|
if not out:
|
|
return None
|
|
|
|
out['path'] = parent_path
|
|
return out
|
|
|
|
def reorder_files(self, files):
|
|
pos = 0
|
|
if 'index.md' in files:
|
|
pos = files.index('index.md')
|
|
elif 'index.html' in files:
|
|
pos = files.index('index.html')
|
|
if pos:
|
|
files[0], files[pos] = files[pos], files[0]
|
|
return files
|