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.
 
 
 
 
 
 

329 lines
10 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. import frappe, os
  5. from frappe.website.utils import can_cache, delete_page_cache, extract_title
  6. from frappe.model.document import get_controller
  7. from frappe import _
  8. def resolve_route(path):
  9. """Returns the page route object based on searching in pages and generators.
  10. The `www` folder is also a part of generator **Web Page**.
  11. The only exceptions are `/about` and `/contact` these will be searched in Web Pages
  12. first before checking the standard pages."""
  13. if path not in ("about", "contact"):
  14. context = get_page_context_from_template(path)
  15. if context:
  16. return context
  17. return get_page_context_from_doctype(path)
  18. else:
  19. context = get_page_context_from_doctype(path)
  20. if context:
  21. return context
  22. return get_page_context_from_template(path)
  23. def get_page_context(path):
  24. page_context = None
  25. if can_cache():
  26. page_context_cache = frappe.cache().hget("page_context", path) or {}
  27. page_context = page_context_cache.get(frappe.local.lang, None)
  28. if not page_context:
  29. page_context = make_page_context(path)
  30. if can_cache(page_context.no_cache):
  31. page_context_cache[frappe.local.lang] = page_context
  32. frappe.cache().hset("page_context", path, page_context_cache)
  33. return page_context
  34. def make_page_context(path):
  35. context = resolve_route(path)
  36. if not context:
  37. raise frappe.DoesNotExistError
  38. context.doctype = context.ref_doctype
  39. if context.page_title:
  40. context.title = context.page_title
  41. context.pathname = frappe.local.path
  42. return context
  43. def get_page_context_from_template(path):
  44. '''Return page_info from path'''
  45. for app in frappe.get_installed_apps(frappe_last=True):
  46. app_path = frappe.get_app_path(app)
  47. folders = frappe.local.flags.web_pages_folders or ('www', 'templates/pages')
  48. for start in folders:
  49. search_path = os.path.join(app_path, start, path)
  50. options = (search_path, search_path + '.html', search_path + '.md',
  51. search_path + '/index.html', search_path + '/index.md')
  52. for o in options:
  53. if os.path.exists(o) and not os.path.isdir(o):
  54. return get_page_info(o, app, app_path=app_path)
  55. return None
  56. def get_page_context_from_doctype(path):
  57. page_info = get_page_info_from_doctypes(path)
  58. if page_info:
  59. return frappe.get_doc(page_info.get("doctype"), page_info.get("name")).get_page_info()
  60. def clear_sitemap():
  61. delete_page_cache("*")
  62. def get_all_page_context_from_doctypes():
  63. '''Get all doctype generated routes (for sitemap.xml)'''
  64. routes = frappe.cache().get_value("website_generator_routes")
  65. if not routes:
  66. routes = get_page_info_from_doctypes()
  67. frappe.cache().set_value("website_generator_routes", routes)
  68. return routes
  69. def get_page_info_from_doctypes(path=None):
  70. routes = {}
  71. for doctype in get_doctypes_with_web_view():
  72. condition = ""
  73. values = []
  74. controller = get_controller(doctype)
  75. meta = frappe.get_meta(doctype)
  76. condition_field = meta.is_published_field or controller.website.condition_field
  77. if condition_field:
  78. condition ="where {0}=1".format(condition_field)
  79. if path:
  80. condition += ' {0} `route`=%s limit 1'.format('and' if 'where' in condition else 'where')
  81. values.append(path)
  82. try:
  83. for r in frappe.db.sql("""select route, name, modified from `tab{0}`
  84. {1}""".format(doctype, condition), values=values, as_dict=True):
  85. routes[r.route] = {"doctype": doctype, "name": r.name, "modified": r.modified}
  86. # just want one path, return it!
  87. if path:
  88. return routes[r.route]
  89. except Exception, e:
  90. if e.args[0]!=1054: raise e
  91. return routes
  92. def get_pages(app=None):
  93. '''Get all pages. Called for docs / sitemap'''
  94. pages = {}
  95. frappe.local.flags.in_get_all_pages = True
  96. folders = frappe.local.flags.web_pages_folders or ('www', 'templates/pages')
  97. if app:
  98. apps = [app]
  99. else:
  100. apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps()
  101. for app in apps:
  102. app_path = frappe.get_app_path(app)
  103. for start in folders:
  104. path = os.path.join(app_path, start)
  105. pages.update(get_pages_from_path(path, app, app_path))
  106. frappe.local.flags.in_get_all_pages = False
  107. return pages
  108. def get_pages_from_path(path, app, app_path):
  109. pages = {}
  110. if os.path.exists(path):
  111. for basepath, folders, files in os.walk(path):
  112. # add missing __init__.py
  113. if not '__init__.py' in files:
  114. open(os.path.join(basepath, '__init__.py'), 'a').close()
  115. for fname in files:
  116. fname = frappe.utils.cstr(fname)
  117. page_name, extn = fname.rsplit(".", 1)
  118. if extn in ('js', 'css') and os.path.exists(os.path.join(basepath, fname + '.html')):
  119. # js, css is linked to html, skip
  120. continue
  121. if extn in ("html", "xml", "js", "css", "md"):
  122. page_info = get_page_info(path, app, basepath, app_path, fname)
  123. pages[page_info.route] = page_info
  124. # print frappe.as_json(pages[-1])
  125. return pages
  126. def get_page_info(path, app, basepath=None, app_path=None, fname=None):
  127. '''Load page info'''
  128. if not fname:
  129. fname = os.path.basename(path)
  130. if not app_path:
  131. app_path = frappe.get_app_path(app)
  132. if not basepath:
  133. basepath = os.path.dirname(path)
  134. page_name, extn = fname.rsplit(".", 1)
  135. # add website route
  136. page_info = frappe._dict()
  137. page_info.basename = page_name if extn in ('html', 'md') else fname
  138. page_info.basepath = basepath
  139. page_info.page_or_generator = "Page"
  140. page_info.template = os.path.relpath(os.path.join(basepath, fname), app_path)
  141. if page_info.basename == 'index':
  142. page_info.basename = ""
  143. page_info.route = page_info.name = page_info.page_name = os.path.join(os.path.relpath(basepath, path),
  144. page_info.basename).strip('/.')
  145. # controller
  146. page_info.controller_path = os.path.join(basepath, page_name.replace("-", "_") + ".py")
  147. if os.path.exists(page_info.controller_path):
  148. controller = app + "." + os.path.relpath(page_info.controller_path,
  149. app_path).replace(os.path.sep, ".")[:-3]
  150. page_info.controller = controller
  151. # get the source
  152. setup_source(page_info)
  153. if page_info.only_content:
  154. # extract properties from HTML comments
  155. load_properties(page_info)
  156. return page_info
  157. def setup_source(page_info):
  158. '''Get the HTML source of the template'''
  159. from markdown2 import markdown
  160. jenv = frappe.get_jenv()
  161. source = jenv.loader.get_source(jenv, page_info.template)[0]
  162. html = ''
  163. if page_info.template.endswith('.md'):
  164. source = markdown(source)
  165. # if only content
  166. if page_info.template.endswith('.html') or page_info.template.endswith('.md'):
  167. if ('</body>' not in source) and ('{% block' not in source):
  168. page_info.only_content = True
  169. html = '{% extends "templates/web.html" %}'
  170. html += '\n{% block page_content %}\n' + source + '\n{% endblock %}'
  171. else:
  172. html = source
  173. # load css/js files
  174. js, css = '', ''
  175. js_path = os.path.join(page_info.basepath, (page_info.basename or 'index') + '.js')
  176. if os.path.exists(js_path):
  177. if not '{% block script %}' in html:
  178. js = unicode(open(js_path, 'r').read(), 'utf-8')
  179. html += '\n{% block script %}<script>' + js + '\n</script>\n{% endblock %}'
  180. css_path = os.path.join(page_info.basepath, (page_info.basename or 'index') + '.css')
  181. if os.path.exists(css_path):
  182. if not '{% block style %}' in html:
  183. css = unicode(open(css_path, 'r').read(), 'utf-8')
  184. html += '\n{% block style %}\n<style>\n' + css + '\n</style>\n{% endblock %}'
  185. page_info.source = html
  186. # show table of contents
  187. setup_index(page_info)
  188. def setup_index(page_info):
  189. '''Build page sequence from index.txt'''
  190. if page_info.basename=='':
  191. # load index.txt if loading all pages
  192. index_txt_path = os.path.join(page_info.basepath, 'index.txt')
  193. if os.path.exists(index_txt_path):
  194. page_info.index = open(index_txt_path, 'r').read().splitlines()
  195. def make_toc(context, out, app=None):
  196. '''Insert full index (table of contents) for {index} tag'''
  197. from frappe.website.utils import get_full_index
  198. if '{index}' in out:
  199. html = frappe.get_template("templates/includes/full_index.html").render({
  200. "full_index": get_full_index(app=app),
  201. "url_prefix": context.url_prefix or "/",
  202. "route": context.route
  203. })
  204. out = out.replace('{index}', html)
  205. if '{next}' in out:
  206. # insert next link
  207. next_item = None
  208. children_map = get_full_index(app=app)
  209. parent_route = os.path.dirname(context.route)
  210. children = children_map[parent_route]
  211. if parent_route and children:
  212. for i, c in enumerate(children):
  213. if c.route == context.route and i < (len(children) - 1):
  214. next_item = children[i+1]
  215. next_item.url_prefix = context.url_prefix or "/"
  216. if next_item:
  217. if next_item.route and next_item.title:
  218. html = ('<p class="btn-next-wrapper">'+_("Next")\
  219. +': <a class="btn-next" href="{url_prefix}{route}">{title}</a></p>').format(**next_item)
  220. out = out.replace('{next}', html)
  221. return out
  222. def load_properties(page_info):
  223. '''Load properties like no_cache, title from raw'''
  224. if not page_info.title:
  225. page_info.title = extract_title(page_info.source, page_info.name)
  226. if page_info.title and not '{% block title %}' in page_info.source:
  227. page_info.source += '\n{% block title %}{{ title }}{% endblock %}'
  228. if "<!-- no-breadcrumbs -->" in page_info.source:
  229. page_info.no_breadcrumbs = 1
  230. if "<!-- show-sidebar -->" in page_info.source:
  231. page_info.show_sidebar = 1
  232. if "<!-- no-header -->" in page_info.source:
  233. page_info.no_header = 1
  234. else:
  235. # every page needs a header
  236. # add missing header if there is no <h1> tag
  237. if (not '{% block header %}' in page_info.source) and (not '<h1' in page_info.source):
  238. page_info.source += '\n{% block header %}<h1>{{ title }}</h1>{% endblock %}'
  239. if "<!-- no-cache -->" in page_info.source:
  240. page_info.no_cache = 1
  241. if "<!-- no-sitemap -->" in page_info.source:
  242. page_info.no_cache = 1
  243. def get_doctypes_with_web_view():
  244. '''Return doctypes with Has Web View or set via hooks'''
  245. def _get():
  246. installed_apps = frappe.get_installed_apps()
  247. doctypes = frappe.get_hooks("website_generators")
  248. doctypes += [d.name for d in frappe.get_all('DocType', 'name, module',
  249. dict(has_web_view=1)) if frappe.local.module_app[frappe.scrub(d.module)] in installed_apps]
  250. return doctypes
  251. return frappe.cache().get_value('doctypes_with_web_view', _get)