diff --git a/frappe/website/doctype/web_page/test_web_page.py b/frappe/website/doctype/web_page/test_web_page.py index b1600a338f..a481337978 100644 --- a/frappe/website/doctype/web_page/test_web_page.py +++ b/frappe/website/doctype/web_page/test_web_page.py @@ -53,4 +53,24 @@ class TestWebPage(unittest.TestCase): web_page.save() self.assertTrue('html content' in get_page_content('/test-content-type')) + web_page.delete() + + def test_dynamic_route(self): + web_page = frappe.get_doc(dict( + doctype = 'Web Page', + title = 'Test Dynamic Route', + published = 1, + dynamic_route = 1, + route = '/doctype-view/', + content_type = 'HTML', + dymamic_template = 1, + main_section_html = '
{{ frappe.form_dict.doctype }}
' + )).insert() + + try: + content = get_page_content('/doctype-view/DocField') + self.assertTrue('
DocField
' in content) + finally: + web_page.delete() + diff --git a/frappe/website/doctype/web_page/web_page.json b/frappe/website/doctype/web_page/web_page.json index 095be791d2..10546881dd 100644 --- a/frappe/website/doctype/web_page/web_page.json +++ b/frappe/website/doctype/web_page/web_page.json @@ -11,6 +11,7 @@ "section_title", "title", "route", + "dynamic_route", "slideshow", "cb1", "published", @@ -25,8 +26,9 @@ "main_section_md", "main_section_html", "page_blocks", + "context_section", + "context_script", "custom_javascript", - "insert_code", "javascript", "custom_css", "insert_style", @@ -66,6 +68,7 @@ { "fieldname": "route", "fieldtype": "Data", + "ignore_xss_filter": 1, "in_list_view": 1, "in_standard_filter": 1, "label": "Route", @@ -141,20 +144,12 @@ }, { "collapsible": 1, - "collapsible_depends_on": "insert_code", + "collapsible_depends_on": "javascript", "fieldname": "custom_javascript", "fieldtype": "Section Break", "label": "Script" }, { - "default": "0", - "description": "Add code as <script>", - "fieldname": "insert_code", - "fieldtype": "Check", - "label": "Insert Code" - }, - { - "depends_on": "insert_code", "fieldname": "javascript", "fieldtype": "Code", "label": "Javascript", @@ -289,6 +284,27 @@ "fieldname": "meta_image", "fieldtype": "Attach Image", "label": "Image" + }, + { + "default": "0", + "description": "Map route parameters into form variables. Example /project/<name>", + "fieldname": "dynamic_route", + "fieldtype": "Check", + "label": "Dynamic Route" + }, + { + "collapsible": 1, + "collapsible_depends_on": "context_script", + "fieldname": "context_section", + "fieldtype": "Section Break", + "label": "Context" + }, + { + "description": "

Set context before rendering a template. Example:

\n

\ncontext.project = frappe.get_doc(\"Project\", frappe.form_dict.name)\n
", + "fieldname": "context_script", + "fieldtype": "Code", + "label": "Context Script", + "options": "Python" } ], "has_web_view": 1, @@ -298,7 +314,7 @@ "is_published_field": "published", "links": [], "max_attachments": 20, - "modified": "2020-09-11 16:15:17.065665", + "modified": "2020-09-21 16:32:53.568573", "modified_by": "Administrator", "module": "Website", "name": "Web Page", diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py index f3e3a5960c..e13231c65a 100644 --- a/frappe/website/doctype/web_page/web_page.py +++ b/frappe/website/doctype/web_page/web_page.py @@ -11,19 +11,22 @@ from jinja2.exceptions import TemplateSyntaxError import frappe from frappe import _ -from frappe.utils import get_datetime, now, strip_html +from frappe.utils import get_datetime, now, strip_html, quoted from frappe.utils.jinja import render_template from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow from frappe.website.router import resolve_route from frappe.website.utils import (extract_title, find_first_image, get_comment_list, get_html_content_based_on_type) from frappe.website.website_generator import WebsiteGenerator +from frappe.utils.safe_exec import safe_exec class WebPage(WebsiteGenerator): def validate(self): self.validate_dates() self.set_route() + if not self.dynamic_route: + self.route = quoted(self.route) def get_feed(self): return self.title @@ -37,6 +40,12 @@ class WebPage(WebsiteGenerator): def get_context(self, context): context.main_section = get_html_content_based_on_type(self, 'main_section', self.content_type) context.source_content_type = self.content_type + + if self.context_script: + _locals = dict(context = frappe._dict()) + safe_exec(self.context_script, None, _locals) + context.update(_locals['context']) + self.render_dynamic(context) # if static page, get static content @@ -46,6 +55,7 @@ class WebPage(WebsiteGenerator): if self.enable_comments: context.comment_list = get_comment_list(self.doctype, self.name) + context.update({ "style": self.css or "", "script": self.javascript or "", diff --git a/frappe/website/render.py b/frappe/website/render.py index 04876786e1..af3b18b233 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -13,14 +13,14 @@ import six from bs4 import BeautifulSoup from six import iteritems from werkzeug.wrappers import Response -from werkzeug.routing import Map, Rule, NotFound +from werkzeug.routing import Rule from werkzeug.wsgi import wrap_file from frappe.website.context import get_context from frappe.website.redirect import resolve_redirect from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, get_toc, get_next_link) -from frappe.website.router import clear_sitemap +from frappe.website.router import clear_sitemap, evaluate_dynamic_routes from frappe.translate import guess_language class PageNotFoundError(Exception): pass @@ -255,23 +255,11 @@ def resolve_path(path): return path def resolve_from_map(path): - m = Map([Rule(r["from_route"], endpoint=r["to_route"], defaults=r.get("defaults")) - for r in get_website_rules()]) + '''transform dynamic route to a static one from hooks and route defined in doctype''' + rules = [Rule(r["from_route"], endpoint=r["to_route"], defaults=r.get("defaults")) + for r in get_website_rules()] - if frappe.local.request: - urls = m.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 path + return evaluate_dynamic_routes(rules, path) or path def get_website_rules(): '''Get website route rules from hooks and DocType route''' diff --git a/frappe/website/router.py b/frappe/website/router.py index 263d5b0f07..2aa9cebe30 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -12,7 +12,7 @@ import yaml import frappe from frappe.model.document import get_controller from frappe.website.utils import can_cache, delete_page_cache, extract_comment_tag, extract_title - +from werkzeug.routing import Map, Rule, NotFound def resolve_route(path): """Returns the page route object based on searching in pages and generators. @@ -80,6 +80,9 @@ def get_page_info_from_template(path): def get_page_context_from_doctype(path): page_info = get_page_info_from_doctypes(path) + if not page_info: + page_info = get_page_info_from_web_page_with_dynamic_routes(path) + if page_info: return frappe.get_doc(page_info.get("doctype"), page_info.get("name")).get_page_info() @@ -88,7 +91,9 @@ def clear_sitemap(): delete_page_cache("*") def get_all_page_context_from_doctypes(): - '''Get all doctype generated routes (for sitemap.xml)''' + ''' + Get all doctype generated routes (for sitemap.xml) + ''' routes = frappe.cache().get_value("website_generator_routes") if not routes: routes = get_page_info_from_doctypes() @@ -97,10 +102,12 @@ def get_all_page_context_from_doctypes(): return routes def get_page_info_from_doctypes(path=None): + ''' + Find a document with matching `route` from all doctypes with `has_web_view`=1 + ''' routes = {} for doctype in get_doctypes_with_web_view(): - condition = "" - values = [] + filters = {} controller = get_controller(doctype) meta = frappe.get_meta(doctype) @@ -109,15 +116,15 @@ def get_page_info_from_doctypes(path=None): (controller.website.condition_field if not meta.custom else None)) if condition_field: - condition ="where {0}=1".format(condition_field) + filters[condition_field] = 1 if path: - condition += ' {0} `route`=%s limit 1'.format('and' if 'where' in condition else 'where') - values.append(path) + filters['route'] = path try: - for r in frappe.db.sql("""select route, name, modified from `tab{0}` - {1}""".format(doctype, condition), values=values, as_dict=True): + for r in frappe.get_all(doctype, fields = ['name', 'route', 'modified'], + filters = filters, limit = 1): + routes[r.route] = {"doctype": doctype, "name": r.name, "modified": r.modified} # just want one path, return it! @@ -128,6 +135,46 @@ def get_page_info_from_doctypes(path=None): return routes +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/ + https://werkzeug.palletsprojects.com/en/1.0.x/routing/ + ''' + route_map = Map(rules) + endpoint = None + + if frappe.local.request: + 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'''