import os from importlib.machinery import all_suffixes import click import frappe from frappe.website.page_renderers.base_template_page import BaseTemplatePage from frappe.website.router import get_base_template, get_page_info from frappe.website.utils import ( cache_html, extract_comment_tag, extract_title, get_frontmatter, get_next_link, get_sidebar_items, get_toc, is_binary_file, ) PY_LOADER_SUFFIXES = tuple(all_suffixes()) WEBPAGE_PY_MODULE_PROPERTIES = ( "base_template_path", "template", "no_cache", "sitemap", "condition_field", ) COMMENT_PROPERTY_KEY_VALUE_MAP = { "no-breadcrumbs": ("no_breadcrumbs", 1), "show-sidebar": ("show_sidebar", 1), "add-breadcrumbs": ("add_breadcrumbs", 1), "no-header": ("no_header", 1), "add-next-prev-links": ("add_next_prev_links", 1), "no-cache": ("no_cache", 1), "no-sitemap": ("sitemap", 0), "sitemap": ("sitemap", 1), } class TemplatePage(BaseTemplatePage): def __init__(self, path, http_status_code=None): super().__init__(path=path, http_status_code=http_status_code) self.set_template_path() def set_template_path(self): """ Searches for file matching the path in the /www and /templates/pages folders and sets path if match is found """ folders = get_start_folders() for app in reversed(frappe.get_installed_apps()): app_path = frappe.get_app_path(app) for dirname in folders: search_path = os.path.join(app_path, dirname, self.path) for file_path in self.get_index_path_options(search_path): if os.path.isfile(file_path) and not is_binary_file(file_path): self.app = app self.app_path = app_path self.file_dir = dirname self.basename = os.path.splitext(file_path)[0] self.template_path = os.path.relpath(file_path, self.app_path) self.basepath = os.path.dirname(file_path) self.filename = os.path.basename(file_path) self.name = os.path.splitext(self.filename)[0] return def can_render(self): return ( hasattr(self, "template_path") and self.template_path and not self.template_path.endswith(PY_LOADER_SUFFIXES) ) @staticmethod def get_index_path_options(search_path): return ( frappe.as_unicode(f"{search_path}{d}") for d in ("", ".html", ".md", "/index.html", "/index.md") ) def render(self): html = self.get_html() html = self.add_csrf_token(html) return self.build_response(html) @cache_html def get_html(self): # context object should be separate from self for security # because it will be accessed via the user defined template self.init_context() self.set_pymodule() self.update_context() self.setup_template_source() self.load_colocated_files() self.set_properties_from_source() self.post_process_context() html = self.render_template() html = self.update_toc(html) return html def post_process_context(self): self.set_user_info() self.add_sidebar_and_breadcrumbs() super().post_process_context() def add_sidebar_and_breadcrumbs(self): if not self.context.sidebar_items: self.context.sidebar_items = get_sidebar_items(self.context.website_sidebar, self.basepath) if self.context.add_breadcrumbs and not self.context.parents: parent_path = os.path.dirname(self.path) if self.path.endswith("index"): # in case of index page move one directory up for parent path parent_path = os.path.dirname(parent_path) for parent_file_path in self.get_index_path_options(parent_path): parent_file_path = os.path.join(self.app_path, self.file_dir, parent_file_path) if os.path.isfile(parent_file_path): parent_page_context = get_page_info(parent_file_path, self.app, self.file_dir) if parent_page_context: self.context.parents = [ dict(route=os.path.dirname(self.path), title=parent_page_context.title) ] break def set_pymodule(self): """ A template may have a python module with a `get_context` method along with it in the same folder. Also the hyphens will be coverted to underscore for python module names. This method sets the pymodule_name if it exists. """ template_basepath = os.path.splitext(self.template_path)[0] self.pymodule_name = None # replace - with _ in the internal modules names self.pymodule_path = os.path.join( os.path.dirname(template_basepath), os.path.basename(template_basepath.replace("-", "_")) + ".py", ) if os.path.exists(os.path.join(self.app_path, self.pymodule_path)): self.pymodule_name = self.app + "." + self.pymodule_path.replace(os.path.sep, ".")[:-3] def setup_template_source(self): """Setup template source, frontmatter and markdown conversion""" self.source = self.get_raw_template() self.extract_frontmatter() self.convert_from_markdown() def update_context(self): self.set_page_properties() self.context.build_version = frappe.utils.get_build_version() if self.pymodule_name: self.pymodule = frappe.get_module(self.pymodule_name) self.set_pymodule_properties() data = self.run_pymodule_method("get_context") # some methods may return a "context" object if data: self.context.update(data) # TODO: self.context.children = self.run_pymodule_method('get_children') self.context.developer_mode = frappe.conf.developer_mode if self.context.http_status_code: self.http_status_code = self.context.http_status_code def set_pymodule_properties(self): for prop in WEBPAGE_PY_MODULE_PROPERTIES: if hasattr(self.pymodule, prop): self.context[prop] = getattr(self.pymodule, prop) def set_page_properties(self): self.context.base_template = self.context.base_template or get_base_template(self.path) self.context.basepath = self.basepath self.context.basename = self.basename self.context.name = self.name self.context.path = self.path self.context.route = self.path self.context.template = self.template_path def set_properties_from_source(self): if not self.source: return context = self.context if not context.title: context.title = extract_title(self.source, self.path) base_template = extract_comment_tag(self.source, "base_template") if base_template: context.base_template = base_template if ( context.base_template and "{%- extends" not in self.source and "{% extends" not in self.source and "" not in self.source ): self.source = f"""{{% extends "{context.base_template}" %}} {{% block page_content %}}{self.source}{{% endblock %}}""" self.set_properties_via_comments() def set_properties_via_comments(self): for comment, (context_key, value) in COMMENT_PROPERTY_KEY_VALUE_MAP.items(): comment_tag = f"" if comment_tag in self.source: self.context[context_key] = value click.echo(f"\n⚠️ DEPRECATION WARNING: {comment_tag} will be deprecated on 2021-12-31.") click.echo(f"Please remove it from {self.template_path} in {self.app}") def run_pymodule_method(self, method_name): if hasattr(self.pymodule, method_name): import inspect method = getattr(self.pymodule, method_name) if inspect.getfullargspec(method).args: return method(self.context) else: return method() def render_template(self): if self.template_path.endswith("min.js"): html = self.source # static else: if self.context.safe_render is not None: safe_render = self.context.safe_render else: safe_render = True html = frappe.render_template(self.source, self.context, safe_render=safe_render) return html def extends_template(self): return self.template_path.endswith((".html", ".md")) and ( "{%- extends" in self.source or "{% extends" in self.source ) def get_raw_template(self): return frappe.get_jloader().get_source(frappe.get_jenv(), self.context.template)[0] def load_colocated_files(self): """load co-located css/js files with the same name""" js_path = self.basename + ".js" if os.path.exists(js_path) and "{% block script %}" not in self.source: self.context.colocated_js = self.get_colocated_file(js_path) css_path = self.basename + ".css" if os.path.exists(css_path) and "{% block style %}" not in self.source: self.context.colocated_css = self.get_colocated_file(css_path) def get_colocated_file(self, path): with open(path, encoding="utf-8") as f: return f.read() def extract_frontmatter(self): if not self.template_path.endswith((".md", ".html")): return try: # values will be used to update self res = get_frontmatter(self.source) if res["attributes"]: self.context.update(res["attributes"]) self.source = res["body"] except Exception: pass def convert_from_markdown(self): if self.template_path.endswith(".md"): self.source = frappe.utils.md_to_html(self.source) self.context.page_toc_html = self.source.toc_html if not self.context.show_sidebar: self.source = '
' + self.source + "
" def update_toc(self, html): if "{index}" in html: html = html.replace("{index}", get_toc(self.path)) if "{next}" in html: html = html.replace("{next}", get_next_link(self.path)) return html def set_standard_path(self, path): self.app = "frappe" self.app_path = frappe.get_app_path("frappe") self.path = path self.template_path = f"www/{path}.html" def set_missing_values(self): super().set_missing_values() # for backward compatibility self.context.docs_base_url = "/docs" def set_user_info(self): from frappe.utils.user import get_fullname_and_avatar info = get_fullname_and_avatar(frappe.session.user) self.context["fullname"] = info.fullname self.context["user_image"] = info.avatar self.context["user"] = info.name def get_start_folders(): return frappe.local.flags.web_pages_folders or ("www", "templates/pages")