# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import os import re from werkzeug.exceptions import NotFound from werkzeug.routing import Map, Rule import frappe from frappe.website.utils import extract_title, get_frontmatter def get_page_info_from_web_page_with_dynamic_routes(path): """ Query Web Page with dynamic_route = 1 and evaluate if any of the routes match """ from frappe.website.doctype.web_page.web_page import get_dynamic_web_pages rules, page_info = [], {} for d in get_dynamic_web_pages(): rules.append(Rule(f"/{d.route}", endpoint=d.name)) d.doctype = d.doctype or "Web Page" page_info[d.name] = d end_point = evaluate_dynamic_routes(rules, path) if end_point: return page_info[end_point] def get_page_info_from_web_form(path): """Query published web forms and evaluate if the route matches""" from frappe.website.doctype.web_form.web_form import get_published_web_forms for d in get_published_web_forms(): if not (path.startswith(f"{d.route}") or path.startswith(f"/{d.route}")): continue rules = [] rules.append(Rule(f"/{d.route}", endpoint=d.name)) rules.append(Rule(f"/{d.route}/list", endpoint=d.name)) rules.append(Rule(f"/{d.route}/new", endpoint=d.name)) rules.append(Rule(f"/{d.route}/", endpoint=d.name)) rules.append(Rule(f"/{d.route}//edit", endpoint=d.name)) d.doctype = "Web Form" end_point = evaluate_dynamic_routes(rules, path) if end_point: if path.endswith("/list"): frappe.form_dict.is_list = True elif path.endswith("/new"): frappe.form_dict.is_new = True elif path.endswith("/edit"): frappe.form_dict.is_edit = True else: frappe.form_dict.is_read = True return d def evaluate_dynamic_routes(rules, path): """ Use Werkzeug routing to evaluate dynamic routes like /project/ https://werkzeug.palletsprojects.com/en/1.0.x/routing/ """ route_map = Map(rules) endpoint = None if hasattr(frappe.local, "request") and frappe.local.request.environ: urls = route_map.bind_to_environ(frappe.local.request.environ) try: endpoint, args = urls.match("/" + path) if args: # don't cache when there's a query string! frappe.local.no_cache = 1 frappe.local.form_dict.update(args) except NotFound: pass return endpoint def get_pages(app=None): """Get all pages. Called for docs / sitemap""" def _build(app): pages = {} if app: apps = [app] else: apps = frappe.get_installed_apps() for app in apps: app_path = frappe.get_app_path(app) for start in get_start_folders(): pages.update(get_pages_from_path(start, app, app_path)) return pages return frappe.cache.get_value("website_pages", lambda: _build(app)) def get_pages_from_path(start, app, app_path): pages = {} start_path = os.path.join(app_path, start) if os.path.exists(start_path): for basepath, folders, files in os.walk(start_path): # noqa: B007 for fname in files: fname = frappe.utils.cstr(fname) if "." not in fname: continue page_name, extn = fname.rsplit(".", 1) if extn in ("js", "css") and os.path.exists(os.path.join(basepath, page_name + ".html")): # js, css is linked to html, skip continue if extn in ("html", "xml", "js", "css", "md"): page_info = get_page_info( os.path.join(basepath, fname), app, start, basepath, app_path, fname ) pages[page_info.route] = page_info return pages def get_page_info(path, app, start, basepath=None, app_path=None, fname=None): """Load page info""" if fname is None: fname = os.path.basename(path) if app_path is None: app_path = frappe.get_app_path(app) if basepath is None: basepath = os.path.dirname(path) page_name, extn = os.path.splitext(fname) # add website route page_info = frappe._dict() page_info.basename = page_name if extn in ("html", "md") else fname page_info.basepath = basepath page_info.page_or_generator = "Page" page_info.template = os.path.relpath(os.path.join(basepath, fname), app_path) if page_info.basename == "index": page_info.basename = "" # get route from template name page_info.route = page_info.template.replace(start, "").strip("/") if os.path.basename(page_info.route) in ("index.html", "index.md"): page_info.route = os.path.dirname(page_info.route) # remove the extension if page_info.route.endswith(".md") or page_info.route.endswith(".html"): page_info.route = page_info.route.rsplit(".", 1)[0] page_info.name = page_info.page_name = page_info.route # controller page_info.controller_path = os.path.join(basepath, page_name.replace("-", "_") + ".py") if os.path.exists(page_info.controller_path): controller = ( app + "." + os.path.relpath(page_info.controller_path, app_path).replace(os.path.sep, ".")[:-3] ) page_info.controller = controller # get the source setup_source(page_info) if not page_info.title: page_info.title = extract_title(page_info.source, page_info.route) # extract properties from controller attributes load_properties_from_controller(page_info) return page_info def setup_source(page_info): """Get the HTML source of the template""" jenv = frappe.get_jenv() source = jenv.loader.get_source(jenv, page_info.template)[0] html = "" if page_info.template.endswith((".md", ".html")): # extract frontmatter block if exists try: # values will be used to update page_info res = get_frontmatter(source) if res["attributes"]: page_info.update(res["attributes"]) source = res["body"] except Exception: pass if page_info.template.endswith(".md"): source = frappe.utils.md_to_html(source) page_info.page_toc_html = source.toc_html if not page_info.show_sidebar: source = '
' + source + "
" if not page_info.base_template: page_info.base_template = get_base_template(page_info.route) if ( page_info.template.endswith( ( ".html", ".md", ) ) and "{%- extends" not in source and "{% extends" not in source ): # set the source only if it contains raw content html = source # load css/js files js_path = os.path.join(page_info.basepath, (page_info.basename or "index") + ".js") if os.path.exists(js_path) and "{% block script %}" not in html: with open(js_path, encoding="utf-8") as f: js = f.read() page_info.colocated_js = js css_path = os.path.join(page_info.basepath, (page_info.basename or "index") + ".css") if os.path.exists(css_path) and "{% block style %}" not in html: with open(css_path, encoding="utf-8") as f: css = f.read() page_info.colocated_css = css if html: page_info.source = html page_info.base_template = page_info.base_template or "templates/web.html" else: page_info.source = "" # show table of contents setup_index(page_info) def get_base_template(path=None): """Return the `base_template` for given `path`. The default `base_template` for any web route is `templates/web.html` defined in `hooks.py`. This can be overridden for certain routes in `custom_app/hooks.py` based on regex pattern. """ if not path: path = frappe.local.request.path base_template_map = frappe.get_hooks("base_template_map") or {} patterns = list(base_template_map.keys()) patterns_desc = sorted(patterns, key=lambda x: len(x), reverse=True) for pattern in patterns_desc: if re.match(pattern, path): templates = base_template_map[pattern] return templates[-1] def setup_index(page_info): """Build page sequence from index.txt""" if page_info.basename == "": # load index.txt if loading all pages index_txt_path = os.path.join(page_info.basepath, "index.txt") if os.path.exists(index_txt_path): with open(index_txt_path) as f: page_info.index = f.read().splitlines() def load_properties_from_controller(page_info): if not page_info.controller: return module = frappe.get_module(page_info.controller) if not module: return for prop in ("base_template_path", "template", "no_cache", "sitemap", "condition_field"): if hasattr(module, prop): page_info[prop] = getattr(module, prop) def get_doctypes_with_web_view(): """Return doctypes with Has Web View or set via hooks""" def _get(): installed_apps = frappe.get_installed_apps() doctypes = frappe.get_hooks("website_generators") doctypes_with_web_view = frappe.get_all( "DocType", fields=["name", "module"], filters=dict(has_web_view=1) ) module_app_map = frappe.local.module_app doctypes += [ d.name for d in doctypes_with_web_view if module_app_map.get(frappe.scrub(d.module)) in installed_apps ] return doctypes return frappe.cache.get_value("doctypes_with_web_view", _get) def get_start_folders(): return frappe.local.flags.web_pages_folders or ("www", "templates/pages") def clear_routing_cache(): from frappe.website.doctype.web_form.web_form import get_published_web_forms from frappe.website.doctype.web_page.web_page import get_dynamic_web_pages from frappe.website.page_renderers.document_page import _find_matching_document_webview from frappe.www.sitemap import get_public_pages_from_doctypes _find_matching_document_webview.clear_cache() get_dynamic_web_pages.clear_cache() get_published_web_forms.clear_cache() get_public_pages_from_doctypes.clear_cache() frappe.cache.delete_value("home_page")