feat(web_page): add dynamic routes to web page like /project/<project>

This commit is contained in:
Rushabh Mehta 2020-09-21 16:51:06 +05:30
parent df6c52cac1
commit 5006ec8f51
5 changed files with 120 additions and 39 deletions

View file

@ -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/<doctype>',
content_type = 'HTML',
dymamic_template = 1,
main_section_html = '<div>{{ frappe.form_dict.doctype }}</div>'
)).insert()
try:
content = get_page_content('/doctype-view/DocField')
self.assertTrue('<div>DocField</div>' in content)
finally:
web_page.delete()

View file

@ -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 &lt;script&gt;",
"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 <code>/project/&lt;name&gt;</code>",
"fieldname": "dynamic_route",
"fieldtype": "Check",
"label": "Dynamic Route"
},
{
"collapsible": 1,
"collapsible_depends_on": "context_script",
"fieldname": "context_section",
"fieldtype": "Section Break",
"label": "Context"
},
{
"description": "<p>Set context before rendering a template. Example:</p><p>\n</p><div><pre><code>\ncontext.project = frappe.get_doc(\"Project\", frappe.form_dict.name)\n</code></pre></div>",
"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",

View file

@ -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 "",

View file

@ -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'''

View file

@ -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/<name>
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'''