You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

284 line
8.4 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: GNU General Public License v3. See license.txt
  3. from __future__ import unicode_literals
  4. import frappe
  5. import hashlib
  6. from frappe.model.db_schema import DbManager
  7. from frappe.installer import get_root_connection
  8. from frappe.database import Database
  9. import os
  10. from markdown2 import markdown
  11. from bs4 import BeautifulSoup
  12. import jinja2.exceptions
  13. def sync():
  14. # make table
  15. print 'Syncing help database...'
  16. help_db = HelpDatabase()
  17. help_db.make_database()
  18. help_db.connect()
  19. help_db.make_table()
  20. help_db.sync_pages()
  21. help_db.build_index()
  22. @frappe.whitelist()
  23. def get_help(text):
  24. return HelpDatabase().search(text)
  25. @frappe.whitelist()
  26. def get_help_content(path):
  27. return HelpDatabase().get_content(path)
  28. class HelpDatabase(object):
  29. def __init__(self):
  30. self.global_help_setup = frappe.conf.get('global_help_setup')
  31. if self.global_help_setup:
  32. bench_name = os.path.basename(os.path.abspath(frappe.get_app_path('frappe')).split('/apps/')[0])
  33. self.help_db_name = hashlib.sha224(bench_name).hexdigest()[:15]
  34. def make_database(self):
  35. '''make database for global help setup'''
  36. if not self.global_help_setup:
  37. return
  38. dbman = DbManager(get_root_connection())
  39. dbman.drop_database(self.help_db_name)
  40. # make database
  41. if not self.help_db_name in dbman.get_database_list():
  42. try:
  43. dbman.create_user(self.help_db_name, self.help_db_name)
  44. except Exception, e:
  45. # user already exists
  46. if e.args[0] != 1396: raise
  47. dbman.create_database(self.help_db_name)
  48. dbman.grant_all_privileges(self.help_db_name, self.help_db_name)
  49. dbman.flush_privileges()
  50. def connect(self):
  51. if self.global_help_setup:
  52. self.db = Database(user=self.help_db_name, password=self.help_db_name)
  53. else:
  54. self.db = frappe.db
  55. def make_table(self):
  56. if not 'help' in self.db.get_tables():
  57. self.db.sql('''create table help(
  58. path varchar(255),
  59. content text,
  60. title text,
  61. intro text,
  62. full_path text,
  63. fulltext(title),
  64. fulltext(content),
  65. index (path))
  66. COLLATE=utf8mb4_unicode_ci
  67. ENGINE=MyISAM
  68. CHARACTER SET=utf8mb4''')
  69. def search(self, words):
  70. self.connect()
  71. return self.db.sql('''
  72. select title, intro, path from help where title like %s union
  73. select title, intro, path from help where match(content) against (%s) limit 10''', ('%'+words+'%', words))
  74. def get_content(self, path):
  75. self.connect()
  76. query = '''select title, content from help
  77. where path like "{path}%" order by path desc limit 1'''
  78. result = None
  79. if not path.endswith('index'):
  80. result = self.db.sql(query.format(path=os.path.join(path, 'index')))
  81. if not result:
  82. result = self.db.sql(query.format(path=path))
  83. return {'title':result[0][0], 'content':result[0][1]} if result else {}
  84. def sync_pages(self):
  85. self.db.sql('truncate help')
  86. doc_contents = '<ol>'
  87. apps = os.listdir('../apps') if self.global_help_setup else frappe.get_installed_apps()
  88. for app in apps:
  89. docs_folder = '../apps/{app}/{app}/docs/user'.format(app=app)
  90. self.out_base_path = '../apps/{app}/{app}/docs'.format(app=app)
  91. if os.path.exists(docs_folder):
  92. app_name = getattr(frappe.get_module(app), '__title__', None) or app.title()
  93. doc_contents += '<li><a data-path="/{app}/index">{app_name}</a></li>'.format(
  94. app=app, app_name=app_name)
  95. for basepath, folders, files in os.walk(docs_folder):
  96. files = self.reorder_files(files)
  97. for fname in files:
  98. if fname.rsplit('.', 1)[-1] in ('md', 'html'):
  99. fpath = os.path.join(basepath, fname)
  100. with open(fpath, 'r') as f:
  101. try:
  102. content = frappe.render_template(unicode(f.read(), 'utf-8'),
  103. {'docs_base_url': '/assets/{app}_docs'.format(app=app)})
  104. relpath = self.get_out_path(fpath)
  105. relpath = relpath.replace("user", app)
  106. content = markdown(content)
  107. title = self.make_title(basepath, fname, content)
  108. intro = self.make_intro(content)
  109. content = self.make_content(content, fpath, relpath)
  110. self.db.sql('''insert into help(path, content, title, intro, full_path)
  111. values (%s, %s, %s, %s, %s)''', (relpath, content, title, intro, fpath))
  112. except jinja2.exceptions.TemplateSyntaxError:
  113. print "Invalid Jinja Template for {0}. Skipping".format(fpath)
  114. doc_contents += "</ol>"
  115. self.db.sql('''insert into help(path, content, title, intro, full_path) values (%s, %s, %s, %s, %s)''',
  116. ('/documentation/index', doc_contents, 'Documentation', '', ''))
  117. def make_title(self, basepath, filename, html):
  118. if '<h1>' in html:
  119. title = html.split("<h1>", 1)[1].split("</h1>", 1)[0]
  120. elif 'index' in filename:
  121. title = basepath.rsplit('/', 1)[-1].title().replace("-", " ")
  122. else:
  123. title = filename.rsplit('.', 1)[0].title().replace("-", " ")
  124. return title
  125. def make_intro(self, html):
  126. intro = ""
  127. if '<p>' in html:
  128. intro = html.split('<p>', 1)[1].split('</p>', 1)[0]
  129. if 'Duration' in html:
  130. intro = "Help Video: " + intro
  131. return intro
  132. def make_content(self, html, path, relpath):
  133. if '<h1>' in html:
  134. html = html.split('</h1>', 1)[1]
  135. if '{next}' in html:
  136. html = html.replace('{next}', '')
  137. target = path.split('/', 3)[-1]
  138. app_name = path.split('/', 3)[2]
  139. html += '''
  140. <div class="page-container">
  141. <div class="page-content">
  142. <div class="edit-container text-center">
  143. <i class="fa fa-smile text-muted"></i>
  144. <a class="edit text-muted" href="https://github.com/frappe/{app_name}/blob/develop/{target}">
  145. Improve this page
  146. </a>
  147. </div>
  148. </div>
  149. </div>'''.format(app_name=app_name, target=target)
  150. soup = BeautifulSoup(html, 'html.parser')
  151. for link in soup.find_all('a'):
  152. if link.has_attr('href'):
  153. url = link['href']
  154. if '/user' in url:
  155. data_path = url[url.index('/user'):]
  156. if '.' in data_path:
  157. data_path = data_path[: data_path.rindex('.')]
  158. if data_path:
  159. link['data-path'] = data_path.replace("user", app_name)
  160. parent = self.get_parent(relpath)
  161. if parent:
  162. parent_tag = soup.new_tag('a')
  163. parent_tag.string = parent['title']
  164. parent_tag['class'] = 'parent-link'
  165. parent_tag['data-path'] = parent['path']
  166. soup.find().insert_before(parent_tag)
  167. return soup.prettify()
  168. def build_index(self):
  169. for data in self.db.sql('select path, full_path, content from help'):
  170. self.make_index(data[0], data[1], data[2])
  171. def make_index(self, original_path, full_path, content):
  172. '''Make index from index.txt'''
  173. if '{index}' in content:
  174. path = os.path.dirname(full_path)
  175. files = []
  176. # get files from index.txt
  177. index_path = os.path.join(path, "index.txt")
  178. if os.path.exists(index_path):
  179. with open(index_path, 'r') as f:
  180. files = f.read().splitlines()
  181. # files not in index.txt
  182. for f in os.listdir(path):
  183. if not os.path.isdir(os.path.join(path, f)):
  184. name, extn = f.rsplit('.', 1)
  185. if name not in files \
  186. and name != 'index' and extn in ('md', 'html'):
  187. files.append(name)
  188. links_html = "<ol class='index-links'>"
  189. for line in files:
  190. fpath = os.path.join(os.path.dirname(original_path), line)
  191. title = self.db.sql('select title from help where path like %s',
  192. os.path.join(fpath, 'index') + '%')
  193. if not title:
  194. title = self.db.sql('select title from help where path like %s',
  195. fpath + '%')
  196. if title:
  197. title = title[0][0]
  198. links_html += "<li><a data-path='{fpath}'> {title} </a></li>".format(
  199. fpath=fpath, title=title)
  200. # else:
  201. # bad entries in .txt files
  202. # print fpath
  203. links_html += "</ol>"
  204. html = content.replace('{index}', links_html)
  205. self.db.sql('update help set content=%s where path=%s', (html, original_path))
  206. def get_out_path(self, path):
  207. return '/' + os.path.relpath(path, self.out_base_path)
  208. def get_parent(self, child_path):
  209. if 'index' in child_path:
  210. child_path = child_path[: child_path.rindex('index')]
  211. if child_path[-1] == '/':
  212. child_path = child_path[:-1]
  213. child_path = child_path[: child_path.rindex('/')]
  214. out = None
  215. if child_path:
  216. parent_path = child_path + "/index"
  217. out = self.get_content(parent_path)
  218. #if parent is documentation root
  219. else:
  220. parent_path = "/documentation/index"
  221. out = {}
  222. out['title'] = "Documentation"
  223. if not out:
  224. return None
  225. out['path'] = parent_path
  226. return out
  227. def reorder_files(self, files):
  228. pos = 0
  229. if 'index.md' in files:
  230. pos = files.index('index.md')
  231. elif 'index.html' in files:
  232. pos = files.index('index.html')
  233. if pos:
  234. files[0], files[pos] = files[pos], files[0]
  235. return files