The license.txt file has been replaced with LICENSE for quite a while now. INAL but it didn't seem accurate to say "hey, checkout license.txt although there's no such file". Apart from this, there were inconsistencies in the headers altogether...this change brings consistency.
279 lines
8.2 KiB
Python
279 lines
8.2 KiB
Python
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: MIT. See LICENSE
|
|
|
|
import io
|
|
import os
|
|
import re
|
|
|
|
import frappe
|
|
from frappe.website.utils import extract_title
|
|
from werkzeug.routing import Map, Rule, NotFound
|
|
|
|
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
|
|
'''
|
|
rules, page_info = [], {}
|
|
|
|
# build rules from all web page with `dynamic_route = 1`
|
|
for d in frappe.get_all('Web Page', fields = ['name', 'route', 'modified'],
|
|
filters = dict(published = 1, dynamic_route=1)):
|
|
rules.append(Rule('/' + d.route, endpoint = d.name))
|
|
d.doctype = 'Web Page'
|
|
page_info[d.name] = d
|
|
|
|
end_point = evaluate_dynamic_routes(rules, path)
|
|
if end_point:
|
|
return page_info[end_point]
|
|
|
|
def evaluate_dynamic_routes(rules, path):
|
|
'''
|
|
Use Werkzeug routing to evaluate dynamic routes like /project/<name>
|
|
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)
|
|
path = endpoint
|
|
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.local.flags.web_pages_apps or 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):
|
|
# add missing __init__.py
|
|
if not '__init__.py' in files:
|
|
open(os.path.join(basepath, '__init__.py'), 'a').close()
|
|
|
|
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
|
|
# print frappe.as_json(pages[-1])
|
|
|
|
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 get_frontmatter(string):
|
|
"""
|
|
Reference: https://github.com/jonbeebe/frontmatter
|
|
"""
|
|
import yaml
|
|
|
|
fmatter = ""
|
|
body = ""
|
|
result = re.compile(r'^\s*(?:---|\+\+\+)(.*?)(?:---|\+\+\+)\s*(.+)$', re.S | re.M).search(string)
|
|
|
|
if result:
|
|
fmatter = result.group(1)
|
|
body = result.group(2)
|
|
|
|
return {
|
|
"attributes": yaml.safe_load(fmatter),
|
|
"body": body,
|
|
}
|
|
|
|
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 = '<div class="from-markdown">' + source + '</div>'
|
|
|
|
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 io.open(js_path, 'r', 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 io.open(css_path, 'r', 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):
|
|
'''
|
|
Returns 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]
|
|
base_template = templates[-1]
|
|
return base_template
|
|
|
|
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, 'r') 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')
|