feat(web_page): add dynamic routes to web page like /project/<project>
This commit is contained in:
parent
df6c52cac1
commit
5006ec8f51
5 changed files with 120 additions and 39 deletions
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <code>/project/<name></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",
|
||||
|
|
|
|||
|
|
@ -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 "",
|
||||
|
|
|
|||
|
|
@ -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'''
|
||||
|
|
|
|||
|
|
@ -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'''
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue