From 435bbe2665a447048058660e86deb6ed2005c635 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 5 Feb 2021 14:31:46 +0530 Subject: [PATCH 0001/1519] wip: refactor website routing and rendering --- frappe/tests/test_website_new.py | 20 ++ frappe/utils/boilerplate.py | 1 - frappe/website/serve.py | 276 ++++++++++++++++++++++++++ frappe/www/_test/static-file-test.png | Bin 0 -> 440 bytes 4 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 frappe/tests/test_website_new.py create mode 100644 frappe/website/serve.py create mode 100644 frappe/www/_test/static-file-test.png diff --git a/frappe/tests/test_website_new.py b/frappe/tests/test_website_new.py new file mode 100644 index 0000000000..aa9e77d5ba --- /dev/null +++ b/frappe/tests/test_website_new.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +import unittest + +import frappe +from frappe.website import serve +from frappe.website.utils import get_home_page +from frappe.utils import set_request + +class TestWebsite(unittest.TestCase): + def test_static_page(self): + set_request(method='GET', path='/_test/static-file-test.png') + response = serve.StaticPage().get() + self.assertEquals(response.status_code, 200) + + def test_error_page(self): + set_request(method='GET', path='/error') + response = serve.TemplatePage().get() + self.assertEquals(response.status_code, 200) + print(response.get_data()) diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 69d5726c34..e613c4d810 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -330,7 +330,6 @@ Configuration for docs """ # source_link = "https://github.com/[org_name]/{app_name}" -# docs_base_url = "https://[org_name].github.io/{app_name}" # headline = "App that does everything" # sub_heading = "Yes, you got that right the first time, everything" diff --git a/frappe/website/serve.py b/frappe/website/serve.py new file mode 100644 index 0000000000..3f3e1d4571 --- /dev/null +++ b/frappe/website/serve.py @@ -0,0 +1,276 @@ +import frappe +import os, mimetypes + +from werkzeug.wrappers import Response +from werkzeug.wsgi import wrap_file + +from frappe.website.render import (resolve_path, build_response) +from frappe.website.redirect import resolve_redirect +from frappe.website.router import get_start_folders +from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, + get_toc, get_next_link) +from frappe.website.doctype.website_settings.website_settings import get_website_settings + +def render(path=None, http_status_code=None): + """render html page""" + if not path: + path = frappe.local.request.path + + try: + path = path.strip('/ ') + resolve_redirect(path) + path = resolve_path(path) + data = None + + response = StaticPage(path).get() + if not response: + response = TemplatePage(path).get() + if not response: + response = DocTypePage(path).get() + if not response: + response = TemplatePage('404').get() + except frappe.PermissionError as e: + response = TemplatePage('403').get() + except: + response = TemplatePage('error').get() + + return response + +class WebPage(object): + def __init__(self, path=None): + self.headers = None + self.status_code = 200 + if not path: + path = frappe.local.request.path + self.path = path.strip('/ ') + + def get(self): + if self.validate(): + return self.render() + + def validate(self): + pass + + def render(self): + pass + +class StaticPage(WebPage): + def validate(self): + if ('.' not in self.path): + return False + extn = self.path.rsplit('.', 1)[-1] + if extn in ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json'): + return False + + if self.find_path_in_apps(): + return True + + return False + + def find_path_in_apps(self): + for app in frappe.get_installed_apps(): + file_path = frappe.get_app_path(app, 'www') + '/' + self.path + if os.path.exists(file_path): + self.file_path = file_path + return True + return False + + def render(self): + try: + f = open(self.file_path, 'rb') + except IOError: + raise NotFound + + response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True) + response.mimetype = mimetypes.guess_type(self.file_path)[0] or 'application/octet-stream' + return response + +class TemplatePage(WebPage): + def validate(self): + for app in frappe.get_installed_apps(frappe_last=True): + if self.find_page_in_app(app): + return True + + def find_page_in_app(self, app): + ''' + Searches for file matching the path in the /www and /templates/pages folders + ''' + app_path = frappe.get_app_path(app) + folders = get_start_folders() + + for dirname in folders: + search_path = os.path.join(app_path, dirname, self.path) + for p in self.get_index_path_options(search_path): + file_path = frappe.as_unicode(p) + if os.path.exists(file_path) and not os.path.isdir(file_path): + self.app = app + self.app_path = app_path + self.dirname = dirname + self.file_path = file_path + self.template_path = os.path.relpath(file_path, self.app_path) + return True + + def get_index_path_options(self, search_path): + return ( + search_path, + search_path + '.html', + search_path + '.md', + search_path + '/index.html', + search_path + '/index.md') + + def render(self): + return build_response(self.path, self.get_html(), self.status_code, self.headers) + + def get_html(self): + # context object should be separate from self for security + # because it will be accessed via the user defined template + self.context = frappe._dict() + + self.set_pymodule() + self.setup_template() + + if self.pymodule_name: + self.update_context() + + if self.source: + html = frappe.render_template(self.source, self.context) + + elif self.template_path: + html = self.render_template() + + html = self.update_toc(html) + + return html + + def set_pymodule(self): + ''' + A template may have a python module with a `get_context` method along with it in the + same folder. Also the hyphens will be coverted to underscore for python module names. + This method sets the pymodule_name if it exists. + ''' + self.basepath = self.template_path.rsplit('.', 1)[0] + self.pymodule_name = None + + # replace - with _ in the internal modules names + self.pymodule_path = os.path.join(self.basepath.replace("-", "_") + ".py") + + if os.path.exists(os.path.join(self.app_path, self.pymodule_path)): + self.pymodule_name = self.app + "." + self.pymodule_path.replace(os.path.sep, ".")[:-3] + + def setup_template(self): + '''Setup template source, frontmatter and markdown conversion''' + self.source = self.get_raw_template() + + if self.template_path.endswith(('.md', '.html')): + self.extract_frontmatter() + + self.convert_from_markdown() + + if self.extends_template(): + self.context.base_template_path = self.context.base_template_path or 'templates/base.html' + else: + self.source = None # clear the source + + # TODO: setup index.txt ? + + def update_context(self): + self.set_page_properties() + self.context.update(get_website_settings(self.context)) + self.context.update(frappe.local.conf.get("website_context") or {}) + + self.pymodule = frappe.get_module(self.pymodule_name) + + if self.pymodule: + self.set_pymodule_properties() + + data = self.run_pymodule_method('get_context') + # some methods may return a "context" object + if data: self.context.update(data) + + # TODO: self.context.children = self.run_pymodule_method('get_children') + + self.context.developer_mode = frappe.conf.developer_mode + + def set_pymodule_properties(self): + for prop in ("base_template_path", "template", "no_cache", "sitemap", + "condition_field"): + if hasattr(self.pymodule, prop): + self.context[prop] = getattr(self.pymodule, prop) + + def set_page_properties(self): + self.context.template = self.template_path + + def run_pymodule_method(self, method): + if hasattr(self.pymodule, method): + try: + return getattr(self.pymodule, method)(self) + except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect): + raise + except: + if not frappe.flags.in_migrate: + frappe.errprint(frappe.utils.get_traceback()) + + def render_template(self): + if self.path.endswith('min.js'): + # directly serve min.js pages using the jloader to find it in various apps + # (can be used as static?) + html = self.get_raw_template() + else: + html = frappe.get_template(self.template_path).render(self.context) + + def extends_template(self): + return (self.template_path.endswith(('.html', '.md', )) + and ('{%- extends' in self.source + or '{% extends' in self.source)) + + def get_raw_template(self): + return frappe.get_jloader().get_source(frappe.get_jenv(), self.template_path)[0] + + def load_colocated_files(self): + '''load co-located css/js files with the same name''' + js_path = self.basepath + '.js' + if os.path.exists(js_path) and '{% block script %}' not in self.source: + self.colocated_js = self.get_colocated_file(js_path) + + css_path = self.basepath + '.css' + if os.path.exists(css_path) and '{% block style %}' not in self.source: + self.colocated_css = self.get_colocated_file(css_path) + + def get_colocated_file(self, path): + with io.open(path, 'r', encoding = 'utf-8') as f: + return f.read() + + def extract_frontmatter(self): + try: + # values will be used to update page_info + res = get_frontmatter(self.source) + if res['attributes']: + self.context.update(res['attributes']) + self.source = res['body'] + except Exception: + pass + + def convert_from_markdown(self): + if self.template_path.endswith('.md'): + self.source = frappe.utils.md_to_html(self.source) + self.page_toc_html = self.toc_html + + if not self.show_sidebar: + self.source = '
' + self.source + '
' + + def update_toc(self, html): + if '{index}' in html: + html = html.replace('{index}', get_toc(self.path)) + + if '{next}' in html: + html = html.replace('{next}', get_next_link(self.path)) + + return html + + +class DocTypePage(WebPage): + pass + +class WebFormPage(WebPage): + pass + diff --git a/frappe/www/_test/static-file-test.png b/frappe/www/_test/static-file-test.png new file mode 100644 index 0000000000000000000000000000000000000000..b51db82f82b906b30609f807f0f663cac40d20b4 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$a>caTa()7Beu2se&-0XOPMVpde#$ zkh>GZx^prwfgF}}M_)$E)e-c@Na=xA}jv*C{Z|`n2ZE+NFc(}bULexYtxXVk! zRa1H)JCmwK`xQRZ9Zdhl)Xnb@xa7e_h#Kc{rb11QMP%$ zyna9bX2;pHxqo~9%j-KE^?Uy3M6(kqS{SBbSa7?V|4TdH>w?cef9;X4JIhPRIT!|D tb@a_URnxz=-&b0D_xf+-FaWXL-%6{zP`s|39McHmdb;|#taD0e0su)Qf|39L literal 0 HcmV?d00001 From 3f38c3300462dfcda96638c0597216613bf21f19 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 5 Feb 2021 15:54:23 +0530 Subject: [PATCH 0002/1519] fix: status_code, login page works --- frappe/app.py | 8 ++++++-- frappe/tests/test_website_new.py | 17 ++++++++++++++++- frappe/website/serve.py | 18 ++++++++++-------- frappe/www/error.py | 2 ++ 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/frappe/app.py b/frappe/app.py index adf2bfa8c9..9495268f00 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -18,7 +18,8 @@ import frappe.handler import frappe.auth import frappe.api import frappe.utils.response -import frappe.website.render +import frappe.website.serve +import frappe.website.page from frappe.utils import get_site_name, sanitize_html from frappe.middlewares import StaticDataMiddleware from frappe.utils.error import make_error_snapshot @@ -73,7 +74,10 @@ def application(request): response = frappe.utils.response.download_private_file(request.path) elif request.method in ('GET', 'HEAD', 'POST'): - response = frappe.website.render.render() + if frappe.conf.flag_new_website: + response = frappe.website.serve.render() + else: + response = frappe.website.render.render() else: raise NotFound diff --git a/frappe/tests/test_website_new.py b/frappe/tests/test_website_new.py index aa9e77d5ba..34786611d6 100644 --- a/frappe/tests/test_website_new.py +++ b/frappe/tests/test_website_new.py @@ -8,6 +8,12 @@ from frappe.website.utils import get_home_page from frappe.utils import set_request class TestWebsite(unittest.TestCase): + def setUp(self): + frappe.set_user('Guest') + + def tearDown(self): + frappe.set_user('Administrator') + def test_static_page(self): set_request(method='GET', path='/_test/static-file-test.png') response = serve.StaticPage().get() @@ -16,5 +22,14 @@ class TestWebsite(unittest.TestCase): def test_error_page(self): set_request(method='GET', path='/error') response = serve.TemplatePage().get() + self.assertEquals(response.status_code, 500) + + def test_login(self): + set_request(method='GET', path='/login') + response = serve.TemplatePage().get() self.assertEquals(response.status_code, 200) - print(response.get_data()) + + html = frappe.safe_decode(response.get_data()) + + self.assertTrue('// login.js' in html) + self.assertTrue('' in html) diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 3f3e1d4571..a0710b9988 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -22,6 +22,8 @@ def render(path=None, http_status_code=None): path = resolve_path(path) data = None + # there is no way to determine the type of the page based on the route + # so evaluate each type of page sequentially response = StaticPage(path).get() if not response: response = TemplatePage(path).get() @@ -93,7 +95,8 @@ class TemplatePage(WebPage): def find_page_in_app(self, app): ''' - Searches for file matching the path in the /www and /templates/pages folders + Searches for file matching the path in the /www + and /templates/pages folders ''' app_path = frappe.get_app_path(app) folders = get_start_folders() @@ -128,9 +131,7 @@ class TemplatePage(WebPage): self.set_pymodule() self.setup_template() - - if self.pymodule_name: - self.update_context() + self.update_context() if self.source: html = frappe.render_template(self.source, self.context) @@ -178,18 +179,19 @@ class TemplatePage(WebPage): self.context.update(get_website_settings(self.context)) self.context.update(frappe.local.conf.get("website_context") or {}) - self.pymodule = frappe.get_module(self.pymodule_name) - - if self.pymodule: + if self.pymodule_name: + self.pymodule = frappe.get_module(self.pymodule_name) self.set_pymodule_properties() data = self.run_pymodule_method('get_context') + # some methods may return a "context" object if data: self.context.update(data) # TODO: self.context.children = self.run_pymodule_method('get_children') self.context.developer_mode = frappe.conf.developer_mode + self.status_code = self.context.http_status_code or 200 def set_pymodule_properties(self): for prop in ("base_template_path", "template", "no_cache", "sitemap", @@ -203,7 +205,7 @@ class TemplatePage(WebPage): def run_pymodule_method(self, method): if hasattr(self.pymodule, method): try: - return getattr(self.pymodule, method)(self) + return getattr(self.pymodule, method)(self.context) except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect): raise except: diff --git a/frappe/www/error.py b/frappe/www/error.py index 161038373d..9274fe7c26 100644 --- a/frappe/www/error.py +++ b/frappe/www/error.py @@ -8,5 +8,7 @@ no_cache = 1 def get_context(context): if frappe.flags.in_migrate: return + context.http_status_code = 500 + print(frappe.get_traceback().encode("utf-8")) return {"error": frappe.get_traceback().replace("<", "<").replace(">", ">") } From 4369b8c0dc3d88c3d37af7ead06a4b35e4812778 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 12 Feb 2021 10:02:10 +0530 Subject: [PATCH 0003/1519] fix(rendering): add doctype generate pages --- frappe/app.py | 7 +- frappe/sessions.py | 3 +- frappe/tests/test_website_new.py | 20 ++- frappe/website/context.py | 2 +- frappe/website/render.py | 7 + frappe/website/serve.py | 231 ++++++++++++++++++++++++++++--- frappe/www/404.py | 2 + 7 files changed, 241 insertions(+), 31 deletions(-) diff --git a/frappe/app.py b/frappe/app.py index 9495268f00..52a8d2fe1c 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -18,8 +18,6 @@ import frappe.handler import frappe.auth import frappe.api import frappe.utils.response -import frappe.website.serve -import frappe.website.page from frappe.utils import get_site_name, sanitize_html from frappe.middlewares import StaticDataMiddleware from frappe.utils.error import make_error_snapshot @@ -74,10 +72,7 @@ def application(request): response = frappe.utils.response.download_private_file(request.path) elif request.method in ('GET', 'HEAD', 'POST'): - if frappe.conf.flag_new_website: - response = frappe.website.serve.render() - else: - response = frappe.website.render.render() + response = frappe.website.render.render() else: raise NotFound diff --git a/frappe/sessions.py b/frappe/sessions.py index 1ca1d4ee6f..dddeb6f37f 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -169,7 +169,8 @@ def get_csrf_token(): def generate_csrf_token(): frappe.local.session.data.csrf_token = frappe.generate_hash() - frappe.local.session_obj.update(force=True) + if not frappe.flags.in_test: + frappe.local.session_obj.update(force=True) class Session: def __init__(self, user, resume=False, full_name=None, user_type=None): diff --git a/frappe/tests/test_website_new.py b/frappe/tests/test_website_new.py index 34786611d6..3d4c2a69ef 100644 --- a/frappe/tests/test_website_new.py +++ b/frappe/tests/test_website_new.py @@ -6,7 +6,6 @@ import frappe from frappe.website import serve from frappe.website.utils import get_home_page from frappe.utils import set_request - class TestWebsite(unittest.TestCase): def setUp(self): frappe.set_user('Guest') @@ -21,15 +20,30 @@ class TestWebsite(unittest.TestCase): def test_error_page(self): set_request(method='GET', path='/error') - response = serve.TemplatePage().get() + response = serve.get_response() self.assertEquals(response.status_code, 500) def test_login(self): set_request(method='GET', path='/login') - response = serve.TemplatePage().get() + response = serve.get_response() self.assertEquals(response.status_code, 200) html = frappe.safe_decode(response.get_data()) self.assertTrue('// login.js' in html) self.assertTrue('' in html) + + def test_app(self): + frappe.set_user('Administrator') + set_request(method='GET', path='/app') + response = serve.get_response() + self.assertEquals(response.status_code, 200) + + html = frappe.safe_decode(response.get_data()) + self.assertTrue('window.app = true;' in html) + frappe.local.session_obj = None + + def test_not_found(self): + set_request(method='GET', path='/_test/missing') + response = serve.get_response() + self.assertEquals(response.status_code, 404) diff --git a/frappe/website/context.py b/frappe/website/context.py index 4236971aec..abda2e3f4f 100644 --- a/frappe/website/context.py +++ b/frappe/website/context.py @@ -243,7 +243,7 @@ def add_metatags(context): tags["og:title"] = tags["twitter:title"] = title tags["twitter:card"] = "summary" - if "description" not in tags and context.description: + if not tags.get('description') and context.description: tags["description"] = context.description description = tags.get("description") diff --git a/frappe/website/render.py b/frappe/website/render.py index 2f8bc59d6d..ce9647c62a 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -26,6 +26,13 @@ from frappe.translate import guess_language class PageNotFoundError(Exception): pass def render(path=None, http_status_code=None): + if frappe.conf.flag_new_website: + from frappe.website.serve import get_response + return get_response() + else: + return render(path, http_status_code) + +def _render(path=None, http_status_code=None): """render html page""" if not path: path = frappe.local.request.path diff --git a/frappe/website/serve.py b/frappe/website/serve.py index a0710b9988..ec54e8c149 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -6,12 +6,13 @@ from werkzeug.wsgi import wrap_file from frappe.website.render import (resolve_path, build_response) from frappe.website.redirect import resolve_redirect -from frappe.website.router import get_start_folders +from frappe.website.router import (get_start_folders, get_doctypes_with_web_view, + get_page_info_from_web_page_with_dynamic_routes) from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, get_toc, get_next_link) from frappe.website.doctype.website_settings.website_settings import get_website_settings -def render(path=None, http_status_code=None): +def get_response(path=None, http_status_code=None): """render html page""" if not path: path = frappe.local.request.path @@ -87,7 +88,120 @@ class StaticPage(WebPage): response.mimetype = mimetypes.guess_type(self.file_path)[0] or 'application/octet-stream' return response -class TemplatePage(WebPage): +class BaseTemplatePage(WebPage): + def init_context(self): + self.context = frappe._dict() + self.context.update(get_website_settings(self.context)) + self.context.update(frappe.local.conf.get("website_context") or {}) + + def add_csrf_token(self, html): + if frappe.local.session: + return html.replace("", ''.format( + frappe.local.session.data.csrf_token)) + else: + return html + + def post_process_context(self): + self.add_metatags() + # add_sidebar_and_breadcrumbs(context) + + self.set_base_template_if_missing() + self.set_title_with_prefix() + self.update_website_context() + + # set using frappe.respond_as_web_page + if hasattr(frappe.local, 'response') and frappe.local.response.get('context'): + context.update(frappe.local.response.context) + + # to be able to inspect the context dict + # Use the macro "inspect" from macros.html + self.context._context_dict = self.context + + def set_base_template_if_missing(self): + if not self.context.base_template_path: + app_base = frappe.get_hooks("base_template") + self.context.base_template_path = app_base[-1] if app_base else "templates/base.html" + + def set_title_with_prefix(self): + if (self.context.title_prefix and self.context.title + and not self.context.title.startswith(self.context.title_prefix)): + self.context.title = '{0} - {1}'.format(self.context.title_prefix, self.context.title) + + def update_website_context(self): + # apply context from hooks + update_website_context = frappe.get_hooks('update_website_context') + for method in update_website_context: + values = frappe.get_attr(method)(self.context) + if values: + self.context.update(values) + + def add_metatags(self): + self.tags = frappe._dict(self.context.get("metatags") or {}) + self.init_metatags_from_context() + self.set_opengraph_tags() + self.set_twitter_tags() + self.set_meta_published_on() + self.set_metatags_from_website_route_meta() + + self.context.metatags = self.tags + + def init_metatags_from_context(self): + for key in ('title', 'description', 'image', 'author', 'url', 'published_on'): + if not key in self.tags and self.context.get(key): + self.tags[key] = self.context[key] + + if not self.tags.get('title'): self.tags['title'] = self.context.get('name') + + if self.tags.get('image'): + self.tags['image'] = frappe.utils.get_url(self.tags['image']) + + self.tags["language"] = frappe.local.lang or "en" + + def set_opengraph_tags(self): + if "og:type" not in self.tags: + self.tags["og:type"] = "article" + + for key in ('title', 'description', 'image', 'author', 'url'): + if self.tags.get(key): + self.tags['og:' + key] = self.tags.get(key) + + def set_twitter_tags(self): + for key in ('title', 'description', 'image', 'author', 'url'): + if self.tags.get(key): + self.tags['twitter:' + key] = self.tags.get(key) + + if self.tags.get('image'): + self.tags['twitter:card'] = "summary_large_image" + else: + self.tags["twitter:card"] = "summary" + + def set_meta_published_on(self): + if "published_on" in self.tags: + self.tags["datePublished"] = self.tags["published_on"] + del self.tags["published_on"] + + def set_metatags_from_website_route_meta(self): + ''' + Get meta tags from Website Route meta + they can override the defaults set above + ''' + route = self.context.path + if route == '': + # homepage + route = frappe.db.get_single_value('Website Settings', 'home_page') + + route_exists = (route + and not route.endswith(('.js', '.css')) + and frappe.db.exists('Website Route Meta', route)) + + if route_exists: + website_route_meta = frappe.get_doc('Website Route Meta', route) + for meta_tag in website_route_meta.meta_tags: + d = meta_tag.get_meta_dict() + self.tags.update(d) + + +class TemplatePage(BaseTemplatePage): def validate(self): for app in frappe.get_installed_apps(frappe_last=True): if self.find_page_in_app(app): @@ -127,19 +241,16 @@ class TemplatePage(WebPage): def get_html(self): # context object should be separate from self for security # because it will be accessed via the user defined template - self.context = frappe._dict() + self.init_context() self.set_pymodule() self.setup_template() self.update_context() - - if self.source: - html = frappe.render_template(self.source, self.context) - - elif self.template_path: - html = self.render_template() + self.post_process_context() + html = self.render_template() html = self.update_toc(html) + html = self.add_csrf_token(html) return html @@ -176,8 +287,6 @@ class TemplatePage(WebPage): def update_context(self): self.set_page_properties() - self.context.update(get_website_settings(self.context)) - self.context.update(frappe.local.conf.get("website_context") or {}) if self.pymodule_name: self.pymodule = frappe.get_module(self.pymodule_name) @@ -213,12 +322,15 @@ class TemplatePage(WebPage): frappe.errprint(frappe.utils.get_traceback()) def render_template(self): - if self.path.endswith('min.js'): - # directly serve min.js pages using the jloader to find it in various apps - # (can be used as static?) - html = self.get_raw_template() - else: - html = frappe.get_template(self.template_path).render(self.context) + if self.source: + html = frappe.render_template(self.source, self.context) + elif self.template_path: + if self.path.endswith('min.js'): + html = self.get_raw_template() # static + else: + html = frappe.get_template(self.template_path).render(self.context) + + return html def extends_template(self): return (self.template_path.endswith(('.html', '.md', )) @@ -270,8 +382,87 @@ class TemplatePage(WebPage): return html -class DocTypePage(WebPage): - pass +class DocTypePage(BaseTemplatePage): + def validate(self): + ''' + Find a document with matching `route` from all doctypes with `has_web_view`=1 + ''' + if self.search_in_doctypes_with_web_view(): + return True + + if self.search_web_page_dynamic_routes(): + return True + + return False + + def search_in_doctypes_with_web_view(self): + for doctype in get_doctypes_with_web_view(): + filters = dict(route=self.path) + meta = frappe.get_meta(doctype) + condition_field = self.get_condition_field(meta) + + if condition_field: + filters[condition_field] = 1 + + try: + self.docname = frappe.db.get_value(doctype, filters, 'name') + if self.docname: + self.doctype = doctype + self.meta = meta + return True + except Exception as e: + if not frappe.db.is_missing_column(e): raise e + + def search_web_page_dynamic_routes(self): + d = get_page_info_from_web_page_with_dynamic_routes(self.path) + if d: + self.doctype = 'Web Page' + self.docname = d.name + self.meta = frappe.get_meta(self.doctype) + return True + else: + return False + + def render(self): + self.doc = frappe.get_doc(self.doctype, self.docname) + self.init_context() + self.update_context() + self.post_process_context() + + html = frappe.get_template(self.context.template_path).render(self.context) + html = self.add_csrf_token(html) + + return build_response(self.path, html, self.status_code, self.headers) + + def update_context(self): + self.context.doc = self.doc + self.context.update(self.context.doc.as_dict()) + self.context.update(self.context.doc.get_website_properties()) + + if not self.context.template_path: + self.context.template_path = self.context.doc.meta.get_web_template() + + if hasattr(self.doc, "get_context"): + ret = self.doc.get_context(self.context) + + if ret: + self.context.update(ret) + + for prop in ("no_cache", "sitemap"): + if not prop in self.context: + self.context[prop] = getattr(self.doc, prop, False) + + def get_condition_field(self, meta): + condition_field = None + if meta.is_published_field: + condition_field = meta.is_published_field + elif not meta.custom: + controller = get_controller(doctype) + condition_field = controller.website.condition_field + + return condition_field + + class WebFormPage(WebPage): pass diff --git a/frappe/www/404.py b/frappe/www/404.py index c9de234743..e02e1c3929 100644 --- a/frappe/www/404.py +++ b/frappe/www/404.py @@ -2,3 +2,5 @@ # MIT License. See license.txt from __future__ import unicode_literals +def get_context(context): + context.http_status_code = 404 From 46dc5af29a81bbe00b022e30e096add46ecb88dd Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 15 Feb 2021 12:55:47 +0530 Subject: [PATCH 0004/1519] fix(list): make list pages work --- frappe/website/doctype/blog_post/blog_post.py | 5 --- frappe/website/render.py | 5 +-- frappe/website/router.py | 2 +- frappe/website/serve.py | 34 +++++++++++++------ 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 28549225be..e7c2772c69 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -13,11 +13,6 @@ from frappe.website.utils import (find_first_image, get_html_content_based_on_ty get_comment_list) class BlogPost(WebsiteGenerator): - website = frappe._dict( - route = 'blog', - order_by = "published_on desc" - ) - def make_route(self): if not self.route: return frappe.db.get_value('Blog Category', self.blog_category, diff --git a/frappe/website/render.py b/frappe/website/render.py index ce9647c62a..21d5dda8d5 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -26,11 +26,12 @@ from frappe.translate import guess_language class PageNotFoundError(Exception): pass def render(path=None, http_status_code=None): - if frappe.conf.flag_new_website: + # temp feature flag + if True or frappe.conf.flag_new_website: from frappe.website.serve import get_response return get_response() else: - return render(path, http_status_code) + return _render(path, http_status_code) def _render(path=None, http_status_code=None): """render html page""" diff --git a/frappe/website/router.py b/frappe/website/router.py index 946c83811a..5244c57ba8 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -441,4 +441,4 @@ def get_doctypes_with_web_view(): 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') \ No newline at end of file + return frappe.local.flags.web_pages_folders or ('www', 'templates/pages') diff --git a/frappe/website/serve.py b/frappe/website/serve.py index ec54e8c149..6784dbefd9 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -29,7 +29,9 @@ def get_response(path=None, http_status_code=None): if not response: response = TemplatePage(path).get() if not response: - response = DocTypePage(path).get() + response = ListPage(path).get() + if not response: + response = DocumentPage(path).get() if not response: response = TemplatePage('404').get() except frappe.PermissionError as e: @@ -117,6 +119,11 @@ class BaseTemplatePage(WebPage): # Use the macro "inspect" from macros.html self.context._context_dict = self.context + # context sends us a new template path + if self.context.template: + self.template_path = self.context.template + + def set_base_template_if_missing(self): if not self.context.base_template_path: app_base = frappe.get_hooks("base_template") @@ -200,7 +207,6 @@ class BaseTemplatePage(WebPage): d = meta_tag.get_meta_dict() self.tags.update(d) - class TemplatePage(BaseTemplatePage): def validate(self): for app in frappe.get_installed_apps(frappe_last=True): @@ -222,8 +228,6 @@ class TemplatePage(BaseTemplatePage): if os.path.exists(file_path) and not os.path.isdir(file_path): self.app = app self.app_path = app_path - self.dirname = dirname - self.file_path = file_path self.template_path = os.path.relpath(file_path, self.app_path) return True @@ -244,9 +248,9 @@ class TemplatePage(BaseTemplatePage): self.init_context() self.set_pymodule() - self.setup_template() self.update_context() self.post_process_context() + self.setup_template() html = self.render_template() html = self.update_toc(html) @@ -380,9 +384,18 @@ class TemplatePage(BaseTemplatePage): html = html.replace('{next}', get_next_link(self.path)) return html - - -class DocTypePage(BaseTemplatePage): +class ListPage(TemplatePage): + def validate(self): + if frappe.db.get_value('DocType', self.path): + self.app = 'frappe' + self.app_path = frappe.get_app_path('frappe') + self.doctype = self.path + self.path = 'list' + self.template_path = 'www/list.html' + frappe.local.form_dict.doctype = self.doctype + return True + return False +class DocumentPage(BaseTemplatePage): def validate(self): ''' Find a document with matching `route` from all doctypes with `has_web_view`=1 @@ -462,8 +475,7 @@ class DocTypePage(BaseTemplatePage): return condition_field - - +class PrintPage(TemplatePage): + pass class WebFormPage(WebPage): pass - From 75ba0ca6a9be2f8a1d43529ff8a28a43efc42457 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 15 Feb 2021 13:49:38 +0530 Subject: [PATCH 0005/1519] fix(minor): website/serve.py --- frappe/website/serve.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 6784dbefd9..8f686144f1 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -113,7 +113,7 @@ class BaseTemplatePage(WebPage): # set using frappe.respond_as_web_page if hasattr(frappe.local, 'response') and frappe.local.response.get('context'): - context.update(frappe.local.response.context) + self.context.update(frappe.local.response.context) # to be able to inspect the context dict # Use the macro "inspect" from macros.html @@ -384,6 +384,7 @@ class TemplatePage(BaseTemplatePage): html = html.replace('{next}', get_next_link(self.path)) return html + class ListPage(TemplatePage): def validate(self): if frappe.db.get_value('DocType', self.path): @@ -395,6 +396,7 @@ class ListPage(TemplatePage): frappe.local.form_dict.doctype = self.doctype return True return False + class DocumentPage(BaseTemplatePage): def validate(self): ''' @@ -477,5 +479,6 @@ class DocumentPage(BaseTemplatePage): class PrintPage(TemplatePage): pass + class WebFormPage(WebPage): pass From e79e1420612b814f1e65ce9b4021d0c31f00d00a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 15 Feb 2021 17:43:17 +0530 Subject: [PATCH 0006/1519] fix(minor): fix some tests and merge old and new website tests --- .../print_settings/print_settings.json | 4 +- frappe/tests/test_website.py | 46 ++++++++++- frappe/tests/test_website_new.py | 49 ------------ frappe/website/render.py | 2 +- frappe/website/serve.py | 80 ++++++++++++++----- frappe/www/message.py | 8 +- 6 files changed, 113 insertions(+), 76 deletions(-) delete mode 100644 frappe/tests/test_website_new.py diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json index d64cb4c6d3..31962be050 100644 --- a/frappe/printing/doctype/print_settings/print_settings.json +++ b/frappe/printing/doctype/print_settings/print_settings.json @@ -148,7 +148,7 @@ "label": "Print Style" }, { - "default": "Modern", + "default": "Redesign", "fieldname": "print_style", "fieldtype": "Link", "in_list_view": 1, @@ -183,7 +183,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-22 23:42:09.471022", + "modified": "2021-02-15 14:16:18.474254", "modified_by": "Administrator", "module": "Printing", "name": "Print Settings", diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index c5da2bdfb7..91e08db457 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -4,13 +4,19 @@ import unittest import frappe from frappe.website import render +from frappe.website import serve from frappe.website.utils import get_home_page from frappe.utils import set_request - class TestWebsite(unittest.TestCase): + def setUp(self): + frappe.set_user('Guest') + + def tearDown(self): + frappe.set_user('Administrator') def test_home_page_for_role(self): + frappe.set_user('Administrator') frappe.delete_doc_if_exists('User', 'test-user-for-home-page@example.com') frappe.delete_doc_if_exists('Role', 'home-page-test') frappe.delete_doc_if_exists('Web Page', 'home-page-test') @@ -44,7 +50,6 @@ class TestWebsite(unittest.TestCase): self.assertEqual(get_home_page(), 'test-portal-home') def test_page_load(self): - frappe.set_user('Guest') set_request(method='POST', path='login') response = render.render() @@ -54,10 +59,47 @@ class TestWebsite(unittest.TestCase): self.assertTrue('// login.js' in html) self.assertTrue('' in html) + + def test_static_page(self): + set_request(method='GET', path='/_test/static-file-test.png') + response = serve.get_response() + self.assertEquals(response.status_code, 200) + + def test_error_page(self): + set_request(method='GET', path='/error') + response = serve.get_response() + self.assertEquals(response.status_code, 500) + + def test_login(self): + set_request(method='GET', path='/login') + response = serve.get_response() + self.assertEquals(response.status_code, 200) + + html = frappe.safe_decode(response.get_data()) + + self.assertTrue('// login.js' in html) + self.assertTrue('' in html) + + def test_app(self): frappe.set_user('Administrator') + set_request(method='GET', path='/app') + response = serve.get_response() + self.assertEquals(response.status_code, 200) + + html = frappe.safe_decode(response.get_data()) + self.assertTrue('window.app = true;' in html) + frappe.local.session_obj = None + + def test_not_found(self): + set_request(method='GET', path='/_test/missing') + response = serve.get_response() + self.assertEquals(response.status_code, 404) + def test_redirect(self): import frappe.hooks + frappe.set_user('Administrator') + frappe.hooks.website_redirects = [ dict(source=r'/testfrom', target=r'://testto1'), dict(source=r'/testfromregex.*', target=r'://testto2'), diff --git a/frappe/tests/test_website_new.py b/frappe/tests/test_website_new.py deleted file mode 100644 index 3d4c2a69ef..0000000000 --- a/frappe/tests/test_website_new.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import unicode_literals - -import unittest - -import frappe -from frappe.website import serve -from frappe.website.utils import get_home_page -from frappe.utils import set_request -class TestWebsite(unittest.TestCase): - def setUp(self): - frappe.set_user('Guest') - - def tearDown(self): - frappe.set_user('Administrator') - - def test_static_page(self): - set_request(method='GET', path='/_test/static-file-test.png') - response = serve.StaticPage().get() - self.assertEquals(response.status_code, 200) - - def test_error_page(self): - set_request(method='GET', path='/error') - response = serve.get_response() - self.assertEquals(response.status_code, 500) - - def test_login(self): - set_request(method='GET', path='/login') - response = serve.get_response() - self.assertEquals(response.status_code, 200) - - html = frappe.safe_decode(response.get_data()) - - self.assertTrue('// login.js' in html) - self.assertTrue('' in html) - - def test_app(self): - frappe.set_user('Administrator') - set_request(method='GET', path='/app') - response = serve.get_response() - self.assertEquals(response.status_code, 200) - - html = frappe.safe_decode(response.get_data()) - self.assertTrue('window.app = true;' in html) - frappe.local.session_obj = None - - def test_not_found(self): - set_request(method='GET', path='/_test/missing') - response = serve.get_response() - self.assertEquals(response.status_code, 404) diff --git a/frappe/website/render.py b/frappe/website/render.py index 21d5dda8d5..65f871b8a0 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -29,7 +29,7 @@ def render(path=None, http_status_code=None): # temp feature flag if True or frappe.conf.flag_new_website: from frappe.website.serve import get_response - return get_response() + return get_response(path, http_status_code) else: return _render(path, http_status_code) diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 8f686144f1..927909e75f 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -25,26 +25,34 @@ def get_response(path=None, http_status_code=None): # there is no way to determine the type of the page based on the route # so evaluate each type of page sequentially - response = StaticPage(path).get() + response = StaticPage(path, http_status_code).get() if not response: - response = TemplatePage(path).get() + response = TemplatePage(path, http_status_code).get() if not response: - response = ListPage(path).get() + response = ListPage(path, http_status_code).get() if not response: - response = DocumentPage(path).get() + response = DocumentPage(path, http_status_code).get() if not response: - response = TemplatePage('404').get() + response = PrintPage(path, http_status_code).get() + if not response: + response = TemplatePage('404', 404).get() + except frappe.Redirect: + return build_response(path, "", 301, { + "Location": frappe.flags.redirect_location or (frappe.local.response or {}).get('location'), + "Cache-Control": "no-store, no-cache, must-revalidate" + }) except frappe.PermissionError as e: - response = TemplatePage('403').get() - except: - response = TemplatePage('error').get() + frappe.local.message = cstr(e) + response = NotPermittedPage(path, http_status_code).get() + except Exception as e: + response = TemplatePage('error', getattr(e, 'http_status_code', 500) or http_status_code).get() return response class WebPage(object): - def __init__(self, path=None): + def __init__(self, path=None, http_status_code=200): self.headers = None - self.status_code = 200 + self.http_status_code = http_status_code if not path: path = frappe.local.request.path self.path = path.strip('/ ') @@ -240,7 +248,7 @@ class TemplatePage(BaseTemplatePage): search_path + '/index.md') def render(self): - return build_response(self.path, self.get_html(), self.status_code, self.headers) + return build_response(self.path, self.get_html(), self.http_status_code, self.headers) def get_html(self): # context object should be separate from self for security @@ -304,7 +312,8 @@ class TemplatePage(BaseTemplatePage): # TODO: self.context.children = self.run_pymodule_method('get_children') self.context.developer_mode = frappe.conf.developer_mode - self.status_code = self.context.http_status_code or 200 + if self.context.http_status_code: + self.http_status_code = self.context.http_status_code def set_pymodule_properties(self): for prop in ("base_template_path", "template", "no_cache", "sitemap", @@ -385,18 +394,50 @@ class TemplatePage(BaseTemplatePage): return html + def set_standard_path(self, path): + self.app = 'frappe' + self.app_path = frappe.get_app_path('frappe') + self.path = path + self.template_path = 'www/{path}.html'.format(path=path) + + class ListPage(TemplatePage): def validate(self): if frappe.db.get_value('DocType', self.path): - self.app = 'frappe' - self.app_path = frappe.get_app_path('frappe') - self.doctype = self.path - self.path = 'list' - self.template_path = 'www/list.html' - frappe.local.form_dict.doctype = self.doctype + frappe.local.form_dict.doctype = self.path + self.set_standard_path('list') return True return False +class PrintPage(TemplatePage): + ''' + default path returns a printable object (based on permission) + /Quotation/Q-0001 + ''' + def validate(self): + parts = self.path.split('/', 1) + if len(parts)==2: + if (frappe.db.get_value('DocType', parts[0]) + and frappe.db.get_value(parts[0], parts[1])): + frappe.form_dict.doctype = parts[0] + frappe.form_dict.name = parts[1] + self.set_standard_path('printview') + return True + + return False + +class NotPermittedPage(TemplatePage): + def validate(self): + frappe.local.message_title = _("Not Permitted") + frappe.local.response['context'] = dict( + indicator_color = 'red', + primary_action = '/login', + primary_label = _('Login'), + fullpage=True + ) + self.set_standard_path('message') + return True + class DocumentPage(BaseTemplatePage): def validate(self): ''' @@ -477,8 +518,5 @@ class DocumentPage(BaseTemplatePage): return condition_field -class PrintPage(TemplatePage): - pass - class WebFormPage(WebPage): pass diff --git a/frappe/www/message.py b/frappe/www/message.py index ea45b9c4b5..d650170073 100644 --- a/frappe/www/message.py +++ b/frappe/www/message.py @@ -9,7 +9,7 @@ from frappe.utils import strip_html_tags no_cache = 1 def get_context(context): - message_context = {} + message_context = frappe._dict() if hasattr(frappe.local, "message"): message_context["header"] = frappe.local.message_title message_context["title"] = strip_html_tags(frappe.local.message_title) @@ -26,4 +26,10 @@ def get_context(context): if message.get('http_status_code'): frappe.local.response['http_status_code'] = message['http_status_code'] + if not message_context.title: + message_context.title = frappe.form_dict.title + + if not message_context.message: + message_context.message = frappe.form_dict.message + return message_context From b699d92f5ee57d54918221fb9d74c54ca4cfe24a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 15 Feb 2021 18:05:44 +0530 Subject: [PATCH 0007/1519] fix(minor): http_status_code --- frappe/website/serve.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 927909e75f..b635d9615b 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -12,7 +12,7 @@ from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, get_toc, get_next_link) from frappe.website.doctype.website_settings.website_settings import get_website_settings -def get_response(path=None, http_status_code=None): +def get_response(path=None, http_status_code=200): """render html page""" if not path: path = frappe.local.request.path @@ -45,14 +45,14 @@ def get_response(path=None, http_status_code=None): frappe.local.message = cstr(e) response = NotPermittedPage(path, http_status_code).get() except Exception as e: - response = TemplatePage('error', getattr(e, 'http_status_code', 500) or http_status_code).get() + response = TemplatePage('error', getattr(e, 'http_status_code', None) or http_status_code).get() return response class WebPage(object): - def __init__(self, path=None, http_status_code=200): + def __init__(self, path=None, http_status_code=None): self.headers = None - self.http_status_code = http_status_code + self.http_status_code = http_status_code or 200 if not path: path = frappe.local.request.path self.path = path.strip('/ ') @@ -488,7 +488,7 @@ class DocumentPage(BaseTemplatePage): html = frappe.get_template(self.context.template_path).render(self.context) html = self.add_csrf_token(html) - return build_response(self.path, html, self.status_code, self.headers) + return build_response(self.path, html, self.http_status_code or 200, self.headers) def update_context(self): self.context.doc = self.doc From 30659b9ee28e034c6eef7ba682eea5b302b6fb6b Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 11 Mar 2021 10:29:12 +0530 Subject: [PATCH 0008/1519] feat: Option to consider query string while writing website redirect - if match_with_query_string is true in a website redirect rule, system will try to match the regex with the result of route + query_string --- frappe/tests/test_website.py | 20 +++++++++++++------- frappe/website/redirect.py | 12 ++++++++---- frappe/website/serve.py | 28 ++++++++++++++++++---------- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 91e08db457..ce165e36d9 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -51,7 +51,7 @@ class TestWebsite(unittest.TestCase): def test_page_load(self): set_request(method='POST', path='login') - response = render.render() + response = serve.get_response() self.assertEquals(response.status_code, 200) @@ -103,7 +103,8 @@ class TestWebsite(unittest.TestCase): frappe.hooks.website_redirects = [ dict(source=r'/testfrom', target=r'://testto1'), dict(source=r'/testfromregex.*', target=r'://testto2'), - dict(source=r'/testsub/(.*)', target=r'://testto3/\1') + dict(source=r'/testsub/(.*)', target=r'://testto3/\1'), + dict(source=r'/courses/course\?course=(.*)', target=r'/courses/\1', match_with_query_string=True), ] website_settings = frappe.get_doc('Website Settings') @@ -117,28 +118,33 @@ class TestWebsite(unittest.TestCase): frappe.cache().delete_key('website_redirects') set_request(method='GET', path='/testfrom') - response = render.render() + response = serve.get_response() self.assertEquals(response.status_code, 301) self.assertEquals(response.headers.get('Location'), r'://testto1') set_request(method='GET', path='/testfromregex/test') - response = render.render() + response = serve.get_response() self.assertEquals(response.status_code, 301) self.assertEquals(response.headers.get('Location'), r'://testto2') set_request(method='GET', path='/testsub/me') - response = render.render() + response = serve.get_response() self.assertEquals(response.status_code, 301) self.assertEquals(response.headers.get('Location'), r'://testto3/me') set_request(method='GET', path='/test404') - response = render.render() + response = serve.get_response() self.assertEquals(response.status_code, 404) set_request(method='GET', path='/testsource') - response = render.render() + response = serve.get_response() self.assertEquals(response.status_code, 301) self.assertEquals(response.headers.get('Location'), '/testtarget') + set_request(method='GET', path='/courses/course?course=data') + response = serve.get_response() + self.assertEquals(response.status_code, 301) + self.assertEquals(response.headers.get('Location'), '/courses/data') + delattr(frappe.hooks, 'website_redirects') frappe.cache().delete_key('app_hooks') diff --git a/frappe/website/redirect.py b/frappe/website/redirect.py index 73e3c21727..e66c0a3b7b 100644 --- a/frappe/website/redirect.py +++ b/frappe/website/redirect.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import re, frappe -def resolve_redirect(path): +def resolve_redirect(path, query_string=None): ''' Resolve redirects from hooks @@ -33,9 +33,13 @@ def resolve_redirect(path): for rule in redirects: pattern = rule['source'].strip('/ ') + '$' - if re.match(pattern, path): - redirect_to = re.sub(pattern, rule['target'], path) + path_to_match = path + if rule.get('match_with_query_string'): + path_to_match = path + '?' + frappe.safe_decode(query_string) + + if re.match(pattern, path_to_match): + redirect_to = re.sub(pattern, rule['target'], path_to_match) frappe.flags.redirect_location = redirect_to - frappe.cache().hset('website_redirects', path, redirect_to) + frappe.cache().hset('website_redirects', path_to_match, redirect_to) raise frappe.Redirect diff --git a/frappe/website/serve.py b/frappe/website/serve.py index b635d9615b..11e950573e 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -1,25 +1,33 @@ -import frappe -import os, mimetypes +import mimetypes +import os from werkzeug.wrappers import Response from werkzeug.wsgi import wrap_file -from frappe.website.render import (resolve_path, build_response) +import frappe +from frappe import _ +from frappe.utils import cint, cstr +from frappe.model.document import get_controller +from frappe.website.doctype.website_settings.website_settings import \ + get_website_settings from frappe.website.redirect import resolve_redirect -from frappe.website.router import (get_start_folders, get_doctypes_with_web_view, - get_page_info_from_web_page_with_dynamic_routes) -from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, - get_toc, get_next_link) -from frappe.website.doctype.website_settings.website_settings import get_website_settings +from frappe.website.render import build_response, resolve_path +from frappe.website.router import (get_doctypes_with_web_view, + get_page_info_from_web_page_with_dynamic_routes, get_start_folders) +from frappe.website.utils import (can_cache, delete_page_cache, get_home_page, + get_next_link, get_toc) + def get_response(path=None, http_status_code=200): """render html page""" + query_string = None if not path: path = frappe.local.request.path + query_string = frappe.local.request.query_string try: path = path.strip('/ ') - resolve_redirect(path) + resolve_redirect(path, query_string) path = resolve_path(path) data = None @@ -513,7 +521,7 @@ class DocumentPage(BaseTemplatePage): if meta.is_published_field: condition_field = meta.is_published_field elif not meta.custom: - controller = get_controller(doctype) + controller = get_controller(meta.doctype) condition_field = controller.website.condition_field return condition_field From 39da825592429ab933e65368ef30d39de7a9208e Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 12 Mar 2021 11:42:24 +0530 Subject: [PATCH 0009/1519] refactor: Move page controllers to separate files - Make template page work --- .../page_controllers/base_template_page.py | 128 +++++ .../website/page_controllers/document_page.py | 88 ++++ frappe/website/page_controllers/list_page.py | 10 + .../page_controllers/not_permitted_page.py | 15 + frappe/website/page_controllers/print_page.py | 19 + .../website/page_controllers/static_page.py | 40 ++ .../website/page_controllers/template_page.py | 276 ++++++++++ frappe/website/page_controllers/web_form.py | 4 + frappe/website/page_controllers/web_page.py | 21 + frappe/website/serve.py | 496 +----------------- 10 files changed, 610 insertions(+), 487 deletions(-) create mode 100644 frappe/website/page_controllers/base_template_page.py create mode 100644 frappe/website/page_controllers/document_page.py create mode 100644 frappe/website/page_controllers/list_page.py create mode 100644 frappe/website/page_controllers/not_permitted_page.py create mode 100644 frappe/website/page_controllers/print_page.py create mode 100644 frappe/website/page_controllers/static_page.py create mode 100644 frappe/website/page_controllers/template_page.py create mode 100644 frappe/website/page_controllers/web_form.py create mode 100644 frappe/website/page_controllers/web_page.py diff --git a/frappe/website/page_controllers/base_template_page.py b/frappe/website/page_controllers/base_template_page.py new file mode 100644 index 0000000000..10e2375356 --- /dev/null +++ b/frappe/website/page_controllers/base_template_page.py @@ -0,0 +1,128 @@ +import frappe +from frappe import _ +from frappe.website.context import add_sidebar_and_breadcrumbs +from frappe.website.doctype.website_settings.website_settings import \ + get_website_settings +from frappe.website.page_controllers.web_page import WebPage + + +class BaseTemplatePage(WebPage): + def init_context(self): + self.context = frappe._dict() + self.context.update(get_website_settings(self.context)) + self.context.update(frappe.local.conf.get("website_context") or {}) + + def add_csrf_token(self, html): + if frappe.local.session: + return html.replace("", ''.format( + frappe.local.session.data.csrf_token)) + else: + return html + + def post_process_context(self): + self.set_missing_values() + self.add_metatags() + self.add_sidebar_and_breadcrumbs() + + self.set_base_template_if_missing() + self.set_title_with_prefix() + self.update_website_context() + + # set using frappe.respond_as_web_page + if hasattr(frappe.local, 'response') and frappe.local.response.get('context'): + self.context.update(frappe.local.response.context) + + # to be able to inspect the context dict + # Use the macro "inspect" from macros.html + self.context._context_dict = self.context + + # context sends us a new template path + if self.context.template: + self.template_path = self.context.template + + + def set_base_template_if_missing(self): + if not self.context.base_template_path: + app_base = frappe.get_hooks("base_template") + self.context.base_template_path = app_base[-1] if app_base else "templates/base.html" + + def set_title_with_prefix(self): + if (self.context.title_prefix and self.context.title + and not self.context.title.startswith(self.context.title_prefix)): + self.context.title = '{0} - {1}'.format(self.context.title_prefix, self.context.title) + + def update_website_context(self): + # apply context from hooks + update_website_context = frappe.get_hooks('update_website_context') + for method in update_website_context: + values = frappe.get_attr(method)(self.context) + if values: + self.context.update(values) + + def add_metatags(self): + self.tags = frappe._dict(self.context.get("metatags") or {}) + self.init_metatags_from_context() + self.set_opengraph_tags() + self.set_twitter_tags() + self.set_meta_published_on() + self.set_metatags_from_website_route_meta() + + self.context.metatags = self.tags + + def add_sidebar_and_breadcrumbs(self): + add_sidebar_and_breadcrumbs(self.context) + + def init_metatags_from_context(self): + for key in ('title', 'description', 'image', 'author', 'url', 'published_on'): + if not key in self.tags and self.context.get(key): + self.tags[key] = self.context[key] + + if not self.tags.get('title'): self.tags['title'] = self.context.get('name') + + if self.tags.get('image'): + self.tags['image'] = frappe.utils.get_url(self.tags['image']) + + self.tags["language"] = frappe.local.lang or "en" + + def set_opengraph_tags(self): + if "og:type" not in self.tags: + self.tags["og:type"] = "article" + + for key in ('title', 'description', 'image', 'author', 'url'): + if self.tags.get(key): + self.tags['og:' + key] = self.tags.get(key) + + def set_twitter_tags(self): + for key in ('title', 'description', 'image', 'author', 'url'): + if self.tags.get(key): + self.tags['twitter:' + key] = self.tags.get(key) + + if self.tags.get('image'): + self.tags['twitter:card'] = "summary_large_image" + else: + self.tags["twitter:card"] = "summary" + + def set_meta_published_on(self): + if "published_on" in self.tags: + self.tags["datePublished"] = self.tags["published_on"] + del self.tags["published_on"] + + def set_metatags_from_website_route_meta(self): + ''' + Get meta tags from Website Route meta + they can override the defaults set above + ''' + route = self.context.path + if route == '': + # homepage + route = frappe.db.get_single_value('Website Settings', 'home_page') + + route_exists = (route + and not route.endswith(('.js', '.css')) + and frappe.db.exists('Website Route Meta', route)) + + if route_exists: + website_route_meta = frappe.get_doc('Website Route Meta', route) + for meta_tag in website_route_meta.meta_tags: + d = meta_tag.get_meta_dict() + self.tags.update(d) diff --git a/frappe/website/page_controllers/document_page.py b/frappe/website/page_controllers/document_page.py new file mode 100644 index 0000000000..bb00d78c0c --- /dev/null +++ b/frappe/website/page_controllers/document_page.py @@ -0,0 +1,88 @@ +import frappe +from frappe import _ +from frappe.model.document import get_controller +from frappe.website.page_controllers.base_template_page import BaseTemplatePage +from frappe.website.render import build_response +from frappe.website.router import (get_doctypes_with_web_view, + get_page_info_from_web_page_with_dynamic_routes) + + +class DocumentPage(BaseTemplatePage): + def validate(self): + ''' + Find a document with matching `route` from all doctypes with `has_web_view`=1 + ''' + if self.search_in_doctypes_with_web_view(): + return True + + if self.search_web_page_dynamic_routes(): + return True + + return False + + def search_in_doctypes_with_web_view(self): + for doctype in get_doctypes_with_web_view(): + filters = dict(route=self.path) + meta = frappe.get_meta(doctype) + condition_field = self.get_condition_field(meta) + + if condition_field: + filters[condition_field] = 1 + + try: + self.docname = frappe.db.get_value(doctype, filters, 'name') + if self.docname: + self.doctype = doctype + self.meta = meta + return True + except Exception as e: + if not frappe.db.is_missing_column(e): raise e + + def search_web_page_dynamic_routes(self): + d = get_page_info_from_web_page_with_dynamic_routes(self.path) + if d: + self.doctype = 'Web Page' + self.docname = d.name + self.meta = frappe.get_meta(self.doctype) + return True + else: + return False + + def render(self): + self.doc = frappe.get_doc(self.doctype, self.docname) + self.init_context() + self.update_context() + self.post_process_context() + + html = frappe.get_template(self.context.template_path).render(self.context) + html = self.add_csrf_token(html) + + return build_response(self.path, html, self.http_status_code or 200, self.headers) + + def update_context(self): + self.context.doc = self.doc + self.context.update(self.context.doc.as_dict()) + self.context.update(self.context.doc.get_website_properties()) + + if not self.context.template_path: + self.context.template_path = self.context.doc.meta.get_web_template() + + if hasattr(self.doc, "get_context"): + ret = self.doc.get_context(self.context) + + if ret: + self.context.update(ret) + + for prop in ("no_cache", "sitemap"): + if not prop in self.context: + self.context[prop] = getattr(self.doc, prop, False) + + def get_condition_field(self, meta): + condition_field = None + if meta.is_published_field: + condition_field = meta.is_published_field + elif not meta.custom: + controller = get_controller(meta.doctype) + condition_field = controller.website.condition_field + + return condition_field diff --git a/frappe/website/page_controllers/list_page.py b/frappe/website/page_controllers/list_page.py new file mode 100644 index 0000000000..2cb6ed5f26 --- /dev/null +++ b/frappe/website/page_controllers/list_page.py @@ -0,0 +1,10 @@ +import frappe +from frappe.website.page_controllers.template_page import TemplatePage + +class ListPage(TemplatePage): + def validate(self): + if frappe.db.get_value('DocType', self.path): + frappe.local.form_dict.doctype = self.path + self.set_standard_path('list') + return True + return False diff --git a/frappe/website/page_controllers/not_permitted_page.py b/frappe/website/page_controllers/not_permitted_page.py new file mode 100644 index 0000000000..e0cd6bf970 --- /dev/null +++ b/frappe/website/page_controllers/not_permitted_page.py @@ -0,0 +1,15 @@ +import frappe +from frappe import _ +from frappe.website.page_controllers.template_page import TemplatePage + +class NotPermittedPage(TemplatePage): + def validate(self): + frappe.local.message_title = _("Not Permitted") + frappe.local.response['context'] = dict( + indicator_color = 'red', + primary_action = '/login', + primary_label = _('Login'), + fullpage=True + ) + self.set_standard_path('message') + return True diff --git a/frappe/website/page_controllers/print_page.py b/frappe/website/page_controllers/print_page.py new file mode 100644 index 0000000000..6daea7fcbc --- /dev/null +++ b/frappe/website/page_controllers/print_page.py @@ -0,0 +1,19 @@ +import frappe +from frappe.website.page_controllers.template_page import TemplatePage + +class PrintPage(TemplatePage): + ''' + default path returns a printable object (based on permission) + /Quotation/Q-0001 + ''' + def validate(self): + parts = self.path.split('/', 1) + if len(parts)==2: + if (frappe.db.get_value('DocType', parts[0]) + and frappe.db.get_value(parts[0], parts[1])): + frappe.form_dict.doctype = parts[0] + frappe.form_dict.name = parts[1] + self.set_standard_path('printview') + return True + + return False diff --git a/frappe/website/page_controllers/static_page.py b/frappe/website/page_controllers/static_page.py new file mode 100644 index 0000000000..4b57c3ec42 --- /dev/null +++ b/frappe/website/page_controllers/static_page.py @@ -0,0 +1,40 @@ +import mimetypes +import os + +from werkzeug.wrappers import Response +from werkzeug.wsgi import wrap_file + +import frappe +from frappe.website.page_controllers.web_page import WebPage + + +class StaticPage(WebPage): + def validate(self): + if ('.' not in self.path): + return False + extn = self.path.rsplit('.', 1)[-1] + if extn in ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json'): + return False + + if self.find_path_in_apps(): + return True + + return False + + def find_path_in_apps(self): + for app in frappe.get_installed_apps(): + file_path = frappe.get_app_path(app, 'www') + '/' + self.path + if os.path.exists(file_path): + self.file_path = file_path + return True + return False + + def render(self): + try: + f = open(self.file_path, 'rb') + except IOError: + raise frappe.exceptions.NotFound + + response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True) + response.mimetype = mimetypes.guess_type(self.file_path)[0] or 'application/octet-stream' + return response diff --git a/frappe/website/page_controllers/template_page.py b/frappe/website/page_controllers/template_page.py new file mode 100644 index 0000000000..46e6e6a57e --- /dev/null +++ b/frappe/website/page_controllers/template_page.py @@ -0,0 +1,276 @@ +import io +import os + +import frappe +from frappe import _ +from frappe.website.page_controllers.base_template_page import BaseTemplatePage +from frappe.website.render import build_response +from frappe.website.utils import (extract_comment_tag, extract_title, + get_next_link, get_toc) + + +class TemplatePage(BaseTemplatePage): + def validate(self): + for app in frappe.get_installed_apps(frappe_last=True): + if self.find_page_in_app(app): + return True + + def find_page_in_app(self, app): + ''' + Searches for file matching the path in the /www + and /templates/pages folders + ''' + app_path = frappe.get_app_path(app) + folders = get_start_folders() + + for dirname in folders: + search_path = os.path.join(app_path, dirname, self.path) + for p in self.get_index_path_options(search_path): + file_path = frappe.as_unicode(p) + if os.path.exists(file_path) and not os.path.isdir(file_path): + self.app = app + self.app_path = app_path + self.template_path = os.path.relpath(file_path, self.app_path) + return True + + def get_index_path_options(self, search_path): + return ( + search_path, + search_path + '.html', + search_path + '.md', + search_path + '/index.html', + search_path + '/index.md') + + def render(self): + return build_response(self.path, self.get_html(), self.http_status_code, self.headers) + + def get_html(self): + # context object should be separate from self for security + # because it will be accessed via the user defined template + self.init_context() + + self.set_pymodule() + self.setup_template() + self.update_context() + self.post_process_context() + html = self.render_template() + + html = self.update_toc(html) + html = self.add_csrf_token(html) + + return html + + def set_pymodule(self): + ''' + A template may have a python module with a `get_context` method along with it in the + same folder. Also the hyphens will be coverted to underscore for python module names. + This method sets the pymodule_name if it exists. + ''' + self.basepath = self.template_path.rsplit('.', 1)[0] + self.pymodule_name = None + + # replace - with _ in the internal modules names + self.pymodule_path = os.path.join(self.basepath.replace("-", "_") + ".py") + + if os.path.exists(os.path.join(self.app_path, self.pymodule_path)): + self.pymodule_name = self.app + "." + self.pymodule_path.replace(os.path.sep, ".")[:-3] + + def setup_template(self): + '''Setup template source, frontmatter and markdown conversion''' + self.source = self.get_raw_template() + + if self.template_path.endswith(('.md', '.html')): + self.extract_frontmatter() + + self.convert_from_markdown() + + if self.extends_template(): + self.context.base_template_path = self.context.base_template_path or 'templates/base.html' + + + # TODO: setup index.txt ? + + def update_context(self): + self.set_page_properties() + self.set_properties_from_source() + self.load_colocated_files() + self.context.build_version = frappe.utils.get_build_version() + + if self.pymodule_name: + self.pymodule = frappe.get_module(self.pymodule_name) + self.set_pymodule_properties() + + data = self.run_pymodule_method('get_context') + + # some methods may return a "context" object + if data: self.context.update(data) + + # TODO: self.context.children = self.run_pymodule_method('get_children') + + self.context.developer_mode = frappe.conf.developer_mode + if self.context.http_status_code: + self.http_status_code = self.context.http_status_code + + def set_pymodule_properties(self): + for prop in ("base_template_path", "template", "no_cache", "sitemap", + "condition_field"): + if hasattr(self.pymodule, prop): + self.context[prop] = getattr(self.pymodule, prop) + + def set_page_properties(self): + self.context.template = self.template_path + self.context.base_template = self.context.base_template or 'templates/web.html' + + def set_properties_from_source(self): + if not self.source: + return + context = self.context + if not context.title: + context.title = extract_title(self.source, self.path) + + base_template = extract_comment_tag(self.source, 'base_template') + if base_template: + context.base_template = base_template + + if (context.base_template + and "{%- extends" not in self.source + and "{% extends" not in self.source + and "" not in self.source): + self.source = '''{{% extends "{0}" %}} + {{% block page_content %}}{1}{{% endblock %}}'''.format(context.base_template, self.source) + + if "" in self.source: + context.no_breadcrumbs = 1 + + if "" in self.source: + context.show_sidebar = 1 + + if "" in self.source: + context.add_breadcrumbs = 1 + + if "" in self.source: + context.no_header = 1 + + if "" in self.source: + context.add_next_prev_links = 1 + + if "" in self.source: + context.no_cache = 1 + + if "" in self.source: + context.sitemap = 0 + + if "" in self.source: + context.sitemap = 1 + + def run_pymodule_method(self, method): + if hasattr(self.pymodule, method): + try: + return getattr(self.pymodule, method)(self.context) + except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect): + raise + except: + if not frappe.flags.in_migrate: + frappe.errprint(frappe.utils.get_traceback()) + + def render_template(self): + if self.source: + html = frappe.render_template(self.source, self.context) + elif self.template_path: + if self.path.endswith('min.js'): + html = self.get_raw_template() # static + else: + html = frappe.get_template(self.template_path).render(self.context) + + return html + + def extends_template(self): + return (self.template_path.endswith(('.html', '.md', )) + and ('{%- extends' in self.source + or '{% extends' in self.source)) + + def get_raw_template(self): + return frappe.get_jloader().get_source(frappe.get_jenv(), self.template_path)[0] + + def load_colocated_files(self): + '''load co-located css/js files with the same name''' + js_path = self.basepath + '.js' + if os.path.exists(js_path) and '{% block script %}' not in self.source: + self.context.colocated_js = self.get_colocated_file(js_path) + + css_path = self.basepath + '.css' + if os.path.exists(css_path) and '{% block style %}' not in self.source: + self.context.colocated_css = self.get_colocated_file(css_path) + + def get_colocated_file(self, path): + with io.open(path, 'r', encoding = 'utf-8') as f: + return f.read() + + def extract_frontmatter(self): + try: + # values will be used to update self + res = get_frontmatter(self.source) + if res['attributes']: + self.context.update(res['attributes']) + self.source = res['body'] + except Exception: + pass + + def convert_from_markdown(self): + if self.template_path.endswith('.md'): + self.source = frappe.utils.md_to_html(self.source) + self.page_toc_html = self.source.toc_html + + if not self.context.show_sidebar: + self.source = '
' + self.source + '
' + + def update_toc(self, html): + if '{index}' in html: + html = html.replace('{index}', get_toc(self.path)) + + if '{next}' in html: + html = html.replace('{next}', get_next_link(self.path)) + + return html + + def set_standard_path(self, path): + self.app = 'frappe' + self.app_path = frappe.get_app_path('frappe') + self.path = path + self.template_path = 'www/{path}.html'.format(path=path) + + def set_missing_values(self): + if not "url_prefix" in self.context: + self.context.url_prefix = "" + + if self.context.url_prefix and self.context.url_prefix[-1]!='/': + self.context.url_prefix += '/' + + # for backward compatibility + self.context.docs_base_url = '/docs' + self.context.path = self.path + + +def get_start_folders(): + return frappe.local.flags.web_pages_folders or ('www', 'templates/pages') + +def get_frontmatter(string): + """ + Reference: https://github.com/jonbeebe/frontmatter + """ + import re + + 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, + } diff --git a/frappe/website/page_controllers/web_form.py b/frappe/website/page_controllers/web_form.py new file mode 100644 index 0000000000..030e8bb3bc --- /dev/null +++ b/frappe/website/page_controllers/web_form.py @@ -0,0 +1,4 @@ +from frappe.website.page_controllers.web_page import WebPage + +class WebFormPage(WebPage): + pass diff --git a/frappe/website/page_controllers/web_page.py b/frappe/website/page_controllers/web_page.py new file mode 100644 index 0000000000..d4af2860f8 --- /dev/null +++ b/frappe/website/page_controllers/web_page.py @@ -0,0 +1,21 @@ +import frappe +from frappe import _ + +class WebPage(object): + def __init__(self, path=None, http_status_code=None): + self.headers = None + self.http_status_code = http_status_code or 200 + if not path: + path = frappe.local.request.path + self.path = path.strip('/ ') + + def get(self): + if self.validate(): + return self.render() + + def validate(self): + pass + + def render(self): + pass + diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 11e950573e..10eddfc641 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -1,22 +1,16 @@ -import mimetypes -import os - -from werkzeug.wrappers import Response -from werkzeug.wsgi import wrap_file - import frappe from frappe import _ -from frappe.utils import cint, cstr -from frappe.model.document import get_controller -from frappe.website.doctype.website_settings.website_settings import \ - get_website_settings +from frappe.utils import cstr + +from frappe.website.page_controllers.document_page import DocumentPage +from frappe.website.page_controllers.list_page import ListPage +from frappe.website.page_controllers.not_permitted_page import NotPermittedPage +from frappe.website.page_controllers.print_page import PrintPage +from frappe.website.page_controllers.template_page import TemplatePage +from frappe.website.page_controllers.static_page import StaticPage + from frappe.website.redirect import resolve_redirect from frappe.website.render import build_response, resolve_path -from frappe.website.router import (get_doctypes_with_web_view, - get_page_info_from_web_page_with_dynamic_routes, get_start_folders) -from frappe.website.utils import (can_cache, delete_page_cache, get_home_page, - get_next_link, get_toc) - def get_response(path=None, http_status_code=200): """render html page""" @@ -56,475 +50,3 @@ def get_response(path=None, http_status_code=200): response = TemplatePage('error', getattr(e, 'http_status_code', None) or http_status_code).get() return response - -class WebPage(object): - def __init__(self, path=None, http_status_code=None): - self.headers = None - self.http_status_code = http_status_code or 200 - if not path: - path = frappe.local.request.path - self.path = path.strip('/ ') - - def get(self): - if self.validate(): - return self.render() - - def validate(self): - pass - - def render(self): - pass - -class StaticPage(WebPage): - def validate(self): - if ('.' not in self.path): - return False - extn = self.path.rsplit('.', 1)[-1] - if extn in ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json'): - return False - - if self.find_path_in_apps(): - return True - - return False - - def find_path_in_apps(self): - for app in frappe.get_installed_apps(): - file_path = frappe.get_app_path(app, 'www') + '/' + self.path - if os.path.exists(file_path): - self.file_path = file_path - return True - return False - - def render(self): - try: - f = open(self.file_path, 'rb') - except IOError: - raise NotFound - - response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True) - response.mimetype = mimetypes.guess_type(self.file_path)[0] or 'application/octet-stream' - return response - -class BaseTemplatePage(WebPage): - def init_context(self): - self.context = frappe._dict() - self.context.update(get_website_settings(self.context)) - self.context.update(frappe.local.conf.get("website_context") or {}) - - def add_csrf_token(self, html): - if frappe.local.session: - return html.replace("", ''.format( - frappe.local.session.data.csrf_token)) - else: - return html - - def post_process_context(self): - self.add_metatags() - # add_sidebar_and_breadcrumbs(context) - - self.set_base_template_if_missing() - self.set_title_with_prefix() - self.update_website_context() - - # set using frappe.respond_as_web_page - if hasattr(frappe.local, 'response') and frappe.local.response.get('context'): - self.context.update(frappe.local.response.context) - - # to be able to inspect the context dict - # Use the macro "inspect" from macros.html - self.context._context_dict = self.context - - # context sends us a new template path - if self.context.template: - self.template_path = self.context.template - - - def set_base_template_if_missing(self): - if not self.context.base_template_path: - app_base = frappe.get_hooks("base_template") - self.context.base_template_path = app_base[-1] if app_base else "templates/base.html" - - def set_title_with_prefix(self): - if (self.context.title_prefix and self.context.title - and not self.context.title.startswith(self.context.title_prefix)): - self.context.title = '{0} - {1}'.format(self.context.title_prefix, self.context.title) - - def update_website_context(self): - # apply context from hooks - update_website_context = frappe.get_hooks('update_website_context') - for method in update_website_context: - values = frappe.get_attr(method)(self.context) - if values: - self.context.update(values) - - def add_metatags(self): - self.tags = frappe._dict(self.context.get("metatags") or {}) - self.init_metatags_from_context() - self.set_opengraph_tags() - self.set_twitter_tags() - self.set_meta_published_on() - self.set_metatags_from_website_route_meta() - - self.context.metatags = self.tags - - def init_metatags_from_context(self): - for key in ('title', 'description', 'image', 'author', 'url', 'published_on'): - if not key in self.tags and self.context.get(key): - self.tags[key] = self.context[key] - - if not self.tags.get('title'): self.tags['title'] = self.context.get('name') - - if self.tags.get('image'): - self.tags['image'] = frappe.utils.get_url(self.tags['image']) - - self.tags["language"] = frappe.local.lang or "en" - - def set_opengraph_tags(self): - if "og:type" not in self.tags: - self.tags["og:type"] = "article" - - for key in ('title', 'description', 'image', 'author', 'url'): - if self.tags.get(key): - self.tags['og:' + key] = self.tags.get(key) - - def set_twitter_tags(self): - for key in ('title', 'description', 'image', 'author', 'url'): - if self.tags.get(key): - self.tags['twitter:' + key] = self.tags.get(key) - - if self.tags.get('image'): - self.tags['twitter:card'] = "summary_large_image" - else: - self.tags["twitter:card"] = "summary" - - def set_meta_published_on(self): - if "published_on" in self.tags: - self.tags["datePublished"] = self.tags["published_on"] - del self.tags["published_on"] - - def set_metatags_from_website_route_meta(self): - ''' - Get meta tags from Website Route meta - they can override the defaults set above - ''' - route = self.context.path - if route == '': - # homepage - route = frappe.db.get_single_value('Website Settings', 'home_page') - - route_exists = (route - and not route.endswith(('.js', '.css')) - and frappe.db.exists('Website Route Meta', route)) - - if route_exists: - website_route_meta = frappe.get_doc('Website Route Meta', route) - for meta_tag in website_route_meta.meta_tags: - d = meta_tag.get_meta_dict() - self.tags.update(d) - -class TemplatePage(BaseTemplatePage): - def validate(self): - for app in frappe.get_installed_apps(frappe_last=True): - if self.find_page_in_app(app): - return True - - def find_page_in_app(self, app): - ''' - Searches for file matching the path in the /www - and /templates/pages folders - ''' - app_path = frappe.get_app_path(app) - folders = get_start_folders() - - for dirname in folders: - search_path = os.path.join(app_path, dirname, self.path) - for p in self.get_index_path_options(search_path): - file_path = frappe.as_unicode(p) - if os.path.exists(file_path) and not os.path.isdir(file_path): - self.app = app - self.app_path = app_path - self.template_path = os.path.relpath(file_path, self.app_path) - return True - - def get_index_path_options(self, search_path): - return ( - search_path, - search_path + '.html', - search_path + '.md', - search_path + '/index.html', - search_path + '/index.md') - - def render(self): - return build_response(self.path, self.get_html(), self.http_status_code, self.headers) - - def get_html(self): - # context object should be separate from self for security - # because it will be accessed via the user defined template - self.init_context() - - self.set_pymodule() - self.update_context() - self.post_process_context() - self.setup_template() - html = self.render_template() - - html = self.update_toc(html) - html = self.add_csrf_token(html) - - return html - - def set_pymodule(self): - ''' - A template may have a python module with a `get_context` method along with it in the - same folder. Also the hyphens will be coverted to underscore for python module names. - This method sets the pymodule_name if it exists. - ''' - self.basepath = self.template_path.rsplit('.', 1)[0] - self.pymodule_name = None - - # replace - with _ in the internal modules names - self.pymodule_path = os.path.join(self.basepath.replace("-", "_") + ".py") - - if os.path.exists(os.path.join(self.app_path, self.pymodule_path)): - self.pymodule_name = self.app + "." + self.pymodule_path.replace(os.path.sep, ".")[:-3] - - def setup_template(self): - '''Setup template source, frontmatter and markdown conversion''' - self.source = self.get_raw_template() - - if self.template_path.endswith(('.md', '.html')): - self.extract_frontmatter() - - self.convert_from_markdown() - - if self.extends_template(): - self.context.base_template_path = self.context.base_template_path or 'templates/base.html' - else: - self.source = None # clear the source - - # TODO: setup index.txt ? - - def update_context(self): - self.set_page_properties() - - if self.pymodule_name: - self.pymodule = frappe.get_module(self.pymodule_name) - self.set_pymodule_properties() - - data = self.run_pymodule_method('get_context') - - # some methods may return a "context" object - if data: self.context.update(data) - - # TODO: self.context.children = self.run_pymodule_method('get_children') - - self.context.developer_mode = frappe.conf.developer_mode - if self.context.http_status_code: - self.http_status_code = self.context.http_status_code - - def set_pymodule_properties(self): - for prop in ("base_template_path", "template", "no_cache", "sitemap", - "condition_field"): - if hasattr(self.pymodule, prop): - self.context[prop] = getattr(self.pymodule, prop) - - def set_page_properties(self): - self.context.template = self.template_path - - def run_pymodule_method(self, method): - if hasattr(self.pymodule, method): - try: - return getattr(self.pymodule, method)(self.context) - except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect): - raise - except: - if not frappe.flags.in_migrate: - frappe.errprint(frappe.utils.get_traceback()) - - def render_template(self): - if self.source: - html = frappe.render_template(self.source, self.context) - elif self.template_path: - if self.path.endswith('min.js'): - html = self.get_raw_template() # static - else: - html = frappe.get_template(self.template_path).render(self.context) - - return html - - def extends_template(self): - return (self.template_path.endswith(('.html', '.md', )) - and ('{%- extends' in self.source - or '{% extends' in self.source)) - - def get_raw_template(self): - return frappe.get_jloader().get_source(frappe.get_jenv(), self.template_path)[0] - - def load_colocated_files(self): - '''load co-located css/js files with the same name''' - js_path = self.basepath + '.js' - if os.path.exists(js_path) and '{% block script %}' not in self.source: - self.colocated_js = self.get_colocated_file(js_path) - - css_path = self.basepath + '.css' - if os.path.exists(css_path) and '{% block style %}' not in self.source: - self.colocated_css = self.get_colocated_file(css_path) - - def get_colocated_file(self, path): - with io.open(path, 'r', encoding = 'utf-8') as f: - return f.read() - - def extract_frontmatter(self): - try: - # values will be used to update page_info - res = get_frontmatter(self.source) - if res['attributes']: - self.context.update(res['attributes']) - self.source = res['body'] - except Exception: - pass - - def convert_from_markdown(self): - if self.template_path.endswith('.md'): - self.source = frappe.utils.md_to_html(self.source) - self.page_toc_html = self.toc_html - - if not self.show_sidebar: - self.source = '
' + self.source + '
' - - def update_toc(self, html): - if '{index}' in html: - html = html.replace('{index}', get_toc(self.path)) - - if '{next}' in html: - html = html.replace('{next}', get_next_link(self.path)) - - return html - - def set_standard_path(self, path): - self.app = 'frappe' - self.app_path = frappe.get_app_path('frappe') - self.path = path - self.template_path = 'www/{path}.html'.format(path=path) - - -class ListPage(TemplatePage): - def validate(self): - if frappe.db.get_value('DocType', self.path): - frappe.local.form_dict.doctype = self.path - self.set_standard_path('list') - return True - return False - -class PrintPage(TemplatePage): - ''' - default path returns a printable object (based on permission) - /Quotation/Q-0001 - ''' - def validate(self): - parts = self.path.split('/', 1) - if len(parts)==2: - if (frappe.db.get_value('DocType', parts[0]) - and frappe.db.get_value(parts[0], parts[1])): - frappe.form_dict.doctype = parts[0] - frappe.form_dict.name = parts[1] - self.set_standard_path('printview') - return True - - return False - -class NotPermittedPage(TemplatePage): - def validate(self): - frappe.local.message_title = _("Not Permitted") - frappe.local.response['context'] = dict( - indicator_color = 'red', - primary_action = '/login', - primary_label = _('Login'), - fullpage=True - ) - self.set_standard_path('message') - return True - -class DocumentPage(BaseTemplatePage): - def validate(self): - ''' - Find a document with matching `route` from all doctypes with `has_web_view`=1 - ''' - if self.search_in_doctypes_with_web_view(): - return True - - if self.search_web_page_dynamic_routes(): - return True - - return False - - def search_in_doctypes_with_web_view(self): - for doctype in get_doctypes_with_web_view(): - filters = dict(route=self.path) - meta = frappe.get_meta(doctype) - condition_field = self.get_condition_field(meta) - - if condition_field: - filters[condition_field] = 1 - - try: - self.docname = frappe.db.get_value(doctype, filters, 'name') - if self.docname: - self.doctype = doctype - self.meta = meta - return True - except Exception as e: - if not frappe.db.is_missing_column(e): raise e - - def search_web_page_dynamic_routes(self): - d = get_page_info_from_web_page_with_dynamic_routes(self.path) - if d: - self.doctype = 'Web Page' - self.docname = d.name - self.meta = frappe.get_meta(self.doctype) - return True - else: - return False - - def render(self): - self.doc = frappe.get_doc(self.doctype, self.docname) - self.init_context() - self.update_context() - self.post_process_context() - - html = frappe.get_template(self.context.template_path).render(self.context) - html = self.add_csrf_token(html) - - return build_response(self.path, html, self.http_status_code or 200, self.headers) - - def update_context(self): - self.context.doc = self.doc - self.context.update(self.context.doc.as_dict()) - self.context.update(self.context.doc.get_website_properties()) - - if not self.context.template_path: - self.context.template_path = self.context.doc.meta.get_web_template() - - if hasattr(self.doc, "get_context"): - ret = self.doc.get_context(self.context) - - if ret: - self.context.update(ret) - - for prop in ("no_cache", "sitemap"): - if not prop in self.context: - self.context[prop] = getattr(self.doc, prop, False) - - def get_condition_field(self, meta): - condition_field = None - if meta.is_published_field: - condition_field = meta.is_published_field - elif not meta.custom: - controller = get_controller(meta.doctype) - condition_field = controller.website.condition_field - - return condition_field - -class WebFormPage(WebPage): - pass From ce3771ab74ab739d18960c50e1dd3826f5ffb4b1 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 12 Mar 2021 13:26:38 +0530 Subject: [PATCH 0010/1519] refactor: Move code to appropriate file add_sidebar_and_breadcrumbs belongs to TemplatePage --- .../page_controllers/base_template_page.py | 12 +++--------- frappe/website/page_controllers/template_page.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/frappe/website/page_controllers/base_template_page.py b/frappe/website/page_controllers/base_template_page.py index 10e2375356..8a5ec992cc 100644 --- a/frappe/website/page_controllers/base_template_page.py +++ b/frappe/website/page_controllers/base_template_page.py @@ -1,6 +1,7 @@ +import os + import frappe from frappe import _ -from frappe.website.context import add_sidebar_and_breadcrumbs from frappe.website.doctype.website_settings.website_settings import \ get_website_settings from frappe.website.page_controllers.web_page import WebPage @@ -15,15 +16,12 @@ class BaseTemplatePage(WebPage): def add_csrf_token(self, html): if frappe.local.session: return html.replace("", ''.format( - frappe.local.session.data.csrf_token)) + frappe.local.session.data.csrf_token)) else: return html def post_process_context(self): - self.set_missing_values() self.add_metatags() - self.add_sidebar_and_breadcrumbs() - self.set_base_template_if_missing() self.set_title_with_prefix() self.update_website_context() @@ -40,7 +38,6 @@ class BaseTemplatePage(WebPage): if self.context.template: self.template_path = self.context.template - def set_base_template_if_missing(self): if not self.context.base_template_path: app_base = frappe.get_hooks("base_template") @@ -69,9 +66,6 @@ class BaseTemplatePage(WebPage): self.context.metatags = self.tags - def add_sidebar_and_breadcrumbs(self): - add_sidebar_and_breadcrumbs(self.context) - def init_metatags_from_context(self): for key in ('title', 'description', 'image', 'author', 'url', 'published_on'): if not key in self.tags and self.context.get(key): diff --git a/frappe/website/page_controllers/template_page.py b/frappe/website/page_controllers/template_page.py index 46e6e6a57e..cfb2d2f310 100644 --- a/frappe/website/page_controllers/template_page.py +++ b/frappe/website/page_controllers/template_page.py @@ -4,6 +4,7 @@ import os import frappe from frappe import _ from frappe.website.page_controllers.base_template_page import BaseTemplatePage +from frappe.website.context import add_sidebar_and_breadcrumbs from frappe.website.render import build_response from frappe.website.utils import (extract_comment_tag, extract_title, get_next_link, get_toc) @@ -60,6 +61,14 @@ class TemplatePage(BaseTemplatePage): return html + def post_process_context(self): + self.add_sidebar_and_breadcrumbs() + self.set_missing_values() + super(TemplatePage, self).post_process_context() + + def add_sidebar_and_breadcrumbs(self): + add_sidebar_and_breadcrumbs(self.context) + def set_pymodule(self): ''' A template may have a python module with a `get_context` method along with it in the @@ -85,7 +94,7 @@ class TemplatePage(BaseTemplatePage): self.convert_from_markdown() if self.extends_template(): - self.context.base_template_path = self.context.base_template_path or 'templates/base.html' + self.context.base_template_path = self.context.base_template_path # TODO: setup index.txt ? @@ -246,9 +255,11 @@ class TemplatePage(BaseTemplatePage): if self.context.url_prefix and self.context.url_prefix[-1]!='/': self.context.url_prefix += '/' + self.context.path = self.path + self.context.pathname = frappe.local.path + # for backward compatibility self.context.docs_base_url = '/docs' - self.context.path = self.path def get_start_folders(): From c6cc70e7dfa2dddcde55acae1ffc21c3ba169a45 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 15 Mar 2021 10:13:40 +0530 Subject: [PATCH 0011/1519] fix: Document Page --- frappe/website/page_controllers/document_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/page_controllers/document_page.py b/frappe/website/page_controllers/document_page.py index bb00d78c0c..f13ea0c724 100644 --- a/frappe/website/page_controllers/document_page.py +++ b/frappe/website/page_controllers/document_page.py @@ -82,7 +82,7 @@ class DocumentPage(BaseTemplatePage): if meta.is_published_field: condition_field = meta.is_published_field elif not meta.custom: - controller = get_controller(meta.doctype) + controller = get_controller(meta.name) condition_field = controller.website.condition_field return condition_field From a5f274b5a3dbb72e109184b7a3c56b2a1dac13b4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 15 Mar 2021 10:15:27 +0530 Subject: [PATCH 0012/1519] fix: Enable WebForm page --- frappe/website/serve.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 10eddfc641..ea27b07859 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -8,6 +8,7 @@ from frappe.website.page_controllers.not_permitted_page import NotPermittedPage from frappe.website.page_controllers.print_page import PrintPage from frappe.website.page_controllers.template_page import TemplatePage from frappe.website.page_controllers.static_page import StaticPage +from frappe.website.page_controllers.web_form import WebFormPage from frappe.website.redirect import resolve_redirect from frappe.website.render import build_response, resolve_path @@ -23,7 +24,6 @@ def get_response(path=None, http_status_code=200): path = path.strip('/ ') resolve_redirect(path, query_string) path = resolve_path(path) - data = None # there is no way to determine the type of the page based on the route # so evaluate each type of page sequentially @@ -32,6 +32,8 @@ def get_response(path=None, http_status_code=200): response = TemplatePage(path, http_status_code).get() if not response: response = ListPage(path, http_status_code).get() + if not response: + response = WebFormPage(path, http_status_code).get() if not response: response = DocumentPage(path, http_status_code).get() if not response: From 25a4eb07575094766111c5cca9b46f79dd1ed59f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 29 Mar 2021 19:25:41 +0200 Subject: [PATCH 0013/1519] feat: google drive picker --- .../google_settings/google_settings.json | 12 ++- .../google_settings/test_google_settings.py | 10 +++ frappe/public/icons/social/google_drive.svg | 1 + .../js/frappe/file_uploader/FileUploader.vue | 46 ++++++++++- frappe/public/js/integrations/google_drive.js | 81 +++++++++++++++++++ 5 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 frappe/integrations/doctype/google_settings/test_google_settings.py create mode 100644 frappe/public/icons/social/google_drive.svg create mode 100644 frappe/public/js/integrations/google_drive.js diff --git a/frappe/integrations/doctype/google_settings/google_settings.json b/frappe/integrations/doctype/google_settings/google_settings.json index 086c56c020..6a4f181f2d 100644 --- a/frappe/integrations/doctype/google_settings/google_settings.json +++ b/frappe/integrations/doctype/google_settings/google_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-06-14 00:08:37.255003", "doctype": "DocType", "engine": "InnoDB", @@ -8,7 +9,8 @@ "client_id", "client_secret", "sb_01", - "api_key" + "api_key", + "app_id" ], "fields": [ { @@ -46,10 +48,16 @@ "fieldname": "sb_01", "fieldtype": "Section Break", "label": "API Key" + }, + { + "fieldname": "app_id", + "fieldtype": "Data", + "label": "App ID" } ], "issingle": 1, - "modified": "2019-08-06 22:37:41.699703", + "links": [], + "modified": "2021-03-28 22:24:05.963403", "modified_by": "Administrator", "module": "Integrations", "name": "Google Settings", diff --git a/frappe/integrations/doctype/google_settings/test_google_settings.py b/frappe/integrations/doctype/google_settings/test_google_settings.py new file mode 100644 index 0000000000..476e772b58 --- /dev/null +++ b/frappe/integrations/doctype/google_settings/test_google_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestGoogleSettings(unittest.TestCase): + pass diff --git a/frappe/public/icons/social/google_drive.svg b/frappe/public/icons/social/google_drive.svg new file mode 100644 index 0000000000..d43b4d3dbd --- /dev/null +++ b/frappe/public/icons/social/google_drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index d0b09c7593..61b87606a5 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -63,6 +63,12 @@
{{ __('Camera') }}
+
{{ upload_notes }} @@ -116,6 +122,7 @@ import FilePreview from './FilePreview.vue'; import FileBrowser from './FileBrowser.vue'; import WebLink from './WebLink.vue'; +import GoogleDrive from '../../integrations/google_drive'; export default { name: 'FileUploader', @@ -173,8 +180,16 @@ export default { currently_uploading: -1, show_file_browser: false, show_web_link: false, + allow_take_photo: false, + allow_google_drive: false } }, + created() { + this.allow_take_photo = window.navigator.mediaDevices; + frappe.db.get_single_value("Google Settings", "enable").then(resp => { + this.allow_google_drive = Boolean(resp); + }); + }, watch: { files(newvalue, oldvalue) { if (!this.allow_multiple && newvalue.length > 1) { @@ -187,9 +202,6 @@ export default { return this.files.length > 0 && this.files.every( file => file.total !== 0 && file.progress === file.total); - }, - allow_take_photo() { - return window.navigator.mediaDevices; } }, methods: { @@ -408,6 +420,10 @@ export default { form_data.append('file_url', file.file_url); } + if (file.file_name) { + form_data.append('file_name', file.file_name); + } + if (this.doctype && this.docname) { form_data.append('doctype', this.doctype); form_data.append('docname', this.docname); @@ -437,6 +453,30 @@ export default { ); }); }, + show_google_drive_picker() { + frappe.db.get_value("Google Settings", "Google Settings", ["client_id", "api_key", "app_id"]).then(resp => { + let dialog = cur_dialog; + dialog.hide(); + let google_drive = new GoogleDrive({ + pickerCallback: data => this.google_drive_callback(data, dialog), + developerKey: resp.message.api_key, + clientId: resp.message.client_id, + appId: resp.message.app_id + }); + google_drive.loadPicker(); + }); + }, + google_drive_callback(data, dialog) { + if (data.action == google.picker.Action.PICKED) { + // debugger; + this.upload_file({ + file_url: data.docs[0].url, + file_name: data.docs[0].name + }); + } else if (data.action == google.picker.Action.CANCEL) { + dialog.show(); + } + }, url_to_file(url, filename, mime_type) { return fetch(url) .then(res => res.arrayBuffer()) diff --git a/frappe/public/js/integrations/google_drive.js b/frappe/public/js/integrations/google_drive.js new file mode 100644 index 0000000000..9c6aea5375 --- /dev/null +++ b/frappe/public/js/integrations/google_drive.js @@ -0,0 +1,81 @@ +export default class GoogleDrive { + constructor({ + pickerCallback, + developerKey, + clientId, + appId + } = {}) { + console.log('GoogleDrive constructor'); + this.pickerCallback = pickerCallback; + this.pickerApiLoaded = false; + this.scope = ['https://www.googleapis.com/auth/drive.file']; + this.developerKey = developerKey; + this.clientId = clientId; + this.appId = appId; + } + + loadPicker() { + // load the google API library + $.ajax({ + method: "GET", + url: "https://apis.google.com/js/api.js", + dataType: "script", + cache: true, + context: this + }).done(function() { + this.loadGapi(); + }.bind(this)); + } + + loadGapi() { + // load auth and picker libraries + if (!frappe.boot.user.google_drive_token) { + gapi.load('auth', function() { + console.log('gapi.load("auth") callback'); + this.onAuthApiLoad(); + }.bind(this)); + } + + gapi.load('picker', function() { + console.log('gapi.load("picker") callback'); + this.onPickerApiLoad(); + }.bind(this)); + } + + onAuthApiLoad() { + gapi.auth.authorize({ + 'client_id': this.clientId, + 'scope': this.scope, + 'immediate': false + }, function(authResult) { + this.handleAuthResult(authResult); + }.bind(this)); + } + + handleAuthResult(authResult) { + if (authResult && !authResult.error) { + frappe.boot.user.google_drive_token = authResult.access_token; + this.createPicker(); + } + } + + onPickerApiLoad() { + this.pickerApiLoaded = true; + this.createPicker(); + } + + createPicker() { + // Create and render a Picker object for searching images. + if (this.pickerApiLoaded && frappe.boot.user.google_drive_token) { + var view = new google.picker.View(google.picker.ViewId.DOCS); + var picker = new google.picker.PickerBuilder() + .setAppId(this.appId) + .setOAuthToken(frappe.boot.user.google_drive_token) + .addView(view) + .setDeveloperKey(this.developerKey) + .setCallback(this.pickerCallback) + .build(); + picker.setVisible(true); + } + } +} From 7d333dbf703001c50dc80aa0a28477b741e9b51f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 29 Mar 2021 21:09:06 +0200 Subject: [PATCH 0014/1519] fix: scope for google picker --- frappe/public/js/integrations/google_drive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/integrations/google_drive.js b/frappe/public/js/integrations/google_drive.js index 9c6aea5375..1c7e4dda21 100644 --- a/frappe/public/js/integrations/google_drive.js +++ b/frappe/public/js/integrations/google_drive.js @@ -8,7 +8,7 @@ export default class GoogleDrive { console.log('GoogleDrive constructor'); this.pickerCallback = pickerCallback; this.pickerApiLoaded = false; - this.scope = ['https://www.googleapis.com/auth/drive.file']; + this.scope = ['https://www.googleapis.com/auth/drive.readonly']; this.developerKey = developerKey; this.clientId = clientId; this.appId = appId; From c44cecae5db1ef7231b742ca84ade406224e17d6 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 29 Mar 2021 21:11:26 +0200 Subject: [PATCH 0015/1519] fix: remove log statements --- frappe/public/js/integrations/google_drive.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frappe/public/js/integrations/google_drive.js b/frappe/public/js/integrations/google_drive.js index 1c7e4dda21..e7093fb29a 100644 --- a/frappe/public/js/integrations/google_drive.js +++ b/frappe/public/js/integrations/google_drive.js @@ -5,7 +5,6 @@ export default class GoogleDrive { clientId, appId } = {}) { - console.log('GoogleDrive constructor'); this.pickerCallback = pickerCallback; this.pickerApiLoaded = false; this.scope = ['https://www.googleapis.com/auth/drive.readonly']; @@ -31,13 +30,11 @@ export default class GoogleDrive { // load auth and picker libraries if (!frappe.boot.user.google_drive_token) { gapi.load('auth', function() { - console.log('gapi.load("auth") callback'); this.onAuthApiLoad(); }.bind(this)); } gapi.load('picker', function() { - console.log('gapi.load("picker") callback'); this.onPickerApiLoad(); }.bind(this)); } From f9dbbf69e0e89e3f36ac821994cfccb6a576f32d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 29 Mar 2021 21:22:13 +0200 Subject: [PATCH 0016/1519] feat: better home view for google drive picker --- frappe/public/js/integrations/google_drive.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/integrations/google_drive.js b/frappe/public/js/integrations/google_drive.js index e7093fb29a..6b9ee9d0ff 100644 --- a/frappe/public/js/integrations/google_drive.js +++ b/frappe/public/js/integrations/google_drive.js @@ -64,7 +64,10 @@ export default class GoogleDrive { createPicker() { // Create and render a Picker object for searching images. if (this.pickerApiLoaded && frappe.boot.user.google_drive_token) { - var view = new google.picker.View(google.picker.ViewId.DOCS); + var view = new google.picker.DocsView(google.picker.ViewId.DOCS) + .setParent('root') // show the root folder by default + .setIncludeFolders(true); // also show folders, not just files + var picker = new google.picker.PickerBuilder() .setAppId(this.appId) .setOAuthToken(frappe.boot.user.google_drive_token) From 11f4edf051bf9842e58228faae2d2a349d2e763d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 29 Mar 2021 21:22:37 +0200 Subject: [PATCH 0017/1519] feat: localize google drive picker --- frappe/public/js/integrations/google_drive.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/integrations/google_drive.js b/frappe/public/js/integrations/google_drive.js index 6b9ee9d0ff..076178046e 100644 --- a/frappe/public/js/integrations/google_drive.js +++ b/frappe/public/js/integrations/google_drive.js @@ -70,11 +70,13 @@ export default class GoogleDrive { var picker = new google.picker.PickerBuilder() .setAppId(this.appId) + .setDeveloperKey(this.developerKey) .setOAuthToken(frappe.boot.user.google_drive_token) .addView(view) - .setDeveloperKey(this.developerKey) + .setLocale(frappe.boot.lang) .setCallback(this.pickerCallback) .build(); + picker.setVisible(true); } } From 741011085b6ba3cccc86ba3514d9deff4eb82b54 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 30 Mar 2021 00:49:02 +0200 Subject: [PATCH 0018/1519] refactor: bind this --- frappe/public/js/integrations/google_drive.js | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/frappe/public/js/integrations/google_drive.js b/frappe/public/js/integrations/google_drive.js index 076178046e..06d9cbbe54 100644 --- a/frappe/public/js/integrations/google_drive.js +++ b/frappe/public/js/integrations/google_drive.js @@ -5,9 +5,9 @@ export default class GoogleDrive { clientId, appId } = {}) { - this.pickerCallback = pickerCallback; - this.pickerApiLoaded = false; this.scope = ['https://www.googleapis.com/auth/drive.readonly']; + this.pickerApiLoaded = false; + this.pickerCallback = pickerCallback; this.developerKey = developerKey; this.clientId = clientId; this.appId = appId; @@ -19,24 +19,17 @@ export default class GoogleDrive { method: "GET", url: "https://apis.google.com/js/api.js", dataType: "script", - cache: true, - context: this - }).done(function() { - this.loadGapi(); - }.bind(this)); + cache: true + }).done(this.loadGapi.bind(this)); } loadGapi() { // load auth and picker libraries if (!frappe.boot.user.google_drive_token) { - gapi.load('auth', function() { - this.onAuthApiLoad(); - }.bind(this)); + gapi.load('auth', this.onAuthApiLoad.bind(this)); } - gapi.load('picker', function() { - this.onPickerApiLoad(); - }.bind(this)); + gapi.load('picker', this.onPickerApiLoad.bind(this)); } onAuthApiLoad() { @@ -44,9 +37,7 @@ export default class GoogleDrive { 'client_id': this.clientId, 'scope': this.scope, 'immediate': false - }, function(authResult) { - this.handleAuthResult(authResult); - }.bind(this)); + }, this.handleAuthResult.bind(this)); } handleAuthResult(authResult) { From a018e7b4458c028ebed29d8e889ae72f086d0479 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 30 Mar 2021 01:28:12 +0200 Subject: [PATCH 0019/1519] refactor: rename GoogleDrive -> GoogleDrivePicker --- frappe/public/js/frappe/file_uploader/FileUploader.vue | 4 ++-- .../integrations/{google_drive.js => google_drive_picker.js} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename frappe/public/js/integrations/{google_drive.js => google_drive_picker.js} (98%) diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index 61b87606a5..154d4e0ea1 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -122,7 +122,7 @@ import FilePreview from './FilePreview.vue'; import FileBrowser from './FileBrowser.vue'; import WebLink from './WebLink.vue'; -import GoogleDrive from '../../integrations/google_drive'; +import GoogleDrivePicker from '../../integrations/google_drive_picker'; export default { name: 'FileUploader', @@ -457,7 +457,7 @@ export default { frappe.db.get_value("Google Settings", "Google Settings", ["client_id", "api_key", "app_id"]).then(resp => { let dialog = cur_dialog; dialog.hide(); - let google_drive = new GoogleDrive({ + let google_drive = new GoogleDrivePicker({ pickerCallback: data => this.google_drive_callback(data, dialog), developerKey: resp.message.api_key, clientId: resp.message.client_id, diff --git a/frappe/public/js/integrations/google_drive.js b/frappe/public/js/integrations/google_drive_picker.js similarity index 98% rename from frappe/public/js/integrations/google_drive.js rename to frappe/public/js/integrations/google_drive_picker.js index 06d9cbbe54..7ce9810f72 100644 --- a/frappe/public/js/integrations/google_drive.js +++ b/frappe/public/js/integrations/google_drive_picker.js @@ -1,4 +1,4 @@ -export default class GoogleDrive { +export default class GoogleDrivePicker { constructor({ pickerCallback, developerKey, From 144608241e9825af20c88b122a70f29d22aa5a41 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 30 Mar 2021 02:20:11 +0200 Subject: [PATCH 0020/1519] refactor: save one API call --- .../google_settings/google_settings.py | 22 +++++++++++++-- .../js/frappe/file_uploader/FileUploader.vue | 28 +++++++++---------- .../js/integrations/google_drive_picker.js | 8 ++++-- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/frappe/integrations/doctype/google_settings/google_settings.py b/frappe/integrations/doctype/google_settings/google_settings.py index ecc975235a..bd0e845977 100644 --- a/frappe/integrations/doctype/google_settings/google_settings.py +++ b/frappe/integrations/doctype/google_settings/google_settings.py @@ -3,11 +3,29 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document class GoogleSettings(Document): pass def get_auth_url(): - return "https://www.googleapis.com/oauth2/v4/token" \ No newline at end of file + return "https://www.googleapis.com/oauth2/v4/token" + + +@frappe.whitelist(allow_guest=True) +def get_file_picker_settings(): + """Return all the data FileUploader needs to start the Google Drive Picker.""" + if frappe.session.user == 'Guest': + return {'enabled': False} + + google_settings = frappe.get_single("Google Settings") + if not google_settings.enable: + return {'enabled': False} + + return { + 'enabled': True, + 'appId': google_settings.app_id, + 'developerKey': google_settings.api_key, + 'clientId': google_settings.client_id + } diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index 154d4e0ea1..64365cfc11 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -63,7 +63,7 @@
{{ __('Camera') }}
- \ - ')); - // add event handler for submit button - verify_token(); + $('.login-content').empty(); + $('.login-content:visible').append($('
').attr({ 'id': 'twofactor_div' }).html('{{ _("Verification") }}{{ _("Verify") }}')); + // add event handler for submit button + verify_token(); } var continue_otp_app = function (setup, qrcode) { From 2b51641799c60ae9b16a1d29e4306d6c35e66070 Mon Sep 17 00:00:00 2001 From: conncampbell Date: Mon, 10 May 2021 16:14:51 -0600 Subject: [PATCH 0043/1519] fix: changed translate entries to javascript rather than python to resolve semgrep linter translate checks --- frappe/templates/includes/login/login.js | 56 ++++++++++++++---------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 93bf95bdb7..4bc17cb14b 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -21,7 +21,7 @@ login.bind_events = function () { args.pwd = $("#login_password").val(); args.device = "desktop"; if (!args.usr || !args.pwd) { - frappe.msgprint('{{ _("Both login and password required") }}'); + frappe.msgprint(__("Both login and password required")); return false; } login.call(args); @@ -36,7 +36,7 @@ login.bind_events = function () { args.redirect_to = frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to")); args.full_name = frappe.utils.xss_sanitise(($("#signup_fullname").val() || "").trim()); if (!args.email || !validate_email(args.email) || !args.full_name) { - login.set_status('{{ _("Valid email and name required") }}', 'red'); + login.set_status(__("Valid email and name required"), 'red'); return false; } login.call(args); @@ -49,7 +49,7 @@ login.bind_events = function () { args.cmd = "frappe.core.doctype.user.user.reset_password"; args.user = ($("#forgot_email").val() || "").trim(); if (!args.user) { - login.set_status('{{ _("Valid Login id required.") }}', 'red'); + login.set_status(__("Valid Login id required."), 'red'); return false; } login.call(args); @@ -60,10 +60,10 @@ login.bind_events = function () { var input = $($(this).attr("toggle")); if (input.attr("type") == "password") { input.attr("type", "text"); - $(this).text('{{ _("Hide") }}') + $(this).text(__("Hide")) } else { input.attr("type", "password"); - $(this).text('{{ _("Show") }}') + $(this).text(__("Show")) } }); @@ -75,7 +75,7 @@ login.bind_events = function () { args.pwd = $("#login_password").val(); args.device = "desktop"; if (!args.usr || !args.pwd) { - login.set_status('{{ _("Both login and password required") }}', 'red'); + login.set_status(__("Both login and password required"), 'red'); return false; } login.call(args); @@ -136,7 +136,7 @@ login.signup = function () { // Login login.call = function (args, callback) { - login.set_status('{{ _("Verifying...") }}', 'blue'); + login.set_status(__("Verifying..."), 'blue'); return frappe.call({ type: "POST", @@ -194,7 +194,7 @@ login.login_handlers = (function () { var login_handlers = { 200: function (data) { if (data.message == 'Logged In') { - login.set_status('{{ _("Success") }}', 'green'); + login.set_status(__("Success"), 'green'); window.location.href = frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to")) || data.home_page; } else if (data.message == 'Password Reset') { window.location.href = frappe.utils.sanitise_redirect(data.redirect_to); @@ -218,13 +218,13 @@ login.login_handlers = (function () { } } else if (window.location.hash === '#forgot') { if (data.message === 'not found') { - login.set_status('{{ _("Not a valid user") }}', 'red'); + login.set_status(__("Not a valid user"), 'red'); } else if (data.message == 'not allowed') { - login.set_status('{{ _("Not Allowed") }}', 'red'); + login.set_status(__("Not Allowed"), 'red'); } else if (data.message == 'disabled') { - login.set_status('{{ _("Not Allowed: Disabled User") }}', 'red'); + login.set_status(__("Not Allowed: Disabled User"), 'red'); } else { - login.set_status('{{ _("Instructions Emailed") }}', 'green'); + login.set_status(__("Instructions Emailed"), 'green'); } @@ -232,7 +232,7 @@ login.login_handlers = (function () { if (cint(data.message[0]) == 0) { login.set_status(data.message[1], 'red'); } else { - login.set_status('{{ _("Success") }}', 'green'); + login.set_status(__("Success"), 'green'); frappe.msgprint(data.message[1]) } //login.set_status(__(data.message), 'green'); @@ -240,7 +240,7 @@ login.login_handlers = (function () { //OTP verification if (data.verification && data.message != 'Logged In') { - login.set_status('{{ _("Success") }}', 'green'); + login.set_status(__("Success"), 'green'); document.cookie = "tmp_id=" + data.tmp_id; @@ -253,8 +253,8 @@ login.login_handlers = (function () { } } }, - 401: get_error_handler('{{ _("Invalid Login. Try again.") }}'), - 417: get_error_handler('{{ _("Oops! Something went wrong") }}') + 401: get_error_handler(__("Invalid Login. Try again.")), + 417: get_error_handler(__("Oops! Something went wrong")) }; return login_handlers; @@ -282,7 +282,7 @@ var verify_token = function (event) { args.otp = $("#login_token").val(); args.tmp_id = frappe.get_cookie('tmp_id'); if (!args.otp) { - frappe.msgprint('{{ _("Login token required") }}'); + frappe.msgprint(__("Login token required")); return false; } login.call(args); @@ -292,9 +292,17 @@ var verify_token = function (event) { var request_otp = function (r) { $('.login-content').empty(); - $('.login-content:visible').append($('
').attr({ 'id': 'twofactor_div' }).html('{{ _("Verification") }}{{ _("Verify") }}')); - // add event handler for submit button - verify_token(); + $('.login-content:visible').append($('
').attr({ 'id': 'twofactor_div' }).html( + '
\ +
\ + {{ _("Verification") }}\ +
\ +
\ + \ + \ +
')); + // add event handler for submit button + verify_token(); } var continue_otp_app = function (setup, qrcode) { @@ -302,11 +310,11 @@ var continue_otp_app = function (setup, qrcode) { var qrcode_div = $('
'); if (setup) { - direction = $('
').attr('id', 'qr_info').text('{{ _("Enter Code displayed in OTP App.") }}'); + direction = $('
').attr('id', 'qr_info').text(__("Enter Code displayed in OTP App.")); qrcode_div.append(direction); $('#otp_div').prepend(qrcode_div); } else { - direction = $('
').attr('id', 'qr_info').text('{{ _("OTP setup using OTP App was not completed. Please contact Administrator.") }}'); + direction = $('
').attr('id', 'qr_info').text(__("OTP setup using OTP App was not completed. Please contact Administrator.")); qrcode_div.append(direction); $('#otp_div').prepend(qrcode_div); } @@ -320,7 +328,7 @@ var continue_sms = function (setup, prompt) { sms_div.append(prompt) $('#otp_div').prepend(sms_div); } else { - direction = $('
').attr('id', 'qr_info').text(prompt || '{{ _("SMS was not sent. Please contact Administrator.") }}'); + direction = $('
').attr('id', 'qr_info').text(prompt || __("SMS was not sent. Please contact Administrator.")); sms_div.append(direction); $('#otp_div').prepend(sms_div) } @@ -334,7 +342,7 @@ var continue_email = function (setup, prompt) { email_div.append(prompt) $('#otp_div').prepend(email_div); } else { - var direction = $('
').attr('id', 'qr_info').text(prompt || '{{ _("Verification code email not sent. Please contact Administrator.") }}'); + var direction = $('
').attr('id', 'qr_info').text(prompt || __("Verification code email not sent. Please contact Administrator.")); email_div.append(direction); $('#otp_div').prepend(email_div); } From e101105bce30b7eae0a4f3371566ba4e85e0606a Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 11 May 2021 10:41:18 +0530 Subject: [PATCH 0044/1519] refactor: Import get_response instead of using serve.get_response --- frappe/tests/test_website.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 97e00b9268..5221cbe6b7 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -3,8 +3,7 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.website import render -from frappe.website import serve +from frappe.website.serve import get_response from frappe.website.utils import get_home_page from frappe.utils import set_request @@ -51,7 +50,7 @@ class TestWebsite(unittest.TestCase): def test_page_load(self): set_request(method='POST', path='login') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 200) @@ -62,17 +61,17 @@ class TestWebsite(unittest.TestCase): def test_static_page(self): set_request(method='GET', path='/_test/static-file-test.png') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 200) def test_error_page(self): set_request(method='GET', path='/error') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 500) def test_login(self): set_request(method='GET', path='/login') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 200) html = frappe.safe_decode(response.get_data()) @@ -83,7 +82,7 @@ class TestWebsite(unittest.TestCase): def test_app(self): frappe.set_user('Administrator') set_request(method='GET', path='/app') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 200) html = frappe.safe_decode(response.get_data()) @@ -92,7 +91,7 @@ class TestWebsite(unittest.TestCase): def test_not_found(self): set_request(method='GET', path='/_test/missing') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 404) @@ -118,31 +117,31 @@ class TestWebsite(unittest.TestCase): frappe.cache().delete_key('website_redirects') set_request(method='GET', path='/testfrom') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get('Location'), r'://testto1') set_request(method='GET', path='/testfromregex/test') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get('Location'), r'://testto2') set_request(method='GET', path='/testsub/me') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get('Location'), r'://testto3/me') set_request(method='GET', path='/test404') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 404) set_request(method='GET', path='/testsource') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get('Location'), '/testtarget') set_request(method='GET', path='/courses/course?course=data') - response = serve.get_response() + response = get_response() self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get('Location'), '/courses/data') From 5c92e8a12962a4c8acd9037c81ae81433357b56f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 11 May 2021 16:43:22 +0530 Subject: [PATCH 0045/1519] feat: Add class for standard pages (Error & NotFound) --- frappe/website/page_controllers/error_page.py | 9 +++++++++ frappe/website/page_controllers/not_found_page.py | 9 +++++++++ frappe/website/page_controllers/not_permitted_page.py | 4 ++++ 3 files changed, 22 insertions(+) create mode 100644 frappe/website/page_controllers/error_page.py create mode 100644 frappe/website/page_controllers/not_found_page.py diff --git a/frappe/website/page_controllers/error_page.py b/frappe/website/page_controllers/error_page.py new file mode 100644 index 0000000000..caa72b015d --- /dev/null +++ b/frappe/website/page_controllers/error_page.py @@ -0,0 +1,9 @@ +from frappe import _ +from frappe.website.page_controllers.not_found_page import NotFoundPage +from frappe.website.page_controllers.template_page import TemplatePage + +class ErrorPage(TemplatePage): + def __init__(self, path, http_status_code, exception): + path = 'error' + super().__init__(path=path, http_status_code=http_status_code) + self.http_status_code = getattr(exception, 'http_status_code', None) or http_status_code or 500 diff --git a/frappe/website/page_controllers/not_found_page.py b/frappe/website/page_controllers/not_found_page.py new file mode 100644 index 0000000000..d1a1797efc --- /dev/null +++ b/frappe/website/page_controllers/not_found_page.py @@ -0,0 +1,9 @@ +from frappe import _ +from frappe.website.page_controllers.template_page import TemplatePage + +class NotFoundPage(TemplatePage): + def __init__(self, path, http_status_code): + super().__init__(path=path, http_status_code=http_status_code) + self.path = '404' + self.http_status_code = 404 + self.template_path = '404' diff --git a/frappe/website/page_controllers/not_permitted_page.py b/frappe/website/page_controllers/not_permitted_page.py index e0cd6bf970..61c6b88e09 100644 --- a/frappe/website/page_controllers/not_permitted_page.py +++ b/frappe/website/page_controllers/not_permitted_page.py @@ -3,6 +3,10 @@ from frappe import _ from frappe.website.page_controllers.template_page import TemplatePage class NotPermittedPage(TemplatePage): + def __init__(self, path, http_status_code): + super().__init__(path=path, http_status_code=http_status_code) + self.http_status_code = 403 + def validate(self): frappe.local.message_title = _("Not Permitted") frappe.local.response['context'] = dict( From 9429d06a17f6cc51716ff40e51b36150d860568f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 11 May 2021 16:44:50 +0530 Subject: [PATCH 0046/1519] refactor: Remove reduntant code - Use Error & NotFound page as required --- frappe/tests/test_website.py | 2 +- frappe/website/serve.py | 22 ++++++++-------------- frappe/www/_test/problematic_page.html | 1 + 3 files changed, 10 insertions(+), 15 deletions(-) create mode 100644 frappe/www/_test/problematic_page.html diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 5221cbe6b7..cc500d1c02 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -65,7 +65,7 @@ class TestWebsite(unittest.TestCase): self.assertEqual(response.status_code, 200) def test_error_page(self): - set_request(method='GET', path='/error') + set_request(method='GET', path='/_test/problematic_page') response = get_response() self.assertEqual(response.status_code, 500) diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 1a5852d036..b6586dc1f3 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -2,7 +2,9 @@ import frappe from frappe.utils import cstr from frappe.website.page_controllers.document_page import DocumentPage +from frappe.website.page_controllers.error_page import ErrorPage from frappe.website.page_controllers.list_page import ListPage +from frappe.website.page_controllers.not_found_page import NotFoundPage from frappe.website.page_controllers.not_permitted_page import NotPermittedPage from frappe.website.page_controllers.print_page import PrintPage from frappe.website.page_controllers.template_page import TemplatePage @@ -26,19 +28,11 @@ def get_response(path=None, http_status_code=200): # there is no way to determine the type of the page based on the route # so evaluate each type of page sequentially - response = StaticPage(path, http_status_code).get() - if not response: - response = TemplatePage(path, http_status_code).get() - if not response: - response = ListPage(path, http_status_code).get() - if not response: - response = WebFormPage(path, http_status_code).get() - if not response: - response = DocumentPage(path, http_status_code).get() - if not response: - response = PrintPage(path, http_status_code).get() - if not response: - response = TemplatePage('404', 404).get() + renderers = [StaticPage, TemplatePage, ListPage, WebFormPage, DocumentPage, PrintPage, NotFoundPage] + for resolver in renderers: + response = resolver(path, http_status_code).get() + if response: + break except frappe.Redirect: return build_response(path, "", 301, { "Location": frappe.flags.redirect_location or (frappe.local.response or {}).get('location'), @@ -48,6 +42,6 @@ def get_response(path=None, http_status_code=200): frappe.local.message = cstr(e) response = NotPermittedPage(path, http_status_code).get() except Exception as e: - response = TemplatePage('error', getattr(e, 'http_status_code', None) or http_status_code).get() + response = ErrorPage(path, http_status_code, exception=e).get() return response diff --git a/frappe/www/_test/problematic_page.html b/frappe/www/_test/problematic_page.html new file mode 100644 index 0000000000..5e194421d2 --- /dev/null +++ b/frappe/www/_test/problematic_page.html @@ -0,0 +1 @@ +{% raise %} From 006f0a2bafb9945c8fdf6fcb41c015e45b5967a4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 11 May 2021 17:03:00 +0530 Subject: [PATCH 0047/1519] fix: Rename resolver to renderer --- frappe/website/page_controllers/error_page.py | 2 -- frappe/website/page_controllers/not_found_page.py | 1 - frappe/website/serve.py | 7 ++++--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frappe/website/page_controllers/error_page.py b/frappe/website/page_controllers/error_page.py index caa72b015d..1f54a5f38c 100644 --- a/frappe/website/page_controllers/error_page.py +++ b/frappe/website/page_controllers/error_page.py @@ -1,5 +1,3 @@ -from frappe import _ -from frappe.website.page_controllers.not_found_page import NotFoundPage from frappe.website.page_controllers.template_page import TemplatePage class ErrorPage(TemplatePage): diff --git a/frappe/website/page_controllers/not_found_page.py b/frappe/website/page_controllers/not_found_page.py index d1a1797efc..1d15877e9d 100644 --- a/frappe/website/page_controllers/not_found_page.py +++ b/frappe/website/page_controllers/not_found_page.py @@ -1,4 +1,3 @@ -from frappe import _ from frappe.website.page_controllers.template_page import TemplatePage class NotFoundPage(TemplatePage): diff --git a/frappe/website/serve.py b/frappe/website/serve.py index b6586dc1f3..eacaef96e1 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -17,6 +17,8 @@ from frappe.website.render import build_response, resolve_path def get_response(path=None, http_status_code=200): """render html page""" query_string = None + response = None + if not path: path = frappe.local.request.path query_string = frappe.local.request.query_string @@ -25,12 +27,11 @@ def get_response(path=None, http_status_code=200): path = path.strip('/ ') resolve_redirect(path, query_string) path = resolve_path(path) - # there is no way to determine the type of the page based on the route # so evaluate each type of page sequentially renderers = [StaticPage, TemplatePage, ListPage, WebFormPage, DocumentPage, PrintPage, NotFoundPage] - for resolver in renderers: - response = resolver(path, http_status_code).get() + for renderer in renderers: + response = renderer(path, http_status_code).get() if response: break except frappe.Redirect: From 4b7a1488b5b465ce2a4ba9c67c9b01b461ba6342 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 11 May 2021 17:28:16 +0530 Subject: [PATCH 0048/1519] fix: Add New Shortcut --- frappe/desk/desktop.py | 50 ++++++++++++ frappe/public/js/frappe/views/wiki.js | 34 +++++++- .../public/js/frappe/widgets/widget_group.js | 4 - frappe/public/js/frappe/wiki_blocks/blank.js | 10 +-- frappe/public/js/frappe/wiki_blocks/card.js | 21 +++-- frappe/public/js/frappe/wiki_blocks/chart.js | 15 ++-- .../public/js/frappe/wiki_blocks/paragraph.js | 10 +-- .../public/js/frappe/wiki_blocks/shortcut.js | 77 ++++++++++++------- 8 files changed, 164 insertions(+), 57 deletions(-) diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index d1b5e27a2f..7f054d5c1f 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -537,6 +537,56 @@ def save_customization(page, config): return True +@frappe.whitelist() +def save_new_widget(page, new_widgets): + original_page = frappe.get_doc("Workspace", page) + widgets = _dict(loads(new_widgets)) + + if widgets.chart: + original_page.charts = new_widget(widgets.chart, "Workspace Chart", "charts") + if widgets.shortcut: + original_page.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts")) + if widgets.card: + original_page.build_links_table_from_cards(widgets.card) + + try: + original_page.save(ignore_permissions=True) + except (ValidationError, TypeError) as e: + # Create a json string to log + json_config = dumps(widgets, sort_keys=True, indent=4) + + # Error log body + log = \ + """ + page: {0} + config: {1} + exception: {2} + """.format(page, json_config, e) + frappe.log_error(log, _("Could not save customization")) + return False + + return True + +def new_widget(config, doctype, parentfield): + if not config: + return [] + prepare_widget_list = [] + for idx, widget in enumerate(config): + # Some cleanup + widget.pop("name", None) + + # New Doc + doc = frappe.new_doc(doctype) + doc.update(widget) + + # Manually Set IDX + doc.idx = idx + 1 + + # Set Parent Field + doc.parentfield = parentfield + + prepare_widget_list.append(doc) + return prepare_widget_list def prepare_widget(config, doctype, parentfield): """Create widget child table entries with parent details diff --git a/frappe/public/js/frappe/views/wiki.js b/frappe/public/js/frappe/views/wiki.js index dcfa840997..b1fe2dbdf5 100644 --- a/frappe/public/js/frappe/views/wiki.js +++ b/frappe/public/js/frappe/views/wiki.js @@ -14,6 +14,7 @@ frappe.views.Wiki = class Wiki { this.sorted_sidebar_items = []; this.tools = {} this.isReadOnly = true; + this.new_page = null; this.prepare_container(); this.setup_wiki_pages(); } @@ -39,6 +40,10 @@ frappe.views.Wiki = class Wiki { frappe.router.route(); this.make_sidebar(root_pages); } + if (this.new_page) { + frappe.set_route(`wiki/${frappe.router.slug(this.new_page)}`) + this.new_page = null; + } }) } @@ -258,6 +263,7 @@ frappe.views.Wiki = class Wiki { this.setup_customization_buttons(); this.make_sidebar_sortable(); this.make_blocks_sortable(); + // this.customize(); }) }, ); @@ -296,7 +302,8 @@ frappe.views.Wiki = class Wiki { make_blocks_sortable() { let me = this; this.page_sortable = Sortable.create(this.page.main.find(".codex-editor__redactor").get(0), { - handle: ".ce-block", + handle: ".drag-handle", + draggable: ".ce-block", animation: 150, onEnd: function (evt){ me.editor.blocks.move(evt.newIndex, evt.oldIndex); @@ -374,6 +381,8 @@ frappe.views.Wiki = class Wiki { this.isReadOnly = false; this.editor.readOnly.toggle(); } + this.make_sidebar_sortable(); + this.make_blocks_sortable(); this.dirty = false; }) } @@ -407,6 +416,17 @@ frappe.views.Wiki = class Wiki { } let me = this; this.editor.save().then((outputData) => { + let new_widgets = {}; + outputData.blocks.forEach(item => { + if (item.data.new) { + if (!new_widgets[item.type]) { + new_widgets[item.type] = [] + } + new_widgets[item.type].push(item.data.new); + delete item.data['new']; + } + }) + frappe.call({ method: "frappe.desk.doctype.internal_wiki_page.internal_wiki_page.save_wiki_page", args: { @@ -419,10 +439,22 @@ frappe.views.Wiki = class Wiki { callback: function(res) { frappe.dom.unfreeze(); if (res.message) { + let cur_page = res.message; + if (!$.isEmptyObject(new_widgets)) { + frappe.call('frappe.desk.desktop.save_new_widget', { + page: me.title, + new_widgets: new_widgets + }).then(res => { + if(res.message) { + me.reload(); + } + }); + } frappe.show_alert({ message: __("Page Saved Successfully"), indicator: "green" }); me.title = ''; me.parent = ''; me.sorted_sidebar_items = []; + me.new_page = cur_page; me.reload(); } } diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js index d1a5e3013c..3d97a8f778 100644 --- a/frappe/public/js/frappe/widgets/widget_group.js +++ b/frappe/public/js/frappe/widgets/widget_group.js @@ -218,13 +218,9 @@ export class SingleWidgetGroup { } customize() { - // if (!this.hidden) this.widget_area.show(); this.widgets_list.forEach((wid) => { wid.customize(this.options); }); - - // this.options.allow_create && this.setup_new_widget(); - // this.options.allow_sorting && this.setup_sortable(); } } diff --git a/frappe/public/js/frappe/wiki_blocks/blank.js b/frappe/public/js/frappe/wiki_blocks/blank.js index fa1a950280..ae8a53fc33 100644 --- a/frappe/public/js/frappe/wiki_blocks/blank.js +++ b/frappe/public/js/frappe/wiki_blocks/blank.js @@ -15,11 +15,11 @@ export default class Blank { this.api = api; this.config = config; this.readOnly = readOnly; - this.col = this.data.col ? this.data.col : "12", - this.pt = this.data.pt ? this.data.pt : "0", - this.pr = this.data.pr ? this.data.pr : "0", - this.pb = this.data.pb ? this.data.pb : "0", - this.pl = this.data.pl ? this.data.pl : "0" + this.col = this.data.col ? this.data.col : "12"; + this.pt = this.data.pt ? this.data.pt : "0"; + this.pr = this.data.pr ? this.data.pr : "0"; + this.pb = this.data.pb ? this.data.pb : "0"; + this.pl = this.data.pl ? this.data.pl : "0"; } render() { diff --git a/frappe/public/js/frappe/wiki_blocks/card.js b/frappe/public/js/frappe/wiki_blocks/card.js index 12061e2d02..a859b04439 100644 --- a/frappe/public/js/frappe/wiki_blocks/card.js +++ b/frappe/public/js/frappe/wiki_blocks/card.js @@ -16,11 +16,12 @@ export default class Card { this.config = config; this.readOnly = readOnly; this.sections = {}; - this.col = this.data.col ? this.data.col : "12", - this.pt = this.data.pt ? this.data.pt : "0", - this.pr = this.data.pr ? this.data.pr : "0", - this.pb = this.data.pb ? this.data.pb : "0", - this.pl = this.data.pl ? this.data.pl : "0" + this.col = this.data.col ? this.data.col : "12"; + this.pt = this.data.pt ? this.data.pt : "0"; + this.pr = this.data.pr ? this.data.pr : "0"; + this.pb = this.data.pb ? this.data.pb : "0"; + this.pl = this.data.pl ? this.data.pl : "0"; + this.allow_customization = !this.readOnly; } render() { @@ -135,15 +136,16 @@ export default class Card { }); this.wrapper.innerHTML = ''; this.sections = {}; + card.in_customize_mode = !this.readOnly; let cards = new frappe.widget.SingleWidgetGroup({ container: this.wrapper, type: "links", columns: 3, options: { allow_sorting: this.allow_customization, - allow_create: false, - allow_delete: false, - allow_hiding: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, allow_edit: false, }, widgets: card @@ -151,5 +153,8 @@ export default class Card { this.sections["cards"] = cards; this.wrapper.setAttribute("card_name", card_name); + if (!this.readOnly) { + this.sections["cards"].customize(); + } } } \ No newline at end of file diff --git a/frappe/public/js/frappe/wiki_blocks/chart.js b/frappe/public/js/frappe/wiki_blocks/chart.js index 054e8d1c12..f8c97852fa 100644 --- a/frappe/public/js/frappe/wiki_blocks/chart.js +++ b/frappe/public/js/frappe/wiki_blocks/chart.js @@ -16,11 +16,12 @@ export default class Chart { this.config = config; this.readOnly = readOnly; this.sections = {}; - this.col = this.data.col ? this.data.col : "12", - this.pt = this.data.pt ? this.data.pt : "0", - this.pr = this.data.pr ? this.data.pr : "0", - this.pb = this.data.pb ? this.data.pb : "0", - this.pl = this.data.pl ? this.data.pl : "0" + this.col = this.data.col ? this.data.col : "12"; + this.pt = this.data.pt ? this.data.pt : "0"; + this.pr = this.data.pr ? this.data.pr : "0"; + this.pb = this.data.pb ? this.data.pb : "0"; + this.pl = this.data.pl ? this.data.pl : "0"; + this.allow_customization = !this.readOnly; } render() { @@ -135,6 +136,7 @@ export default class Chart { }); this.wrapper.innerHTML = ''; this.sections = {}; + chart.in_customize_mode = !this.readOnly; this.sections["charts"] = new frappe.widget.SingleWidgetGroup({ container: this.wrapper, type: "chart", @@ -152,5 +154,8 @@ export default class Chart { widgets: chart }); this.wrapper.setAttribute("chart_name", chart_name) + if (!this.readOnly) { + this.sections["charts"].customize(); + } } } \ No newline at end of file diff --git a/frappe/public/js/frappe/wiki_blocks/paragraph.js b/frappe/public/js/frappe/wiki_blocks/paragraph.js index 0cab2b9373..570a04e661 100644 --- a/frappe/public/js/frappe/wiki_blocks/paragraph.js +++ b/frappe/public/js/frappe/wiki_blocks/paragraph.js @@ -23,11 +23,11 @@ export default class Paragraph { this._preserveBlank = config.preserveBlank !== undefined ? config.preserveBlank : false; this.data = data; - this.col = this.data.col ? this.data.col : "12", - this.pt = this.data.pt ? this.data.pt : "0", - this.pr = this.data.pr ? this.data.pr : "0", - this.pb = this.data.pb ? this.data.pb : "0", - this.pl = this.data.pl ? this.data.pl : "0" + this.col = this.data.col ? this.data.col : "12"; + this.pt = this.data.pt ? this.data.pt : "0"; + this.pr = this.data.pr ? this.data.pr : "0"; + this.pb = this.data.pb ? this.data.pb : "0"; + this.pl = this.data.pl ? this.data.pl : "0"; } onKeyUp(e) { diff --git a/frappe/public/js/frappe/wiki_blocks/shortcut.js b/frappe/public/js/frappe/wiki_blocks/shortcut.js index 0cb8de6ef8..1a3d2ae970 100644 --- a/frappe/public/js/frappe/wiki_blocks/shortcut.js +++ b/frappe/public/js/frappe/wiki_blocks/shortcut.js @@ -1,3 +1,4 @@ +import get_dialog_constructor from "../widgets/widget_dialog.js"; export default class Shortcut { static get toolbox() { return { @@ -15,28 +16,25 @@ export default class Shortcut { this.api = api; this.config = config; this.readOnly = readOnly; - this.sections = {}; - this.col = this.data.col ? this.data.col : "12", - this.pt = this.data.pt ? this.data.pt : "0", - this.pr = this.data.pr ? this.data.pr : "0", - this.pb = this.data.pb ? this.data.pb : "0", - this.pl = this.data.pl ? this.data.pl : "0" + this.col = this.data.col ? this.data.col : "12"; + this.pt = this.data.pt ? this.data.pt : "0"; + this.pr = this.data.pr ? this.data.pr : "0"; + this.pb = this.data.pb ? this.data.pb : "0"; + this.pl = this.data.pl ? this.data.pl : "0"; + this.allow_customization = !this.readOnly; + this.options = { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true, + } } render() { - let me = this; this.wrapper = document.createElement('div'); - this._make_fieldgroup(this.wrapper, [{ - fieldtype: "Select", - label: "Shortcut Name", - fieldname: "shortcut_name", - options: this.config.page_data.shortcuts.items.map(({ label }) => label), - change: function() { - if (this.value) { - me._make_shortcuts(this.value); - } - } - }]); + this._new_shortcut(); + if (this.data && this.data.shortcut_name) { this._make_shortcuts(this.data.shortcut_name) } @@ -50,7 +48,8 @@ export default class Shortcut { pt: this._getPadding("t"), pr: this._getPadding("r"), pb: this._getPadding("b"), - pl: this._getPadding("l") + pl: this._getPadding("l"), + new: this.new_shortcut_widget } } @@ -63,6 +62,30 @@ export default class Shortcut { e.classList.add("pl-" + this.pl) } + _new_shortcut() { + const dialog_class = get_dialog_constructor('shortcut'); + this.dialog = new dialog_class({ + label: this.label, + type: 'shortcut', + primary_action: (widget) => { + widget.in_customize_mode = 1; + let wid = frappe.widget.make_widget({ + ...widget, + widget_type: 'shortcut', + container: this.wrapper, + options: this.options, + }); + wid.customize(this.options); + this.wrapper.setAttribute("shortcut_name", wid.label); + this.new_shortcut_widget = wid.get_config(); + }, + }); + + if (!this.readOnly && this.data && !this.data.shortcut_name) { + this.dialog.make(); + } + } + _getCol() { var e = 12, t = "col-12", @@ -134,20 +157,16 @@ export default class Shortcut { return obj.label == shortcut_name }); this.wrapper.innerHTML = ''; - this.sections = {}; - this.sections["shortcuts"] = new frappe.widget.SingleWidgetGroup({ + shortcut.in_customize_mode = !this.readOnly; + let shortcut_widget = new frappe.widget.SingleWidgetGroup({ container: this.wrapper, type: "shortcut", - columns: 3, - options: { - allow_sorting: this.allow_customization, - allow_create: this.allow_customization, - allow_delete: this.allow_customization, - allow_hiding: false, - allow_edit: true, - }, + options: this.options, widgets: shortcut }); this.wrapper.setAttribute("shortcut_name", shortcut_name); + if (!this.readOnly) { + shortcut_widget.customize(); + } } } \ No newline at end of file From 80db1a715a8de19fdcd65a0b5b33aa178d79253f Mon Sep 17 00:00:00 2001 From: shariquerik Date: Wed, 12 May 2021 13:23:47 +0530 Subject: [PATCH 0049/1519] fix: Add New Card --- frappe/desk/desktop.py | 4 +- frappe/desk/doctype/workspace/workspace.py | 29 ++++++ .../public/js/frappe/widgets/widget_dialog.js | 93 +++++++++++++++++++ frappe/public/js/frappe/wiki_blocks/card.js | 65 +++++++------ 4 files changed, 163 insertions(+), 28 deletions(-) diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 7f054d5c1f..c096f50b78 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -543,11 +543,11 @@ def save_new_widget(page, new_widgets): widgets = _dict(loads(new_widgets)) if widgets.chart: - original_page.charts = new_widget(widgets.chart, "Workspace Chart", "charts") + original_page.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts")) if widgets.shortcut: original_page.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts")) if widgets.card: - original_page.build_links_table_from_cards(widgets.card) + original_page.build_links_table_from_card(widgets.card) try: original_page.save(ignore_permissions=True) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 0934138821..20c9240188 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -100,6 +100,35 @@ class Workspace(Document): "is_query_report": link.get('is_query_report') }) + def build_links_table_from_card(self, config): + # Empty links table + # self.links = [] + # order = config.get('order') + # widgets = config.get('widgets') + + for idx, card in enumerate(config): + # card = widgets[name].copy() + links = loads(card.get('links')) + + self.append('links', { + "label": card.get('label'), + "type": "Card Break", + "icon": card.get('icon'), + "hidden": card.get('hidden') or False + }) + + for link in links: + self.append('links', { + "label": link.get('label'), + "type": "Link", + "link_type": link.get('link_type'), + "link_to": link.get('link_to'), + "onboard": link.get('onboard'), + "only_for": link.get('only_for'), + "dependencies": link.get('dependencies'), + "is_query_report": link.get('is_query_report') + }) + def disable_saving_as_standard(): return frappe.flags.in_install or \ diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index eefb78c29a..5bcaaf9e3e 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -124,6 +124,98 @@ class ChartDialog extends WidgetDialog { } } +class CardDialog extends WidgetDialog { + constructor(opts) { + super(opts); + } + + get_fields() { + let me = this; + return [ + { + fieldtype: "Data", + fieldname: "label", + label: "Label", + }, + { + fieldname: 'links', + fieldtype: 'Table', + label: __('Card Links'), + editable_grid: 1, + data: this.data || [], + get_data: () => { + return this.data || []; + }, + fields: [ + { + fieldname: "label", + fieldtype: "Data", + in_list_view: 1, + label: "Label" + }, + { + fieldname: "icon", + fieldtype: "Data", + label: "Icon" + }, + { + fieldname: "link_type", + fieldtype: "Select", + in_list_view: 1, + label: "Link Type", + options: ["DocType", "Page", "Report"], + onchange: (e) => { + me.link_to = e.currentTarget.value; + } + }, + { + fieldname: "link_to", + fieldtype: "Dynamic Link", + in_list_view: 1, + label: "Link To", + options: "link_type", + get_options: () => { + return me.link_to; + } + }, + { + fieldname: "column_break_7", + fieldtype: "Column Break" + }, + { + fieldname: "dependencies", + fieldtype: "Data", + label: "Dependencies" + }, + { + fieldname: "only_for", + fieldtype: "Link", + label: "Only for ", + options: "Country" + }, + { + default: "0", + fieldname: "onboard", + fieldtype: "Check", + label: "Onboard" + }, + { + default: "0", + fieldname: "is_query_report", + fieldtype: "Check", + label: "Is Query Report" + } + ], + }, + ]; + } + + process_data(data) { + data.label = data.label ? data.label : data.chart_name; + return data; + } +} + class ShortcutDialog extends WidgetDialog { constructor(opts) { super(opts); @@ -437,6 +529,7 @@ export default function get_dialog_constructor(type) { chart: ChartDialog, shortcut: ShortcutDialog, number_card: NumberCardDialog, + card: CardDialog, }; return widget_map[type] || WidgetDialog; diff --git a/frappe/public/js/frappe/wiki_blocks/card.js b/frappe/public/js/frappe/wiki_blocks/card.js index a859b04439..cc79a57c3e 100644 --- a/frappe/public/js/frappe/wiki_blocks/card.js +++ b/frappe/public/js/frappe/wiki_blocks/card.js @@ -1,3 +1,4 @@ +import get_dialog_constructor from "../widgets/widget_dialog.js"; export default class Card { static get toolbox() { return { @@ -22,22 +23,19 @@ export default class Card { this.pb = this.data.pb ? this.data.pb : "0"; this.pl = this.data.pl ? this.data.pl : "0"; this.allow_customization = !this.readOnly; + this.options = { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: false, + } } render() { - let me = this; this.wrapper = document.createElement('div'); - this._make_fieldgroup(this.wrapper, [{ - fieldtype: "Select", - label: "Card Name", - fieldname: "card_name", - options: this.config.page_data.cards.items.map(({ label }) => label), - change: function() { - if (this.value) { - me._make_cards(this.value) - } - } - }]); + this._new_card(); + if (this.data && this.data.card_name) { this._make_cards(this.data.card_name) } @@ -51,7 +49,8 @@ export default class Card { pt: this._getPadding("t"), pr: this._getPadding("r"), pb: this._getPadding("b"), - pl: this._getPadding("l") + pl: this._getPadding("l"), + new: this.new_card_widget } } @@ -64,6 +63,30 @@ export default class Card { e.classList.add("pl-" + this.pl) } + _new_card() { + const dialog_class = get_dialog_constructor('card'); + this.dialog = new dialog_class({ + label: this.label, + type: 'card', + primary_action: (widget) => { + widget.in_customize_mode = 1; + let wid = frappe.widget.make_widget({ + ...widget, + widget_type: 'links', + container: this.wrapper, + options: this.options, + }); + wid.customize(this.options); + this.wrapper.setAttribute("card_name", wid.label); + this.new_card_widget = wid.get_config(); + }, + }); + + if (!this.readOnly && this.data && !this.data.card_name) { + this.dialog.make(); + } + } + _getCol() { var e = 12, t = "col-12", @@ -135,26 +158,16 @@ export default class Card { return obj.label == card_name }); this.wrapper.innerHTML = ''; - this.sections = {}; card.in_customize_mode = !this.readOnly; - let cards = new frappe.widget.SingleWidgetGroup({ + let card_widget = new frappe.widget.SingleWidgetGroup({ container: this.wrapper, type: "links", - columns: 3, - options: { - allow_sorting: this.allow_customization, - allow_create: this.allow_customization, - allow_delete: this.allow_customization, - allow_hiding: false, - allow_edit: false, - }, + options: this.options, widgets: card }); - - this.sections["cards"] = cards; this.wrapper.setAttribute("card_name", card_name); if (!this.readOnly) { - this.sections["cards"].customize(); + card_widget.customize(); } } } \ No newline at end of file From 2f89787fa96a8883ee056c90c0f539b7be7ab14e Mon Sep 17 00:00:00 2001 From: shariquerik Date: Wed, 12 May 2021 14:33:12 +0530 Subject: [PATCH 0050/1519] fix: sider fix --- .../internal_wiki_page/internal_wiki_page.py | 4 +- frappe/desk/page/wiki/wiki.js | 4 +- frappe/public/js/frappe/views/wiki.js | 77 ++++++++-------- frappe/public/js/frappe/wiki_blocks/blank.js | 34 +++---- frappe/public/js/frappe/wiki_blocks/blocks.js | 4 +- frappe/public/js/frappe/wiki_blocks/card.js | 40 ++++----- frappe/public/js/frappe/wiki_blocks/chart.js | 42 ++++----- .../public/js/frappe/wiki_blocks/paragraph.js | 48 +++++----- .../public/js/frappe/wiki_blocks/shortcut.js | 40 ++++----- .../js/frappe/wiki_blocks/spacing_tune.js | 88 +++++++++---------- frappe/public/scss/desk/wiki.scss | 49 +++++------ 11 files changed, 213 insertions(+), 217 deletions(-) diff --git a/frappe/desk/doctype/internal_wiki_page/internal_wiki_page.py b/frappe/desk/doctype/internal_wiki_page/internal_wiki_page.py index 9e291ed265..6e695ef516 100644 --- a/frappe/desk/doctype/internal_wiki_page/internal_wiki_page.py +++ b/frappe/desk/doctype/internal_wiki_page/internal_wiki_page.py @@ -3,8 +3,8 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, json -from frappe import _ +import frappe +import json from frappe.model.document import Document class InternalWikiPage(Document): diff --git a/frappe/desk/page/wiki/wiki.js b/frappe/desk/page/wiki/wiki.js index 81ff2a7fa2..6ac8bc89cb 100644 --- a/frappe/desk/page/wiki/wiki.js +++ b/frappe/desk/page/wiki/wiki.js @@ -1,4 +1,4 @@ -frappe.provide('frappe.views') +frappe.provide('frappe.views'); frappe.pages['wiki'].on_page_load = function(wrapper) { frappe.ui.make_app_page({ @@ -12,4 +12,4 @@ frappe.pages['wiki'].on_page_load = function(wrapper) { $(wrapper).bind('show', function () { frappe.wiki.show(); }); -} +}; diff --git a/frappe/public/js/frappe/views/wiki.js b/frappe/public/js/frappe/views/wiki.js index b1fe2dbdf5..a7c41b66bc 100644 --- a/frappe/public/js/frappe/views/wiki.js +++ b/frappe/public/js/frappe/views/wiki.js @@ -12,7 +12,7 @@ frappe.views.Wiki = class Wiki { this.sections = {}; this.sidebar_items = {}; this.sorted_sidebar_items = []; - this.tools = {} + this.tools = {}; this.isReadOnly = true; this.new_page = null; this.prepare_container(); @@ -31,9 +31,9 @@ frappe.views.Wiki = class Wiki { setup_wiki_pages() { this.get_pages().then(() => { - if(this.all_pages) { + if (this.all_pages) { frappe.wiki_pages = {}; - let root_pages = this.all_pages.filter(page => page.parent_page == '' || page.parent_page == null) + let root_pages = this.all_pages.filter(page => page.parent_page == '' || page.parent_page == null); for (let page of this.all_pages || []) { frappe.wiki_pages[frappe.router.slug(page.name)] = page; } @@ -41,10 +41,10 @@ frappe.views.Wiki = class Wiki { this.make_sidebar(root_pages); } if (this.new_page) { - frappe.set_route(`wiki/${frappe.router.slug(this.new_page)}`) + frappe.set_route(`wiki/${frappe.router.slug(this.new_page)}`); this.new_page = null; } - }) + }); } get_pages() { @@ -55,7 +55,7 @@ frappe.views.Wiki = class Wiki { make_sidebar(items) { if (this.sidebar.find('.standard-sidebar-section')[0]) { - this.sidebar.find('.standard-sidebar-section')[0].remove() + this.sidebar.find('.standard-sidebar-section')[0].remove(); } let sidebar_section = $(`
`); @@ -95,25 +95,25 @@ frappe.views.Wiki = class Wiki {
`); - } + }; const make_sidebar_child_item = item => { let $child_item = get_child_item(item); - $child_item.appendTo(child_item_section) + $child_item.appendTo(child_item_section); this.sidebar_items[item.name] = $child_item; - } + }; let $item = get_sidebar_item(item); let drop_icon = $item.find('.drop-icon').get(0); let child_item_section = $item.find('.sidebar-child-item').get(0); - if(this.all_pages.some(e => e.parent_page == item.name)) { + if (this.all_pages.some(e => e.parent_page == item.name)) { drop_icon.classList.remove('hidden'); drop_icon.addEventListener('click', () => { child_item_section.classList.toggle("hidden"); }); } - let child_items = this.all_pages.filter(page => page.parent_page == item.name) + let child_items = this.all_pages.filter(page => page.parent_page == item.name); child_items.forEach(item => make_sidebar_child_item(item)); $item.appendTo(sidebar_section); @@ -138,7 +138,7 @@ frappe.views.Wiki = class Wiki { this.show_page(page); this.get_content(page).then(() => { this.get_data(page).then(() => { - if(this.content){ + if (this.content) { this.tools = { header: { class: Header, @@ -176,22 +176,22 @@ frappe.views.Wiki = class Wiki { }, blank: frappe.wiki_block.blocks['blank'], spacingTune: frappe.wiki_block.tunes['spacing_tune'], - } - if(this.editor) { + }; + if (this.editor) { this.editor.isReady.then(() => { this.editor.configuration.tools.chart.config.page_data = this.page_data; this.editor.configuration.tools.shortcut.config.page_data = this.page_data; this.editor.configuration.tools.card.config.page_data = this.page_data; this.editor.render({ blocks: JSON.parse(this.content) || [] - }) - }) + }); + }); } else { this.initialize_editorjs(JSON.parse(this.content)); } } - }) - }) + }); + }); } get_content(page) { @@ -257,14 +257,14 @@ frappe.views.Wiki = class Wiki { this.isReadOnly = false; this.editor.readOnly.toggle(); this.editor.isReady - .then(() => { - this.undo = new Undo({ editor: this.editor }); - this.undo.initialize({blocks: JSON.parse(this.content)}); - this.setup_customization_buttons(); - this.make_sidebar_sortable(); - this.make_blocks_sortable(); - // this.customize(); - }) + .then(() => { + this.undo = new Undo({ editor: this.editor }); + this.undo.initialize({blocks: JSON.parse(this.content)}); + this.setup_customization_buttons(); + this.make_sidebar_sortable(); + this.make_blocks_sortable(); + // this.customize(); + }); }, ); @@ -279,14 +279,14 @@ frappe.views.Wiki = class Wiki { handle: ".standard-sidebar-item-container", draggable: ".standard-sidebar-item-container", animation: 150, - onEnd: function (evt){ + onEnd: function (evt) { let new_sb_items = []; - let old_sb_items = me.all_pages.filter(page => page.parent_page == '' || page.parent_page == null) + let old_sb_items = me.all_pages.filter(page => page.parent_page == '' || page.parent_page == null); for (let page of evt.srcElement.childNodes) { new_sb_items.push({ name: page.attributes['item-name'].value, sequence_id: parseInt(page.attributes['item-sequence'].value) - }) + }); } me.sorted_sidebar_items = []; new_sb_items.forEach((old, index) => { @@ -305,7 +305,7 @@ frappe.views.Wiki = class Wiki { handle: ".drag-handle", draggable: ".ce-block", animation: 150, - onEnd: function (evt){ + onEnd: function (evt) { me.editor.blocks.move(evt.newIndex, evt.oldIndex); }, setData: function (dataTransfer, dragEl) { @@ -377,14 +377,14 @@ frappe.views.Wiki = class Wiki { } ] }).then(() => { - if(this.editor.configuration.readOnly) { + if (this.editor.configuration.readOnly) { this.isReadOnly = false; this.editor.readOnly.toggle(); } this.make_sidebar_sortable(); this.make_blocks_sortable(); this.dirty = false; - }) + }); } }); d.show(); @@ -394,7 +394,7 @@ frappe.views.Wiki = class Wiki { this.dirty = false; const data = { blocks: blocks || [] - } + }; this.editor = new EditorJS({ tools: this.tools, autofocus: false, @@ -421,11 +421,11 @@ frappe.views.Wiki = class Wiki { if (item.data.new) { if (!new_widgets[item.type]) { new_widgets[item.type] = [] - } + }; new_widgets[item.type].push(item.data.new); delete item.data['new']; } - }) + }); frappe.call({ method: "frappe.desk.doctype.internal_wiki_page.internal_wiki_page.save_wiki_page", @@ -445,7 +445,7 @@ frappe.views.Wiki = class Wiki { page: me.title, new_widgets: new_widgets }).then(res => { - if(res.message) { + if (res.message) { me.reload(); } }); @@ -460,7 +460,8 @@ frappe.views.Wiki = class Wiki { } }); }).catch((error) => { - console.log('Saving failed: ', error); + error; + // console.log('Saving failed: ', error); }); } @@ -468,4 +469,4 @@ frappe.views.Wiki = class Wiki { this.setup_wiki_pages(); this.dirty = false; } -} +}; diff --git a/frappe/public/js/frappe/wiki_blocks/blank.js b/frappe/public/js/frappe/wiki_blocks/blank.js index ae8a53fc33..f32007de6c 100644 --- a/frappe/public/js/frappe/wiki_blocks/blank.js +++ b/frappe/public/js/frappe/wiki_blocks/blank.js @@ -10,7 +10,7 @@ export default class Blank { return true; } - constructor({data, api, config, readOnly}){ + constructor({data, api, config, readOnly}) { this.data = data; this.api = api; this.config = config; @@ -39,22 +39,22 @@ export default class Blank { pr: this._getPadding("r"), pb: this._getPadding("b"), pl: this._getPadding("l") - } + }; } rendered() { var e = this.wrapper.parentNode.parentNode; - e.classList.add("col-" + this.col) - e.classList.add("pt-" + this.pt) - e.classList.add("pr-" + this.pr) - e.classList.add("pb-" + this.pb) - e.classList.add("pl-" + this.pl) + e.classList.add("col-" + this.col); + e.classList.add("pt-" + this.pt); + e.classList.add("pr-" + this.pr); + e.classList.add("pb-" + this.pb); + e.classList.add("pl-" + this.pl); } _getCol() { - var e = 12, - t = "col-12", - n = this.wrapper.parentNode.parentNode, + var e = 12; + t = "col-12"; + n = this.wrapper.parentNode.parentNode; r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { @@ -67,13 +67,13 @@ export default class Blank { } _getPadding() { - var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l", - t = 0, - n = "p" + e + "-0", - r = this.wrapper.parentNode.parentNode, - a = new RegExp(/\pl-.+?\b/, "g"), - i = new RegExp(/\pr-.+?\b/, "g"), - o = new RegExp(/\pt-.+?\b/, "g"), + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; + t = 0; + n = "p" + e + "-0"; + r = this.wrapper.parentNode.parentNode; + a = new RegExp(/\pl-.+?\b/, "g"); + i = new RegExp(/\pr-.+?\b/, "g"); + o = new RegExp(/\pt-.+?\b/, "g"); c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { diff --git a/frappe/public/js/frappe/wiki_blocks/blocks.js b/frappe/public/js/frappe/wiki_blocks/blocks.js index 1b556eeaee..239127a674 100644 --- a/frappe/public/js/frappe/wiki_blocks/blocks.js +++ b/frappe/public/js/frappe/wiki_blocks/blocks.js @@ -19,5 +19,5 @@ frappe.wiki_block.blocks = { }; frappe.wiki_block.tunes = { - spacing_tune : SpacingTune -} \ No newline at end of file + spacing_tune: SpacingTune +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/wiki_blocks/card.js b/frappe/public/js/frappe/wiki_blocks/card.js index cc79a57c3e..dd457fea4c 100644 --- a/frappe/public/js/frappe/wiki_blocks/card.js +++ b/frappe/public/js/frappe/wiki_blocks/card.js @@ -11,7 +11,7 @@ export default class Card { return true; } - constructor({data, api, config, readOnly, block}){ + constructor({data, api, config, readOnly}) { this.data = data; this.api = api; this.config = config; @@ -29,7 +29,7 @@ export default class Card { allow_delete: this.allow_customization, allow_hiding: false, allow_edit: false, - } + }; } render() { @@ -37,7 +37,7 @@ export default class Card { this._new_card(); if (this.data && this.data.card_name) { - this._make_cards(this.data.card_name) + this._make_cards(this.data.card_name); } return this.wrapper; } @@ -51,16 +51,16 @@ export default class Card { pb: this._getPadding("b"), pl: this._getPadding("l"), new: this.new_card_widget - } + }; } rendered() { var e = this.wrapper.parentNode.parentNode; - e.classList.add("col-" + this.col) - e.classList.add("pt-" + this.pt) - e.classList.add("pr-" + this.pr) - e.classList.add("pb-" + this.pb) - e.classList.add("pl-" + this.pl) + e.classList.add("col-" + this.col); + e.classList.add("pt-" + this.pt); + e.classList.add("pr-" + this.pr); + e.classList.add("pb-" + this.pb); + e.classList.add("pl-" + this.pl); } _new_card() { @@ -88,9 +88,9 @@ export default class Card { } _getCol() { - var e = 12, - t = "col-12", - n = this.wrapper.parentNode.parentNode, + var e = 12; + t = "col-12"; + n = this.wrapper.parentNode.parentNode; r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { @@ -103,13 +103,13 @@ export default class Card { } _getPadding() { - var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l", - t = 0, - n = "p" + e + "-0", - r = this.wrapper.parentNode.parentNode, - a = new RegExp(/\pl-.+?\b/, "g"), - i = new RegExp(/\pr-.+?\b/, "g"), - o = new RegExp(/\pt-.+?\b/, "g"), + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; + t = 0; + n = "p" + e + "-0"; + r = this.wrapper.parentNode.parentNode; + a = new RegExp(/\pl-.+?\b/, "g"); + i = new RegExp(/\pr-.+?\b/, "g"); + o = new RegExp(/\pt-.+?\b/, "g"); c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { @@ -155,7 +155,7 @@ export default class Card { _make_cards(card_name) { let card = this.config.page_data.cards.items.find(obj => { - return obj.label == card_name + return obj.label == card_name; }); this.wrapper.innerHTML = ''; card.in_customize_mode = !this.readOnly; diff --git a/frappe/public/js/frappe/wiki_blocks/chart.js b/frappe/public/js/frappe/wiki_blocks/chart.js index f8c97852fa..5acaa81627 100644 --- a/frappe/public/js/frappe/wiki_blocks/chart.js +++ b/frappe/public/js/frappe/wiki_blocks/chart.js @@ -10,7 +10,7 @@ export default class Chart { return true; } - constructor({data, api, config, readOnly}){ + constructor({data, api, config, readOnly}) { this.data = data; this.api = api; this.config = config; @@ -34,12 +34,12 @@ export default class Chart { options: this.config.page_data.charts.items.map(({ chart_name }) => chart_name), change: function() { if (this.value) { - me._make_charts(this.value) + me._make_charts(this.value); } } }]); if (this.data && this.data.chart_name) { - this._make_charts(this.data.chart_name) + this._make_charts(this.data.chart_name); } return this.wrapper; } @@ -52,22 +52,22 @@ export default class Chart { pr: this._getPadding("r"), pb: this._getPadding("b"), pl: this._getPadding("l") - } + }; } rendered() { var e = this.wrapper.parentNode.parentNode; - e.classList.add("col-" + this.col) - e.classList.add("pt-" + this.pt) - e.classList.add("pr-" + this.pr) - e.classList.add("pb-" + this.pb) - e.classList.add("pl-" + this.pl) + e.classList.add("col-" + this.col); + e.classList.add("pt-" + this.pt); + e.classList.add("pr-" + this.pr); + e.classList.add("pb-" + this.pb); + e.classList.add("pl-" + this.pl); } _getCol() { - var e = 12, - t = "col-12", - n = this.wrapper.parentNode.parentNode, + var e = 12; + t = "col-12"; + n = this.wrapper.parentNode.parentNode; r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { @@ -80,13 +80,13 @@ export default class Chart { } _getPadding() { - var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l", - t = 0, - n = "p" + e + "-0", - r = this.wrapper.parentNode.parentNode, - a = new RegExp(/\pl-.+?\b/, "g"), - i = new RegExp(/\pr-.+?\b/, "g"), - o = new RegExp(/\pt-.+?\b/, "g"), + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; + t = 0; + n = "p" + e + "-0"; + r = this.wrapper.parentNode.parentNode; + a = new RegExp(/\pl-.+?\b/, "g"); + i = new RegExp(/\pr-.+?\b/, "g"); + o = new RegExp(/\pt-.+?\b/, "g"); c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { @@ -132,7 +132,7 @@ export default class Chart { _make_charts(chart_name) { let chart = this.config.page_data.charts.items.find(obj => { - return obj.chart_name == chart_name + return obj.chart_name == chart_name; }); this.wrapper.innerHTML = ''; this.sections = {}; @@ -153,7 +153,7 @@ export default class Chart { }, widgets: chart }); - this.wrapper.setAttribute("chart_name", chart_name) + this.wrapper.setAttribute("chart_name", chart_name); if (!this.readOnly) { this.sections["charts"].customize(); } diff --git a/frappe/public/js/frappe/wiki_blocks/paragraph.js b/frappe/public/js/frappe/wiki_blocks/paragraph.js index 570a04e661..affc2b436d 100644 --- a/frappe/public/js/frappe/wiki_blocks/paragraph.js +++ b/frappe/public/js/frappe/wiki_blocks/paragraph.js @@ -62,7 +62,7 @@ export default class Paragraph { merge(data) { let newData = { - text : this.data.text + data.text + text: this.data.text + data.text }; this.data = newData; @@ -70,7 +70,7 @@ export default class Paragraph { validate(savedData) { if (savedData.text.trim() === '' && !this._preserveBlank) { - return false; + return false; } return true; @@ -89,17 +89,17 @@ export default class Paragraph { rendered() { var e = this._element.parentNode.parentNode; - e.classList.add("col-" + this.col) - e.classList.add("pt-" + this.pt) - e.classList.add("pr-" + this.pr) - e.classList.add("pb-" + this.pb) - e.classList.add("pl-" + this.pl) + e.classList.add("col-" + this.col); + e.classList.add("pt-" + this.pt); + e.classList.add("pr-" + this.pr); + e.classList.add("pb-" + this.pb); + e.classList.add("pl-" + this.pl); } _getCol() { - var e = 12, - t = "col-12", - n = this._element.parentNode.parentNode, + var e = 12; + t = "col-12"; + n = this._element.parentNode.parentNode; r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { @@ -112,13 +112,13 @@ export default class Paragraph { } _getPadding() { - var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l", - t = 0, - n = "p" + e + "-0", - r = this._element.parentNode.parentNode, - a = new RegExp(/\pl-.+?\b/, "g"), - i = new RegExp(/\pr-.+?\b/, "g"), - o = new RegExp(/\pt-.+?\b/, "g"), + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; + t = 0; + n = "p" + e + "-0"; + r = this._element.parentNode.parentNode; + a = new RegExp(/\pl-.+?\b/, "g"); + i = new RegExp(/\pr-.+?\b/, "g"); + o = new RegExp(/\pt-.+?\b/, "g"); c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { @@ -156,7 +156,7 @@ export default class Paragraph { onPaste(event) { const data = { - text: event.detail.data.innerHTML + text: event.detail.data.innerHTML }; this.data = data; @@ -164,16 +164,16 @@ export default class Paragraph { static get conversionConfig() { return { - export: 'text', // to convert Paragraph to other block, use 'text' property of saved data - import: 'text' // to covert other block's exported string to Paragraph, fill 'text' property of tool data + export: 'text', // to convert Paragraph to other block, use 'text' property of saved data + import: 'text' // to covert other block's exported string to Paragraph, fill 'text' property of tool data }; } static get sanitize() { return { - text: { - br: true, - } + text: { + br: true, + } }; } @@ -197,7 +197,7 @@ export default class Paragraph { static get pasteConfig() { return { - tags: [ 'P' ] + tags: [ 'P' ] }; } diff --git a/frappe/public/js/frappe/wiki_blocks/shortcut.js b/frappe/public/js/frappe/wiki_blocks/shortcut.js index 1a3d2ae970..7bcebb478a 100644 --- a/frappe/public/js/frappe/wiki_blocks/shortcut.js +++ b/frappe/public/js/frappe/wiki_blocks/shortcut.js @@ -11,7 +11,7 @@ export default class Shortcut { return true; } - constructor({data, api, config, readOnly}){ + constructor({data, api, config, readOnly}) { this.data = data; this.api = api; this.config = config; @@ -28,7 +28,7 @@ export default class Shortcut { allow_delete: this.allow_customization, allow_hiding: false, allow_edit: true, - } + }; } render() { @@ -36,7 +36,7 @@ export default class Shortcut { this._new_shortcut(); if (this.data && this.data.shortcut_name) { - this._make_shortcuts(this.data.shortcut_name) + this._make_shortcuts(this.data.shortcut_name); } return this.wrapper; } @@ -50,16 +50,16 @@ export default class Shortcut { pb: this._getPadding("b"), pl: this._getPadding("l"), new: this.new_shortcut_widget - } + }; } rendered() { var e = this.wrapper.parentNode.parentNode; - e.classList.add("col-" + this.col) - e.classList.add("pt-" + this.pt) - e.classList.add("pr-" + this.pr) - e.classList.add("pb-" + this.pb) - e.classList.add("pl-" + this.pl) + e.classList.add("col-" + this.col); + e.classList.add("pt-" + this.pt); + e.classList.add("pr-" + this.pr); + e.classList.add("pb-" + this.pb); + e.classList.add("pl-" + this.pl); } _new_shortcut() { @@ -87,9 +87,9 @@ export default class Shortcut { } _getCol() { - var e = 12, - t = "col-12", - n = this.wrapper.parentNode.parentNode, + var e = 12; + t = "col-12"; + n = this.wrapper.parentNode.parentNode; r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { @@ -102,13 +102,13 @@ export default class Shortcut { } _getPadding() { - var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l", - t = 0, - n = "p" + e + "-0", - r = this.wrapper.parentNode.parentNode, - a = new RegExp(/\pl-.+?\b/, "g"), - i = new RegExp(/\pr-.+?\b/, "g"), - o = new RegExp(/\pt-.+?\b/, "g"), + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; + t = 0; + n = "p" + e + "-0"; + r = this.wrapper.parentNode.parentNode; + a = new RegExp(/\pl-.+?\b/, "g"); + i = new RegExp(/\pr-.+?\b/, "g"); + o = new RegExp(/\pt-.+?\b/, "g"); c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { @@ -154,7 +154,7 @@ export default class Shortcut { _make_shortcuts(shortcut_name) { let shortcut = this.config.page_data.shortcuts.items.find(obj => { - return obj.label == shortcut_name + return obj.label == shortcut_name; }); this.wrapper.innerHTML = ''; shortcut.in_customize_mode = !this.readOnly; diff --git a/frappe/public/js/frappe/wiki_blocks/spacing_tune.js b/frappe/public/js/frappe/wiki_blocks/spacing_tune.js index 494ba2e97a..ee76503097 100644 --- a/frappe/public/js/frappe/wiki_blocks/spacing_tune.js +++ b/frappe/public/js/frappe/wiki_blocks/spacing_tune.js @@ -71,7 +71,7 @@ export default class SpacingTune { } let currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -82,13 +82,13 @@ export default class SpacingTune { let colClass = new RegExp(/\bcol-.+?\b/, 'g'); if (currentBlockElement.className.match(colClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(colClass)){ + if (cn.match(colClass)) { className = cn; } }); let parts = className.split('-'); let width = parseInt(parts[1]); - if(width >= 2){ + if (width >= 2) { currentBlockElement.classList.remove('col-'+width); width = width - 1; currentBlockElement.classList.add('col-'+width); @@ -104,7 +104,7 @@ export default class SpacingTune { } const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -115,23 +115,23 @@ export default class SpacingTune { const colClass = new RegExp(/\bcol-.+?\b/, 'g'); if (currentBlockElement.className.match(colClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(colClass)){ - className = cn; - } + if (cn.match(colClass)) { + className = cn; + } }); let parts = className.split('-'); let width = parseInt(parts[1]); - if(width <= 11){ - currentBlockElement.classList.remove('col-'+width); - width = width + 1; + if (width <= 11) { + currentBlockElement.classList.remove('col-'+width); + width = width + 1; currentBlockElement.classList.add('col-'+width); - } + } } } showPadding(event, button) { let me = this; - if(button.classList.contains('cdx-settings-button--active')){ + if (button.classList.contains('cdx-settings-button--active')) { this.sidebar.remove(); button.classList.remove('cdx-settings-button--active'); } else { @@ -290,12 +290,12 @@ export default class SpacingTune { increasePaddingLeft(event, button) { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); - if(currentBlockIndex < 0){ + if (currentBlockIndex < 0) { return; } const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -306,13 +306,13 @@ export default class SpacingTune { const paddingClass = new RegExp(/\pl-.+?\b/, 'g'); if (currentBlockElement.className.match(paddingClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(paddingClass)){ + if (cn.match(paddingClass)) { className = cn; } }); let parts = className.split('-'); let padding = parseInt(parts[1]); - if(padding <= 4){ + if (padding <= 4) { currentBlockElement.classList.remove('pl-'+padding); padding = padding + 1; currentBlockElement.classList.add('pl-'+padding); @@ -324,12 +324,12 @@ export default class SpacingTune { decreasePaddingLeft(event, button) { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); - if(currentBlockIndex < 0){ + if (currentBlockIndex < 0) { return; } const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -340,13 +340,13 @@ export default class SpacingTune { const paddingClass = new RegExp(/\pl-.+?\b/, 'g'); if (currentBlockElement.className.match(paddingClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(paddingClass)){ + if (cn.match(paddingClass)) { className = cn; } }); let parts = className.split('-'); let padding = parseInt(parts[1]); - if(padding >= 1){ + if (padding >= 1) { currentBlockElement.classList.remove('pl-'+padding); padding = padding - 1; currentBlockElement.classList.add('pl-'+padding); @@ -357,12 +357,12 @@ export default class SpacingTune { increasePaddingRight(event, button) { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); - if(currentBlockIndex < 0){ + if (currentBlockIndex < 0) { return; } const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -373,13 +373,13 @@ export default class SpacingTune { const paddingClass = new RegExp(/\pr-.+?\b/, 'g'); if (currentBlockElement.className.match(paddingClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(paddingClass)){ + if (cn.match(paddingClass)) { className = cn; } }); let parts = className.split('-'); let padding = parseInt(parts[1]); - if(padding <= 4){ + if (padding <= 4) { currentBlockElement.classList.remove('pr-'+padding); padding = padding + 1; currentBlockElement.classList.add('pr-'+padding); @@ -390,12 +390,12 @@ export default class SpacingTune { decreasePaddingRight(event, button) { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); - if(currentBlockIndex < 0){ + if (currentBlockIndex < 0) { return; } const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -406,13 +406,13 @@ export default class SpacingTune { const paddingClass = new RegExp(/\pr-.+?\b/, 'g'); if (currentBlockElement.className.match(paddingClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(paddingClass)){ + if (cn.match(paddingClass)) { className = cn; } }); let parts = className.split('-'); let padding = parseInt(parts[1]); - if(padding >= 1){ + if (padding >= 1) { currentBlockElement.classList.remove('pr-'+padding); padding = padding - 1; currentBlockElement.classList.add('pr-'+padding); @@ -423,12 +423,12 @@ export default class SpacingTune { increasePaddingTop(event, button) { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); - if(currentBlockIndex < 0){ + if (currentBlockIndex < 0) { return; } const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -439,13 +439,13 @@ export default class SpacingTune { const paddingClass = new RegExp(/\pt-.+?\b/, 'g'); if (currentBlockElement.className.match(paddingClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(paddingClass)){ + if (cn.match(paddingClass)) { className = cn; } }); let parts = className.split('-'); let padding = parseInt(parts[1]); - if(padding <= 4){ + if (padding <= 4) { currentBlockElement.classList.remove('pt-'+padding); padding = padding + 1; currentBlockElement.classList.add('pt-'+padding); @@ -456,12 +456,12 @@ export default class SpacingTune { decreasePaddingTop(event, button) { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); - if(currentBlockIndex < 0){ + if (currentBlockIndex < 0) { return; } const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -472,13 +472,13 @@ export default class SpacingTune { const paddingClass = new RegExp(/\pt-.+?\b/, 'g'); if (currentBlockElement.className.match(paddingClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(paddingClass)){ + if (cn.match(paddingClass)) { className = cn; } }); let parts = className.split('-'); let padding = parseInt(parts[1]); - if(padding >= 1){ + if (padding >= 1) { currentBlockElement.classList.remove('pt-'+padding); padding = padding - 1; currentBlockElement.classList.add('pt-'+padding); @@ -489,12 +489,12 @@ export default class SpacingTune { increasePaddingBottom(event, button) { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); - if(currentBlockIndex < 0){ + if (currentBlockIndex < 0) { return; } const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -505,13 +505,13 @@ export default class SpacingTune { const paddingClass = new RegExp(/\pb-.+?\b/, 'g'); if (currentBlockElement.className.match(paddingClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(paddingClass)){ + if (cn.match(paddingClass)) { className = cn; } }); let parts = className.split('-'); let padding = parseInt(parts[1]); - if(padding <= 4){ + if (padding <= 4) { currentBlockElement.classList.remove('pb-'+padding); padding = padding + 1; currentBlockElement.classList.add('pb-'+padding); @@ -522,12 +522,12 @@ export default class SpacingTune { decreasePaddingBottom(event, button) { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); - if(currentBlockIndex < 0){ + if (currentBlockIndex < 0) { return; } const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); - if (!currentBlock){ + if (!currentBlock) { return; } @@ -538,13 +538,13 @@ export default class SpacingTune { const paddingClass = new RegExp(/\pb-.+?\b/, 'g'); if (currentBlockElement.className.match(paddingClass)) { currentBlockElement.classList.forEach( cn => { - if(cn.match(paddingClass)){ + if (cn.match(paddingClass)) { className = cn; } }); let parts = className.split('-'); let padding = parseInt(parts[1]); - if(padding >= 1){ + if (padding >= 1) { currentBlockElement.classList.remove('pb-'+padding); padding = padding - 1; currentBlockElement.classList.add('pb-'+padding); diff --git a/frappe/public/scss/desk/wiki.scss b/frappe/public/scss/desk/wiki.scss index a04fb1e501..8c464a7a48 100644 --- a/frappe/public/scss/desk/wiki.scss +++ b/frappe/public/scss/desk/wiki.scss @@ -11,6 +11,28 @@ } } } + + .standard-sidebar-item { + justify-content: space-between; + } + + .sidebar-child-item-container { + margin-left: 10px; + + .standard-sidebar-item { + justify-content: start; + } + } + + .sidebar-item-label { + flex: 1; + } + + .item-anchor { + display: flex; + overflow: hidden; + flex: 1; + } } .ce-header { @@ -124,31 +146,4 @@ height: 145px; box-shadow: 0 3px 15px -3px rgba(13,20,33,.13); border-radius: 0 4px 4px 0;z-index: 0; -} - - -[data-page-route="wiki"] { - - .standard-sidebar-item { - justify-content: space-between; - } - - .sidebar-child-item-container { - margin-left: 10px; - - .standard-sidebar-item { - justify-content: start; - } - } - - .sidebar-item-label { - flex: 1; - } - - .item-anchor { - display: flex; - overflow: hidden; - flex: 1; - } - } \ No newline at end of file From 85f849d3e9d7303815a760a2296bf2374098e738 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Wed, 12 May 2021 14:39:11 +0530 Subject: [PATCH 0051/1519] fix: sider fix --- frappe/public/js/frappe/wiki_blocks/blank.js | 20 +++++++++---------- frappe/public/js/frappe/wiki_blocks/card.js | 20 +++++++++---------- frappe/public/js/frappe/wiki_blocks/chart.js | 20 +++++++++---------- .../public/js/frappe/wiki_blocks/paragraph.js | 20 +++++++++---------- .../public/js/frappe/wiki_blocks/shortcut.js | 20 +++++++++---------- 5 files changed, 50 insertions(+), 50 deletions(-) diff --git a/frappe/public/js/frappe/wiki_blocks/blank.js b/frappe/public/js/frappe/wiki_blocks/blank.js index f32007de6c..f23f2f6225 100644 --- a/frappe/public/js/frappe/wiki_blocks/blank.js +++ b/frappe/public/js/frappe/wiki_blocks/blank.js @@ -53,9 +53,9 @@ export default class Blank { _getCol() { var e = 12; - t = "col-12"; - n = this.wrapper.parentNode.parentNode; - r = new RegExp(/\bcol-.+?\b/, "g"); + var t = "col-12"; + var n = this.wrapper.parentNode.parentNode; + var r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { e.match(r) && (t = e); @@ -68,13 +68,13 @@ export default class Blank { _getPadding() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; - t = 0; - n = "p" + e + "-0"; - r = this.wrapper.parentNode.parentNode; - a = new RegExp(/\pl-.+?\b/, "g"); - i = new RegExp(/\pr-.+?\b/, "g"); - o = new RegExp(/\pt-.+?\b/, "g"); - c = new RegExp(/\pb-.+?\b/, "g"); + var t = 0; + var n = "p" + e + "-0"; + var r = this.wrapper.parentNode.parentNode; + var a = new RegExp(/\pl-.+?\b/, "g"); + var i = new RegExp(/\pr-.+?\b/, "g"); + var o = new RegExp(/\pt-.+?\b/, "g"); + var c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { r.classList.forEach(function (e) { diff --git a/frappe/public/js/frappe/wiki_blocks/card.js b/frappe/public/js/frappe/wiki_blocks/card.js index dd457fea4c..3e7fd54dfd 100644 --- a/frappe/public/js/frappe/wiki_blocks/card.js +++ b/frappe/public/js/frappe/wiki_blocks/card.js @@ -89,9 +89,9 @@ export default class Card { _getCol() { var e = 12; - t = "col-12"; - n = this.wrapper.parentNode.parentNode; - r = new RegExp(/\bcol-.+?\b/, "g"); + var t = "col-12"; + var n = this.wrapper.parentNode.parentNode; + var r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { e.match(r) && (t = e); @@ -104,13 +104,13 @@ export default class Card { _getPadding() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; - t = 0; - n = "p" + e + "-0"; - r = this.wrapper.parentNode.parentNode; - a = new RegExp(/\pl-.+?\b/, "g"); - i = new RegExp(/\pr-.+?\b/, "g"); - o = new RegExp(/\pt-.+?\b/, "g"); - c = new RegExp(/\pb-.+?\b/, "g"); + var t = 0; + var n = "p" + e + "-0"; + var r = this.wrapper.parentNode.parentNode; + var a = new RegExp(/\pl-.+?\b/, "g"); + var i = new RegExp(/\pr-.+?\b/, "g"); + var o = new RegExp(/\pt-.+?\b/, "g"); + var c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { r.classList.forEach(function (e) { diff --git a/frappe/public/js/frappe/wiki_blocks/chart.js b/frappe/public/js/frappe/wiki_blocks/chart.js index 5acaa81627..1d637ef5b6 100644 --- a/frappe/public/js/frappe/wiki_blocks/chart.js +++ b/frappe/public/js/frappe/wiki_blocks/chart.js @@ -66,9 +66,9 @@ export default class Chart { _getCol() { var e = 12; - t = "col-12"; - n = this.wrapper.parentNode.parentNode; - r = new RegExp(/\bcol-.+?\b/, "g"); + var t = "col-12"; + var n = this.wrapper.parentNode.parentNode; + var r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { e.match(r) && (t = e); @@ -81,13 +81,13 @@ export default class Chart { _getPadding() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; - t = 0; - n = "p" + e + "-0"; - r = this.wrapper.parentNode.parentNode; - a = new RegExp(/\pl-.+?\b/, "g"); - i = new RegExp(/\pr-.+?\b/, "g"); - o = new RegExp(/\pt-.+?\b/, "g"); - c = new RegExp(/\pb-.+?\b/, "g"); + var t = 0; + var n = "p" + e + "-0"; + var r = this.wrapper.parentNode.parentNode; + var a = new RegExp(/\pl-.+?\b/, "g"); + var i = new RegExp(/\pr-.+?\b/, "g"); + var o = new RegExp(/\pt-.+?\b/, "g"); + var c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { r.classList.forEach(function (e) { diff --git a/frappe/public/js/frappe/wiki_blocks/paragraph.js b/frappe/public/js/frappe/wiki_blocks/paragraph.js index affc2b436d..e80d300ec3 100644 --- a/frappe/public/js/frappe/wiki_blocks/paragraph.js +++ b/frappe/public/js/frappe/wiki_blocks/paragraph.js @@ -98,9 +98,9 @@ export default class Paragraph { _getCol() { var e = 12; - t = "col-12"; - n = this._element.parentNode.parentNode; - r = new RegExp(/\bcol-.+?\b/, "g"); + var t = "col-12"; + var n = this._element.parentNode.parentNode; + var r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { e.match(r) && (t = e); @@ -113,13 +113,13 @@ export default class Paragraph { _getPadding() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; - t = 0; - n = "p" + e + "-0"; - r = this._element.parentNode.parentNode; - a = new RegExp(/\pl-.+?\b/, "g"); - i = new RegExp(/\pr-.+?\b/, "g"); - o = new RegExp(/\pt-.+?\b/, "g"); - c = new RegExp(/\pb-.+?\b/, "g"); + var t = 0; + var n = "p" + e + "-0"; + var r = this._element.parentNode.parentNode; + var a = new RegExp(/\pl-.+?\b/, "g"); + var i = new RegExp(/\pr-.+?\b/, "g"); + var o = new RegExp(/\pt-.+?\b/, "g"); + var c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { r.classList.forEach(function (e) { diff --git a/frappe/public/js/frappe/wiki_blocks/shortcut.js b/frappe/public/js/frappe/wiki_blocks/shortcut.js index 7bcebb478a..53b5a5eed5 100644 --- a/frappe/public/js/frappe/wiki_blocks/shortcut.js +++ b/frappe/public/js/frappe/wiki_blocks/shortcut.js @@ -88,9 +88,9 @@ export default class Shortcut { _getCol() { var e = 12; - t = "col-12"; - n = this.wrapper.parentNode.parentNode; - r = new RegExp(/\bcol-.+?\b/, "g"); + var t = "col-12"; + var n = this.wrapper.parentNode.parentNode; + var r = new RegExp(/\bcol-.+?\b/, "g"); if (n.className.match(r)) { n.classList.forEach(function (e) { e.match(r) && (t = e); @@ -103,13 +103,13 @@ export default class Shortcut { _getPadding() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "l"; - t = 0; - n = "p" + e + "-0"; - r = this.wrapper.parentNode.parentNode; - a = new RegExp(/\pl-.+?\b/, "g"); - i = new RegExp(/\pr-.+?\b/, "g"); - o = new RegExp(/\pt-.+?\b/, "g"); - c = new RegExp(/\pb-.+?\b/, "g"); + var t = 0; + var n = "p" + e + "-0"; + var r = this.wrapper.parentNode.parentNode; + var a = new RegExp(/\pl-.+?\b/, "g"); + var i = new RegExp(/\pr-.+?\b/, "g"); + var o = new RegExp(/\pt-.+?\b/, "g"); + var c = new RegExp(/\pb-.+?\b/, "g"); if ("l" == e) { if (r.className.match(a)) { r.classList.forEach(function (e) { From 31e087d1a9b1f90a5bb9b737ca51f078e8dc3f58 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Wed, 12 May 2021 14:58:41 +0530 Subject: [PATCH 0052/1519] fix: sider fix --- frappe/public/js/frappe/views/wiki.js | 6 +-- frappe/public/js/frappe/wiki_blocks/blank.js | 2 +- .../js/frappe/wiki_blocks/spacing_tune.js | 44 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/frappe/public/js/frappe/views/wiki.js b/frappe/public/js/frappe/views/wiki.js index a7c41b66bc..ec4f2a49e8 100644 --- a/frappe/public/js/frappe/views/wiki.js +++ b/frappe/public/js/frappe/views/wiki.js @@ -308,7 +308,7 @@ frappe.views.Wiki = class Wiki { onEnd: function (evt) { me.editor.blocks.move(evt.newIndex, evt.oldIndex); }, - setData: function (dataTransfer, dragEl) { + setData: function () { //Do Nothing } }); @@ -420,8 +420,8 @@ frappe.views.Wiki = class Wiki { outputData.blocks.forEach(item => { if (item.data.new) { if (!new_widgets[item.type]) { - new_widgets[item.type] = [] - }; + new_widgets[item.type] = []; + } new_widgets[item.type].push(item.data.new); delete item.data['new']; } diff --git a/frappe/public/js/frappe/wiki_blocks/blank.js b/frappe/public/js/frappe/wiki_blocks/blank.js index f23f2f6225..761bd2d9e9 100644 --- a/frappe/public/js/frappe/wiki_blocks/blank.js +++ b/frappe/public/js/frappe/wiki_blocks/blank.js @@ -32,7 +32,7 @@ export default class Blank { return this.wrapper; } - save(blockContent) { + save() { return { col: this._getCol(), pt: this._getPadding("t"), diff --git a/frappe/public/js/frappe/wiki_blocks/spacing_tune.js b/frappe/public/js/frappe/wiki_blocks/spacing_tune.js index ee76503097..1cf6891071 100644 --- a/frappe/public/js/frappe/wiki_blocks/spacing_tune.js +++ b/frappe/public/js/frappe/wiki_blocks/spacing_tune.js @@ -37,7 +37,7 @@ export default class SpacingTune { this.api.listeners.on( paddingButton, 'click', - (event) => me.showPadding(event, paddingButton), + () => me.showPadding(paddingButton), false ); @@ -46,7 +46,7 @@ export default class SpacingTune { this.api.listeners.on( decreaseWidthButton, 'click', - (event) => me.decreaseWidth(event, decreaseWidthButton), + () => me.decreaseWidth(), false ); @@ -55,7 +55,7 @@ export default class SpacingTune { this.api.listeners.on( increaseWidthButton, 'click', - (event) => me.increaseWidth(event, increaseWidthButton), + () => me.increaseWidth(), false ); @@ -63,7 +63,7 @@ export default class SpacingTune { return layoutWrapper; } - decreaseWidth(event, button) { + decreaseWidth() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { @@ -96,7 +96,7 @@ export default class SpacingTune { } } - increaseWidth(event, button) { + increaseWidth() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { @@ -129,7 +129,7 @@ export default class SpacingTune { } } - showPadding(event, button) { + showPadding(button) { let me = this; if (button.classList.contains('cdx-settings-button--active')) { this.sidebar.remove(); @@ -195,7 +195,7 @@ export default class SpacingTune { this.api.listeners.on( increasePaddingLeft, 'click', - (event) => me.increasePaddingLeft(event, increasePaddingLeft), + () => me.increasePaddingLeft(), false ); sidebarWrapper.appendChild(increasePaddingLeft); @@ -206,7 +206,7 @@ export default class SpacingTune { this.api.listeners.on( decreasePaddingLeft, 'click', - (event) => me.decreasePaddingLeft(event, decreasePaddingLeft), + () => me.decreasePaddingLeft(), false ); sidebarWrapper.appendChild(decreasePaddingLeft); @@ -219,7 +219,7 @@ export default class SpacingTune { this.api.listeners.on( increasePaddingRight, 'click', - (event) => me.increasePaddingRight(event, increasePaddingRight), + () => me.increasePaddingRight(), false ); sidebarWrapper.appendChild(increasePaddingRight); @@ -230,7 +230,7 @@ export default class SpacingTune { this.api.listeners.on( decreasePaddingRight, 'click', - (event) => me.decreasePaddingRight(event, decreasePaddingRight), + () => me.decreasePaddingRight(), false ); sidebarWrapper.appendChild(decreasePaddingRight); @@ -243,7 +243,7 @@ export default class SpacingTune { this.api.listeners.on( increasePaddingTop, 'click', - (event) => me.increasePaddingTop(event, increasePaddingTop), + () => me.increasePaddingTop(), false ); sidebarWrapper.appendChild(increasePaddingTop); @@ -254,7 +254,7 @@ export default class SpacingTune { this.api.listeners.on( decreasePaddingTop, 'click', - (event) => me.decreasePaddingTop(event, decreasePaddingTop), + () => me.decreasePaddingTop(), false ); sidebarWrapper.appendChild(decreasePaddingTop); @@ -267,7 +267,7 @@ export default class SpacingTune { this.api.listeners.on( increasePaddingBottom, 'click', - (event) => me.increasePaddingBottom(event, increasePaddingBottom), + () => me.increasePaddingBottom(), false ); sidebarWrapper.appendChild(increasePaddingBottom); @@ -278,7 +278,7 @@ export default class SpacingTune { this.api.listeners.on( decreasePaddingBottom, 'click', - (event) => me.decreasePaddingBottom(event, decreasePaddingBottom), + () => me.decreasePaddingBottom(), false ); sidebarWrapper.appendChild(decreasePaddingBottom); @@ -287,7 +287,7 @@ export default class SpacingTune { } } - increasePaddingLeft(event, button) { + increasePaddingLeft() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { @@ -321,7 +321,7 @@ export default class SpacingTune { } - decreasePaddingLeft(event, button) { + decreasePaddingLeft() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { @@ -354,7 +354,7 @@ export default class SpacingTune { } } - increasePaddingRight(event, button) { + increasePaddingRight() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { @@ -387,7 +387,7 @@ export default class SpacingTune { } } - decreasePaddingRight(event, button) { + decreasePaddingRight() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { @@ -420,7 +420,7 @@ export default class SpacingTune { } } - increasePaddingTop(event, button) { + increasePaddingTop() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { @@ -453,7 +453,7 @@ export default class SpacingTune { } } - decreasePaddingTop(event, button) { + decreasePaddingTop() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { @@ -486,7 +486,7 @@ export default class SpacingTune { } } - increasePaddingBottom(event, button) { + increasePaddingBottom() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { @@ -519,7 +519,7 @@ export default class SpacingTune { } } - decreasePaddingBottom(event, button) { + decreasePaddingBottom() { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); if (currentBlockIndex < 0) { From 788c511274eda1e9ac452e739a83ecc16f4cdf98 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Wed, 12 May 2021 17:47:55 +0530 Subject: [PATCH 0053/1519] fix: Delete Card and Shortcut --- frappe/public/js/frappe/widgets/widget_group.js | 13 ++++++++----- frappe/public/js/frappe/wiki_blocks/card.js | 10 +++++++--- frappe/public/js/frappe/wiki_blocks/shortcut.js | 12 ++++++++---- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js index 3d97a8f778..2b85359330 100644 --- a/frappe/public/js/frappe/widgets/widget_group.js +++ b/frappe/public/js/frappe/widgets/widget_group.js @@ -205,18 +205,21 @@ export class SingleWidgetGroup { widget_type: this.type, container: this.container, height: this.height || null, - options: { - ...this.options, - on_delete: (name) => this.on_delete(name), - }, }); - + widget_object.options = { + ...this.options, + on_delete: (name) => this.on_delete(name) + }; this.widgets_list.push(widget_object); this.widgets_dict[widget.name] = widget_object; return widget_object; } + on_delete(name, setup_new) { + this.api.blocks.delete(); + } + customize() { this.widgets_list.forEach((wid) => { wid.customize(this.options); diff --git a/frappe/public/js/frappe/wiki_blocks/card.js b/frappe/public/js/frappe/wiki_blocks/card.js index 3e7fd54dfd..f06e4754e0 100644 --- a/frappe/public/js/frappe/wiki_blocks/card.js +++ b/frappe/public/js/frappe/wiki_blocks/card.js @@ -73,9 +73,12 @@ export default class Card { let wid = frappe.widget.make_widget({ ...widget, widget_type: 'links', - container: this.wrapper, - options: this.options, + container: this.wrapper }); + wid.options = { + ...this.options, + on_delete: () => this.api.blocks.delete() + } wid.customize(this.options); this.wrapper.setAttribute("card_name", wid.label); this.new_card_widget = wid.get_config(); @@ -163,7 +166,8 @@ export default class Card { container: this.wrapper, type: "links", options: this.options, - widgets: card + widgets: card, + api: this.api }); this.wrapper.setAttribute("card_name", card_name); if (!this.readOnly) { diff --git a/frappe/public/js/frappe/wiki_blocks/shortcut.js b/frappe/public/js/frappe/wiki_blocks/shortcut.js index 53b5a5eed5..138ff8eac7 100644 --- a/frappe/public/js/frappe/wiki_blocks/shortcut.js +++ b/frappe/public/js/frappe/wiki_blocks/shortcut.js @@ -27,7 +27,7 @@ export default class Shortcut { allow_create: this.allow_customization, allow_delete: this.allow_customization, allow_hiding: false, - allow_edit: true, + allow_edit: true }; } @@ -72,9 +72,12 @@ export default class Shortcut { let wid = frappe.widget.make_widget({ ...widget, widget_type: 'shortcut', - container: this.wrapper, - options: this.options, + container: this.wrapper }); + wid.options = { + ...this.options, + on_delete: () => this.api.blocks.delete(), + } wid.customize(this.options); this.wrapper.setAttribute("shortcut_name", wid.label); this.new_shortcut_widget = wid.get_config(); @@ -162,7 +165,8 @@ export default class Shortcut { container: this.wrapper, type: "shortcut", options: this.options, - widgets: shortcut + widgets: shortcut, + api: this.api }); this.wrapper.setAttribute("shortcut_name", shortcut_name); if (!this.readOnly) { From fed99bffac29ff4cb99866d6a3e3246170525916 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 13 May 2021 13:34:56 +0530 Subject: [PATCH 0054/1519] fix: Set basepath and toc data to show sidebar --- frappe/website/page_controllers/template_page.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/website/page_controllers/template_page.py b/frappe/website/page_controllers/template_page.py index b050a62bad..49a811b587 100644 --- a/frappe/website/page_controllers/template_page.py +++ b/frappe/website/page_controllers/template_page.py @@ -5,6 +5,7 @@ import frappe from frappe.website.page_controllers.base_template_page import BaseTemplatePage from frappe.website.context import add_sidebar_and_breadcrumbs from frappe.website.render import build_response +from frappe.website.router import get_base_template from frappe.website.utils import (extract_comment_tag, extract_title, get_next_link, get_toc) @@ -31,6 +32,7 @@ class TemplatePage(BaseTemplatePage): self.app = app self.app_path = app_path self.template_path = os.path.relpath(file_path, self.app_path) + self.basepath = os.path.dirname(file_path) return True def get_index_path_options(self, search_path): @@ -74,7 +76,6 @@ class TemplatePage(BaseTemplatePage): same folder. Also the hyphens will be coverted to underscore for python module names. This method sets the pymodule_name if it exists. ''' - self.basepath = self.template_path.rsplit('.', 1)[0] self.pymodule_name = None # replace - with _ in the internal modules names @@ -90,6 +91,9 @@ class TemplatePage(BaseTemplatePage): self.convert_from_markdown() def update_context(self): + self.context.base_template = self.context.base_template or get_base_template(self.path) + self.context.basepath = self.basepath + self.context.path = self.path self.set_page_properties() self.set_properties_from_source() self.load_colocated_files() @@ -196,7 +200,7 @@ class TemplatePage(BaseTemplatePage): def convert_from_markdown(self): if self.template_path.endswith('.md'): self.source = frappe.utils.md_to_html(self.source) - self.page_toc_html = self.source.toc_html + self.context.page_toc_html = self.source.toc_html if not self.context.show_sidebar: self.source = '
' + self.source + '
' From dbd8b66d815c8d255fb493c97eadb68671dcb844 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 13 May 2021 14:22:26 +0530 Subject: [PATCH 0055/1519] fix: Template basepath --- frappe/website/page_controllers/template_page.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/website/page_controllers/template_page.py b/frappe/website/page_controllers/template_page.py index 49a811b587..4db6d17541 100644 --- a/frappe/website/page_controllers/template_page.py +++ b/frappe/website/page_controllers/template_page.py @@ -76,10 +76,11 @@ class TemplatePage(BaseTemplatePage): same folder. Also the hyphens will be coverted to underscore for python module names. This method sets the pymodule_name if it exists. ''' + template_basepath = os.path.splitext(self.template_path)[0] self.pymodule_name = None # replace - with _ in the internal modules names - self.pymodule_path = os.path.join(self.basepath.replace("-", "_") + ".py") + self.pymodule_path = os.path.join(template_basepath.replace("-", "_") + ".py") if os.path.exists(os.path.join(self.app_path, self.pymodule_path)): self.pymodule_name = self.app + "." + self.pymodule_path.replace(os.path.sep, ".")[:-3] From 24434ed9dee018f4910898979f90d596f3f64dc3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 13:26:45 +0530 Subject: [PATCH 0056/1519] refactor: Delete unused code from render.py --- frappe/website/render.py | 174 +-------------------------------------- 1 file changed, 2 insertions(+), 172 deletions(-) diff --git a/frappe/website/render.py b/frappe/website/render.py index b7221e364c..ee80fa3b9f 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -22,112 +22,11 @@ from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, from frappe.website.router import clear_sitemap, evaluate_dynamic_routes from frappe.translate import guess_language +from frappe.website.serve import get_response class PageNotFoundError(Exception): pass def render(path=None, http_status_code=None): - # temp feature flag - if True or frappe.conf.flag_new_website: - from frappe.website.serve import get_response - return get_response(path, http_status_code) - else: - return _render(path, http_status_code) - -def _render(path=None, http_status_code=None): - """render html page""" - if not path: - path = frappe.local.request.path - - try: - path = path.strip('/ ') - raise_if_disabled(path) - resolve_redirect(path) - path = resolve_path(path) - data = None - - # if in list of already known 404s, send it - if can_cache() and frappe.cache().hget('website_404', frappe.request.url): - data = render_page('404') - http_status_code = 404 - elif is_static_file(path): - return get_static_file_response() - elif is_web_form(path): - data = render_web_form(path) - else: - try: - data = render_page_by_language(path) - except frappe.PageDoesNotExistError: - doctype, name = get_doctype_from_path(path) - if doctype and name: - path = "printview" - frappe.local.form_dict.doctype = doctype - frappe.local.form_dict.name = name - elif doctype: - path = "list" - frappe.local.form_dict.doctype = doctype - else: - # 404s are expensive, cache them! - frappe.cache().hset('website_404', frappe.request.url, True) - data = render_page('404') - http_status_code = 404 - - if not data: - try: - data = render_page(path) - except frappe.PermissionError as e: - data, http_status_code = render_403(e, path) - - except frappe.PermissionError as e: - data, http_status_code = render_403(e, path) - - except frappe.Redirect as e: - raise e - - except Exception: - path = "error" - data = render_page(path) - http_status_code = 500 - - data = add_csrf_token(data) - - except frappe.Redirect: - return build_response(path, "", 301, { - "Location": frappe.flags.redirect_location or (frappe.local.response or {}).get('location'), - "Cache-Control": "no-store, no-cache, must-revalidate" - }) - - return build_response(path, data, http_status_code or 200) - -def is_static_file(path): - if ('.' not in path): - return False - extn = path.rsplit('.', 1)[-1] - if extn in ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json'): - return False - - for app in frappe.get_installed_apps(): - file_path = frappe.get_app_path(app, 'www') + '/' + path - if os.path.exists(file_path): - frappe.flags.file_path = file_path - return True - - return False - -def is_web_form(path): - return bool(frappe.get_all("Web Form", filters={'route': path})) - -def render_web_form(path): - data = render_page(path) - return data - -def get_static_file_response(): - try: - f = open(frappe.flags.file_path, 'rb') - except IOError: - raise NotFound - - response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True) - response.mimetype = mimetypes.guess_type(frappe.flags.file_path)[0] or 'application/octet-stream' - return response + return get_response(path, http_status_code) def build_response(path, data, http_status_code, headers=None): # build response @@ -168,23 +67,6 @@ def add_preload_headers(response): traceback.print_exc() -def render_page_by_language(path): - translated_languages = frappe.get_hooks("translated_languages_for_website") - user_lang = guess_language(translated_languages) - if translated_languages and user_lang in translated_languages: - try: - if path and path != "index": - lang_path = '{0}/{1}'.format(user_lang, path) - else: - lang_path = user_lang # index - - return render_page(lang_path) - except frappe.DoesNotExistError: - return render_page(path) - - else: - return render_page(path) - def render_page(path): """get page html""" out = None @@ -307,7 +189,6 @@ def set_content_type(response, data, path): def clear_cache(path=None): '''Clear website caches - :param path: (optional) for the given path''' for key in ('website_generator_routes', 'website_pages', 'website_full_index', 'sitemap_routes'): @@ -327,54 +208,3 @@ def clear_cache(path=None): for method in frappe.get_hooks("website_clear_cache"): frappe.get_attr(method)(path) - -def render_403(e, pathname): - frappe.local.message = cstr(e.message if six.PY2 else e) - frappe.local.message_title = _("Not Permitted") - frappe.local.response['context'] = dict( - indicator_color = 'red', - primary_action = '/login', - primary_label = _('Login'), - fullpage=True - ) - return render_page("message"), e.http_status_code - -def get_doctype_from_path(path): - doctypes = frappe.db.sql_list("select name from tabDocType") - - parts = path.split("/") - - doctype = parts[0] - name = parts[1] if len(parts) > 1 else None - - if doctype in doctypes: - return doctype, name - - # try scrubbed - doctype = doctype.replace("_", " ").title() - if doctype in doctypes: - return doctype, name - - return None, None - -def add_csrf_token(data): - if frappe.local.session: - return data.replace("", ''.format( - frappe.local.session.data.csrf_token)) - else: - return data - -def raise_if_disabled(path): - routes = frappe.db.get_all('Portal Menu Item', - fields=['route', 'enabled'], - filters={ - 'enabled': 0, - 'route': ['like', '%{0}'.format(path)] - } - ) - - for r in routes: - _path = r.route.lstrip('/') - if path == _path and not r.enabled: - raise frappe.PermissionError - From 4219ef98d18e1b9caf00558bf1adca96c76e3154 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 13:31:13 +0530 Subject: [PATCH 0057/1519] fix: Update renderer sequence --- frappe/website/serve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/serve.py b/frappe/website/serve.py index eacaef96e1..f11ba1c4fc 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -29,7 +29,7 @@ def get_response(path=None, http_status_code=200): path = resolve_path(path) # there is no way to determine the type of the page based on the route # so evaluate each type of page sequentially - renderers = [StaticPage, TemplatePage, ListPage, WebFormPage, DocumentPage, PrintPage, NotFoundPage] + renderers = [StaticPage, WebFormPage, TemplatePage, ListPage, DocumentPage, PrintPage, NotFoundPage] for renderer in renderers: response = renderer(path, http_status_code).get() if response: From 39c1a94f896fff0e1781b8df6b60047cb8b7e286 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 13:53:04 +0530 Subject: [PATCH 0058/1519] fix: Circular import --- frappe/website/page_controllers/web_page.py | 1 + frappe/website/render.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/website/page_controllers/web_page.py b/frappe/website/page_controllers/web_page.py index 2020d491e4..4d9e642b4b 100644 --- a/frappe/website/page_controllers/web_page.py +++ b/frappe/website/page_controllers/web_page.py @@ -7,6 +7,7 @@ class WebPage(object): if not path: path = frappe.local.request.path self.path = path.strip('/ ') + self.basepath = '' def get(self): if self.validate(): diff --git a/frappe/website/render.py b/frappe/website/render.py index ee80fa3b9f..3868a91232 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -22,10 +22,10 @@ from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, from frappe.website.router import clear_sitemap, evaluate_dynamic_routes from frappe.translate import guess_language -from frappe.website.serve import get_response class PageNotFoundError(Exception): pass def render(path=None, http_status_code=None): + from frappe.website.serve import get_response return get_response(path, http_status_code) def build_response(path, data, http_status_code, headers=None): From 56ee6e449cedd64856bc9af9539006d5149266c8 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 14:17:52 +0530 Subject: [PATCH 0059/1519] refactor: Remove render method - also, move clear cache to utils.py --- frappe/utils/response.py | 3 ++- frappe/website/render.py | 33 +++++---------------------------- frappe/website/router.py | 3 --- frappe/website/utils.py | 27 ++++++++++++++++++++++++++- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 44b037f3cc..97097ab280 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -217,7 +217,8 @@ def send_private_file(path): return response def handle_session_stopped(): + from frappe.website.serve import get_response frappe.respond_as_web_page(_("Updating"), _("Your system is being updated. Please refresh again after a few moments."), http_status_code=503, indicator_color='orange', fullpage = True, primary_action=None) - return frappe.website.render.render("message", http_status_code=503) + return get_response("message", http_status_code=503) diff --git a/frappe/website/render.py b/frappe/website/render.py index 3868a91232..895faa5b5f 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -6,27 +6,21 @@ import frappe from frappe import _ import frappe.sessions from frappe.utils import cstr -import os, mimetypes, json +import mimetypes, json import re -import six from six import iteritems from werkzeug.wrappers import Response 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, evaluate_dynamic_routes -from frappe.translate import guess_language +from frappe.website.router import evaluate_dynamic_routes class PageNotFoundError(Exception): pass -def render(path=None, http_status_code=None): - from frappe.website.serve import get_response - return get_response(path, http_status_code) def build_response(path, data, http_status_code, headers=None): # build response @@ -188,23 +182,6 @@ def set_content_type(response, data, path): return data def clear_cache(path=None): - '''Clear website caches - :param path: (optional) for the given path''' - for key in ('website_generator_routes', 'website_pages', - 'website_full_index', 'sitemap_routes'): - frappe.cache().delete_value(key) - - frappe.cache().delete_value("website_404") - if path: - frappe.cache().hdel('website_redirects', path) - delete_page_cache(path) - else: - clear_sitemap() - frappe.clear_cache("Guest") - for key in ('portal_menu_items', 'home_page', 'website_route_rules', - 'doctypes_with_web_view', 'website_redirects', 'page_context', - 'website_page'): - frappe.cache().delete_value(key) - - for method in frappe.get_hooks("website_clear_cache"): - frappe.get_attr(method)(path) + # TODO: Remove this + from frappe.website.utils import clear_cache + return clear_cache(path) diff --git a/frappe/website/router.py b/frappe/website/router.py index 1e61f6dcc3..36779923b5 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -85,9 +85,6 @@ def get_page_context_from_doctype(path): return frappe.get_doc(page_info.get("doctype"), page_info.get("name")).get_page_info() -def clear_sitemap(): - delete_page_cache("*") - def get_all_page_context_from_doctypes(): ''' Get all doctype generated routes (for sitemap.xml) diff --git a/frappe/website/utils.py b/frappe/website/utils.py index efa15bdb14..8c75508d07 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -11,7 +11,6 @@ from six import iteritems from past.builtins import cmp from frappe.utils import md_to_html - def delete_page_cache(path): cache = frappe.cache() cache.delete_value('full_index') @@ -399,3 +398,29 @@ def get_html_content_based_on_type(doc, fieldname, content_type): content = '' return content + + +def clear_cache(path=None): + '''Clear website caches + :param path: (optional) for the given path''' + for key in ('website_generator_routes', 'website_pages', + 'website_full_index', 'sitemap_routes'): + frappe.cache().delete_value(key) + + frappe.cache().delete_value("website_404") + if path: + frappe.cache().hdel('website_redirects', path) + delete_page_cache(path) + else: + clear_sitemap() + frappe.clear_cache("Guest") + for key in ('portal_menu_items', 'home_page', 'website_route_rules', + 'doctypes_with_web_view', 'website_redirects', 'page_context', + 'website_page'): + frappe.cache().delete_value(key) + + for method in frappe.get_hooks("website_clear_cache"): + frappe.get_attr(method)(path) + +def clear_sitemap(): + delete_page_cache("*") From 9ba5bf1133b05d2435e74099227609970501c87c Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 18:42:41 +0530 Subject: [PATCH 0060/1519] refactor: Move metatags related code into a separate file --- .../page_controllers/base_template_page.py | 75 ++----------------- frappe/website/website_components/metatags.py | 68 +++++++++++++++++ 2 files changed, 73 insertions(+), 70 deletions(-) create mode 100644 frappe/website/website_components/metatags.py diff --git a/frappe/website/page_controllers/base_template_page.py b/frappe/website/page_controllers/base_template_page.py index 74ca400372..c40afb7b1c 100644 --- a/frappe/website/page_controllers/base_template_page.py +++ b/frappe/website/page_controllers/base_template_page.py @@ -1,13 +1,13 @@ import frappe -from frappe.website.doctype.website_settings.website_settings import \ - get_website_settings +from frappe.website.doctype.website_settings.website_settings import get_website_settings from frappe.website.page_controllers.web_page import WebPage +from frappe.website.website_components.metatags import MetaTags class BaseTemplatePage(WebPage): def init_context(self): self.context = frappe._dict() - self.context.update(get_website_settings(self.context)) + self.context.update(get_website_settings()) self.context.update(frappe.local.conf.get("website_context") or {}) def add_csrf_token(self, html): @@ -19,7 +19,8 @@ class BaseTemplatePage(WebPage): return html def post_process_context(self): - self.add_metatags() + self.tags = MetaTags(self.path, self.context).tags + self.context.metatags = self.tags self.set_base_template_if_missing() self.set_title_with_prefix() self.update_website_context() @@ -53,69 +54,3 @@ class BaseTemplatePage(WebPage): values = frappe.get_attr(method)(self.context) if values: self.context.update(values) - - def add_metatags(self): - self.tags = frappe._dict(self.context.get("metatags") or {}) - self.init_metatags_from_context() - self.set_opengraph_tags() - self.set_twitter_tags() - self.set_meta_published_on() - self.set_metatags_from_website_route_meta() - - self.context.metatags = self.tags - - def init_metatags_from_context(self): - for key in ('title', 'description', 'image', 'author', 'url', 'published_on'): - if key not in self.tags and self.context.get(key): - self.tags[key] = self.context[key] - - if not self.tags.get('title'): - self.tags['title'] = self.context.get('name') - - if self.tags.get('image'): - self.tags['image'] = frappe.utils.get_url(self.tags['image']) - - self.tags["language"] = frappe.local.lang or "en" - - def set_opengraph_tags(self): - if "og:type" not in self.tags: - self.tags["og:type"] = "article" - - for key in ('title', 'description', 'image', 'author', 'url'): - if self.tags.get(key): - self.tags['og:' + key] = self.tags.get(key) - - def set_twitter_tags(self): - for key in ('title', 'description', 'image', 'author', 'url'): - if self.tags.get(key): - self.tags['twitter:' + key] = self.tags.get(key) - - if self.tags.get('image'): - self.tags['twitter:card'] = "summary_large_image" - else: - self.tags["twitter:card"] = "summary" - - def set_meta_published_on(self): - if "published_on" in self.tags: - self.tags["datePublished"] = self.tags["published_on"] - del self.tags["published_on"] - - def set_metatags_from_website_route_meta(self): - ''' - Get meta tags from Website Route meta - they can override the defaults set above - ''' - route = self.path - if route == '': - # homepage - route = frappe.db.get_single_value('Website Settings', 'home_page') - - route_exists = (route - and not route.endswith(('.js', '.css')) - and frappe.db.exists('Website Route Meta', route)) - - if route_exists: - website_route_meta = frappe.get_doc('Website Route Meta', route) - for meta_tag in website_route_meta.meta_tags: - d = meta_tag.get_meta_dict() - self.tags.update(d) diff --git a/frappe/website/website_components/metatags.py b/frappe/website/website_components/metatags.py new file mode 100644 index 0000000000..045bef8fe1 --- /dev/null +++ b/frappe/website/website_components/metatags.py @@ -0,0 +1,68 @@ +import frappe + +class MetaTags(): + def __init__(self, path, context): + self.path = path + self.context = context + self.tags = frappe._dict(self.context.get("metatags") or {}) + self.init_metatags_from_context() + self.set_opengraph_tags() + self.set_twitter_tags() + self.set_meta_published_on() + self.set_metatags_from_website_route_meta() + + def init_metatags_from_context(self): + for key in ('title', 'description', 'image', 'author', 'url', 'published_on'): + if key not in self.tags and self.context.get(key): + self.tags[key] = self.context[key] + + if not self.tags.get('title'): + self.tags['title'] = self.context.get('name') + + if self.tags.get('image'): + self.tags['image'] = frappe.utils.get_url(self.tags['image']) + + self.tags["language"] = frappe.local.lang or "en" + + def set_opengraph_tags(self): + if "og:type" not in self.tags: + self.tags["og:type"] = "article" + + for key in ('title', 'description', 'image', 'author', 'url'): + if self.tags.get(key): + self.tags['og:' + key] = self.tags.get(key) + + def set_twitter_tags(self): + for key in ('title', 'description', 'image', 'author', 'url'): + if self.tags.get(key): + self.tags['twitter:' + key] = self.tags.get(key) + + if self.tags.get('image'): + self.tags['twitter:card'] = "summary_large_image" + else: + self.tags["twitter:card"] = "summary" + + def set_meta_published_on(self): + if "published_on" in self.tags: + self.tags["datePublished"] = self.tags["published_on"] + del self.tags["published_on"] + + def set_metatags_from_website_route_meta(self): + ''' + Get meta tags from Website Route meta + they can override the defaults set above + ''' + route = self.path + if route == '': + # homepage + route = frappe.db.get_single_value('Website Settings', 'home_page') + + route_exists = (route + and not route.endswith(('.js', '.css')) + and frappe.db.exists('Website Route Meta', route)) + + if route_exists: + website_route_meta = frappe.get_doc('Website Route Meta', route) + for meta_tag in website_route_meta.meta_tags: + d = meta_tag.get_meta_dict() + self.tags.update(d) From 20bab0f631acd5dfbf3ddda250472568f057fc6a Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 18:58:04 +0530 Subject: [PATCH 0061/1519] refactor: Move code to appropriate places --- frappe/utils/__init__.py | 4 +-- .../website/page_controllers/template_page.py | 36 +++---------------- frappe/website/router.py | 2 +- frappe/website/utils.py | 16 +++++++++ 4 files changed, 24 insertions(+), 34 deletions(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 43a7b82c16..845cb9be4a 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -770,9 +770,9 @@ def set_request(**kwargs): frappe.local.request = Request(builder.get_environ()) def get_html_for_route(route): - from frappe.website import render + from frappe.website.serve import get_response set_request(method='GET', path=route) - response = render.render() + response = get_response() html = frappe.safe_decode(response.get_data()) return html diff --git a/frappe/website/page_controllers/template_page.py b/frappe/website/page_controllers/template_page.py index 4db6d17541..f2664fb928 100644 --- a/frappe/website/page_controllers/template_page.py +++ b/frappe/website/page_controllers/template_page.py @@ -6,9 +6,10 @@ from frappe.website.page_controllers.base_template_page import BaseTemplatePage from frappe.website.context import add_sidebar_and_breadcrumbs from frappe.website.render import build_response from frappe.website.router import get_base_template -from frappe.website.utils import (extract_comment_tag, extract_title, - get_next_link, get_toc) +from frappe.website.utils import (extract_comment_tag, + extract_title, get_next_link, get_toc, get_frontmatter) +WEBPAGE_PY_MODULE_PROPERTIES = ("base_template_path", "template", "no_cache", "sitemap", "condition_field") class TemplatePage(BaseTemplatePage): def validate(self): @@ -36,12 +37,7 @@ class TemplatePage(BaseTemplatePage): return True def get_index_path_options(self, search_path): - return ( - search_path, - search_path + '.html', - search_path + '.md', - search_path + '/index.html', - search_path + '/index.md') + return (f'{search_path}{d}' for d in ('', '.html', '.md', '/index.html', '/index.md')) def render(self): return build_response(self.path, self.get_html(), self.http_status_code, self.headers) @@ -115,8 +111,7 @@ class TemplatePage(BaseTemplatePage): self.http_status_code = self.context.http_status_code def set_pymodule_properties(self): - for prop in ("base_template_path", "template", "no_cache", "sitemap", - "condition_field"): + for prop in WEBPAGE_PY_MODULE_PROPERTIES: if hasattr(self.pymodule, prop): self.context[prop] = getattr(self.pymodule, prop) @@ -237,24 +232,3 @@ class TemplatePage(BaseTemplatePage): def get_start_folders(): return frappe.local.flags.web_pages_folders or ('www', 'templates/pages') - -def get_frontmatter(string): - """ - Reference: https://github.com/jonbeebe/frontmatter - """ - import re - - 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, - } diff --git a/frappe/website/router.py b/frappe/website/router.py index 36779923b5..b048fa55c0 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -9,7 +9,7 @@ import re 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 frappe.website.utils import can_cache, extract_comment_tag, extract_title from werkzeug.routing import Map, Rule, NotFound def resolve_route(path): diff --git a/frappe/website/utils.py b/frappe/website/utils.py index 8c75508d07..87e47571fb 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -6,6 +6,8 @@ import functools import re import os import frappe +import re +import yaml from six import iteritems from past.builtins import cmp @@ -424,3 +426,17 @@ def clear_cache(path=None): def clear_sitemap(): delete_page_cache("*") + +def get_frontmatter(string): + "Reference: https://github.com/jonbeebe/frontmatter" + frontmatter = "" + body = "" + result = re.compile(r'^\s*(?:---|\+\+\+)(.*?)(?:---|\+\+\+)\s*(.+)$', re.S | re.M).search(string) + if result: + frontmatter = result.group(1) + body = result.group(2) + + return { + "attributes": yaml.safe_load(frontmatter), + "body": body, + } From 18497989dc71d24c3e7662382fb58cc2d84c1e0a Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 21:30:28 +0530 Subject: [PATCH 0062/1519] refactor: Remove render_page from render.py & replace all usages of render_page with get_response --- frappe/__init__.py | 4 +- frappe/search/website_search.py | 4 +- frappe/tests/test_recorder.py | 4 +- frappe/utils/global_search.py | 4 +- .../website/doctype/web_form/test_web_form.py | 1 - frappe/website/render.py | 50 +++---------------- frappe/website/router.py | 6 +-- 7 files changed, 19 insertions(+), 54 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 02b8d71e40..bd46f0f874 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1485,7 +1485,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None, :param style: Print Format style. :param as_pdf: Return as PDF. Default False. :param password: Password to encrypt the pdf with. Default None""" - from frappe.website.render import build_page + from frappe.website.serve import get_response from frappe.utils.pdf import get_pdf local.form_dict.doctype = doctype @@ -1500,7 +1500,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None, options = {'password': password} if not html: - html = build_page("printview") + html = get_response("printview") if as_pdf: return get_pdf(html, output = output, options = options) diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py index 452ea2a427..ad2ebb504d 100644 --- a/frappe/search/website_search.py +++ b/frappe/search/website_search.py @@ -9,7 +9,7 @@ from whoosh.fields import ID, TEXT, Schema import frappe from frappe.search.full_text_search import FullTextSearch from frappe.utils import set_request, update_progress_bar -from frappe.website.render import render_page +from frappe.website.serve import get_response INDEX_NAME = "web_routes" @@ -61,7 +61,7 @@ class WebsiteSearch(FullTextSearch): try: set_request(method="GET", path=route) - content = render_page(route) + content = get_response(route) soup = BeautifulSoup(content, "html.parser") page_content = soup.find(class_="page_content") text_content = page_content.text if page_content else "" diff --git a/frappe/tests/test_recorder.py b/frappe/tests/test_recorder.py index 64d3c52f63..7dcccd1d71 100644 --- a/frappe/tests/test_recorder.py +++ b/frappe/tests/test_recorder.py @@ -8,7 +8,7 @@ import unittest import frappe import frappe.recorder from frappe.utils import set_request -from frappe.website.render import render_page +from frappe.website.serve import get_response import sqlparse @@ -122,5 +122,5 @@ class TestRecorder(unittest.TestCase): self.assertEqual(call['exact_copies'], query[1]) def test_error_page_rendering(self): - content = render_page("error") + content = get_response("error") self.assertIn("Error", content) diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index c20f3b29d4..7541e8b31d 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -310,14 +310,14 @@ def get_routes_to_index(): def add_route_to_global_search(route): from bs4 import BeautifulSoup - from frappe.website.render import render_page + from frappe.website.serve import get_response from frappe.utils import set_request frappe.set_user('Guest') frappe.local.no_cache = True try: set_request(method='GET', path=route) - content = render_page(route) + content = get_response(route) soup = BeautifulSoup(content, 'html.parser') page_content = soup.find(class_='page_content') text_content = page_content.text if page_content else '' diff --git a/frappe/website/doctype/web_form/test_web_form.py b/frappe/website/doctype/web_form/test_web_form.py index 78f7fd6337..0e9ce7c660 100644 --- a/frappe/website/doctype/web_form/test_web_form.py +++ b/frappe/website/doctype/web_form/test_web_form.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import frappe import unittest, json -from frappe.website.render import build_page from frappe.website.doctype.web_form.web_form import accept test_dependencies = ['Web Form'] diff --git a/frappe/website/render.py b/frappe/website/render.py index 895faa5b5f..77c1af1e87 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -1,23 +1,21 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals -import frappe -from frappe import _ -import frappe.sessions -from frappe.utils import cstr -import mimetypes, json +import json +import mimetypes import re from six import iteritems -from werkzeug.wrappers import Response from werkzeug.routing import Rule -from werkzeug.wsgi import wrap_file +from werkzeug.wrappers import Response +import frappe +import frappe.sessions +from frappe import _ from frappe.website.context import get_context -from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, - get_toc, get_next_link) from frappe.website.router import evaluate_dynamic_routes +from frappe.website.utils import (can_cache, get_home_page, get_next_link, get_toc) + class PageNotFoundError(Exception): pass @@ -61,38 +59,6 @@ def add_preload_headers(response): traceback.print_exc() -def render_page(path): - """get page html""" - out = None - - if can_cache(): - # return rendered page - page_cache = frappe.cache().hget("website_page", path) - if page_cache and frappe.local.lang in page_cache: - out = page_cache[frappe.local.lang] - - if out: - frappe.local.response.from_cache = True - return out - - return build(path) - -def build(path): - if not frappe.db: - frappe.connect() - - try: - return build_page(path) - except frappe.DoesNotExistError: - hooks = frappe.get_hooks() - if hooks.website_catch_all: - path = hooks.website_catch_all[0] - return build_page(path) - else: - raise - except Exception: - raise - def build_page(path): if not getattr(frappe.local, "path", None): frappe.local.path = path diff --git a/frappe/website/router.py b/frappe/website/router.py index b048fa55c0..2e27b4c690 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -1,16 +1,16 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals - import io import os import re +from werkzeug.routing import Map, NotFound, Rule + import frappe from frappe.model.document import get_controller from frappe.website.utils import can_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. From 080a5b3c6a1395fe085d6d7d7218449139353da9 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 21:33:39 +0530 Subject: [PATCH 0063/1519] chore: Delete an unnecessary file - purifycss is no longer used. --- frappe/website/purifycss.py | 43 ------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 frappe/website/purifycss.py diff --git a/frappe/website/purifycss.py b/frappe/website/purifycss.py deleted file mode 100644 index 39e989db6e..0000000000 --- a/frappe/website/purifycss.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import print_function, unicode_literals -''' -Check for unused CSS Classes - -sUpdate source and target apps below and run from CLI - - bench --site [sitename] execute frappe.website.purifycss.purify.css - -''' - -import frappe, re, os - -source = frappe.get_app_path('frappe_theme', 'public', 'less', 'frappe_theme.less') -target_apps = ['erpnext_com', 'frappe_io', 'translator', 'chart_of_accounts_builder', 'frappe_theme'] - -def purifycss(): - with open(source, 'r') as f: - src = f.read() - - classes = [] - for line in src.splitlines(): - line = line.strip() - if not line: - continue - if line[0]=='@': - continue - classes.extend(re.findall('\.([^0-9][^ :&.{,(]*)', line)) - - classes = list(set(classes)) - - for app in target_apps: - for basepath, folders, files in os.walk(frappe.get_app_path(app)): - for fname in files: - if fname.endswith('.html') or fname.endswith('.md'): - #print 'checking {0}...'.format(fname) - with open(os.path.join(basepath, fname), 'r') as f: - src = f.read() - for c in classes: - if c in src: - classes.remove(c) - - for c in sorted(classes): - print(c) From c964217106538a8950961cb2a1df4445c3fb0291 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 21:40:44 +0530 Subject: [PATCH 0064/1519] style: Fix import issues --- frappe/website/render.py | 4 ---- frappe/website/utils.py | 1 - 2 files changed, 5 deletions(-) diff --git a/frappe/website/render.py b/frappe/website/render.py index 77c1af1e87..87eddf3463 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -11,15 +11,11 @@ from werkzeug.wrappers import Response import frappe import frappe.sessions -from frappe import _ from frappe.website.context import get_context from frappe.website.router import evaluate_dynamic_routes from frappe.website.utils import (can_cache, get_home_page, get_next_link, get_toc) -class PageNotFoundError(Exception): pass - - def build_response(path, data, http_status_code, headers=None): # build response response = Response() diff --git a/frappe/website/utils.py b/frappe/website/utils.py index 87e47571fb..a5fe9c3986 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -6,7 +6,6 @@ import functools import re import os import frappe -import re import yaml from six import iteritems From c5b981524868141f2dee58fa7fc5c24e6584bc70 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 14 May 2021 23:43:36 +0530 Subject: [PATCH 0065/1519] fix: Add get_response_content to get page content --- frappe/__init__.py | 4 ++-- frappe/search/website_search.py | 4 ++-- frappe/utils/global_search.py | 8 ++++---- frappe/website/serve.py | 4 ++++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index bd46f0f874..a90fd24d7b 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1485,7 +1485,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None, :param style: Print Format style. :param as_pdf: Return as PDF. Default False. :param password: Password to encrypt the pdf with. Default None""" - from frappe.website.serve import get_response + from frappe.website.serve import get_response_content from frappe.utils.pdf import get_pdf local.form_dict.doctype = doctype @@ -1500,7 +1500,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None, options = {'password': password} if not html: - html = get_response("printview") + html = get_response_content("printview") if as_pdf: return get_pdf(html, output = output, options = options) diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py index ad2ebb504d..49bdade936 100644 --- a/frappe/search/website_search.py +++ b/frappe/search/website_search.py @@ -9,7 +9,7 @@ from whoosh.fields import ID, TEXT, Schema import frappe from frappe.search.full_text_search import FullTextSearch from frappe.utils import set_request, update_progress_bar -from frappe.website.serve import get_response +from frappe.website.serve import get_response_content INDEX_NAME = "web_routes" @@ -61,7 +61,7 @@ class WebsiteSearch(FullTextSearch): try: set_request(method="GET", path=route) - content = get_response(route) + content = get_response_content(route) soup = BeautifulSoup(content, "html.parser") page_content = soup.find(class_="page_content") text_content = page_content.text if page_content else "" diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 7541e8b31d..4c78d63918 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -310,14 +310,14 @@ def get_routes_to_index(): def add_route_to_global_search(route): from bs4 import BeautifulSoup - from frappe.website.serve import get_response + from frappe.website.serve import get_response_content from frappe.utils import set_request frappe.set_user('Guest') frappe.local.no_cache = True try: set_request(method='GET', path=route) - content = get_response(route) + content = get_response_content(route) soup = BeautifulSoup(content, 'html.parser') page_content = soup.find(class_='page_content') text_content = page_content.text if page_content else '' @@ -332,8 +332,8 @@ def add_route_to_global_search(route): route=route ) sync_value_in_queue(value) - except (frappe.PermissionError, frappe.DoesNotExistError, frappe.ValidationError, Exception): - pass + except (frappe.PermissionError, frappe.DoesNotExistError, frappe.ValidationError, Exception) as e: + raise e frappe.set_user('Administrator') diff --git a/frappe/website/serve.py b/frappe/website/serve.py index f11ba1c4fc..47ca0aa1d2 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -46,3 +46,7 @@ def get_response(path=None, http_status_code=200): response = ErrorPage(path, http_status_code, exception=e).get() return response + +def get_response_content(path=None, http_status_code=200): + response = get_response(path, http_status_code) + return response.data From 9b26307fe5e2210a6c36645e220d077c1e997090 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 May 2021 12:23:50 +0530 Subject: [PATCH 0066/1519] test: Use get_response_content to get content --- frappe/tests/test_recorder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/tests/test_recorder.py b/frappe/tests/test_recorder.py index 7dcccd1d71..8a1ae36ad9 100644 --- a/frappe/tests/test_recorder.py +++ b/frappe/tests/test_recorder.py @@ -8,7 +8,7 @@ import unittest import frappe import frappe.recorder from frappe.utils import set_request -from frappe.website.serve import get_response +from frappe.website.serve import get_response_content import sqlparse @@ -122,5 +122,5 @@ class TestRecorder(unittest.TestCase): self.assertEqual(call['exact_copies'], query[1]) def test_error_page_rendering(self): - content = get_response("error") + content = get_response_content("error") self.assertIn("Error", content) From e07fb1f736e459af0668fa83e8a5c23e5793a4d4 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 17 May 2021 15:50:39 +0530 Subject: [PATCH 0067/1519] fix: Edit Shortcut --- frappe/desk/desktop.py | 6 ++++ frappe/desk/doctype/workspace/workspace.py | 5 ---- frappe/public/js/frappe/views/wiki.js | 30 +++++++++---------- .../public/js/frappe/widgets/base_widget.js | 1 + .../public/js/frappe/widgets/widget_group.js | 9 ++++-- frappe/public/js/frappe/wiki_blocks/card.js | 2 +- .../public/js/frappe/wiki_blocks/shortcut.js | 30 ++++++++++++------- 7 files changed, 49 insertions(+), 34 deletions(-) diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index c096f50b78..2a09978744 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +import json from json import loads, dumps from frappe import _, DoesNotExistError, ValidationError, _dict from frappe.boot import get_allowed_pages, get_allowed_reports @@ -549,6 +550,11 @@ def save_new_widget(page, new_widgets): if widgets.card: original_page.build_links_table_from_card(widgets.card) + content = frappe.db.get_value("Internal Wiki Page", page, "content") + for wid in ['shortcut']: + widd = [x['data'][ wid + '_name'] for x in json.loads(content) if x['type'] == wid] + original_page.set(wid+'s', [ele for ele in original_page.get(wid+'s') if ele.label in widd]) + try: original_page.save(ignore_permissions=True) except (ValidationError, TypeError) as e: diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 20c9240188..18aa383455 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -101,13 +101,8 @@ class Workspace(Document): }) def build_links_table_from_card(self, config): - # Empty links table - # self.links = [] - # order = config.get('order') - # widgets = config.get('widgets') for idx, card in enumerate(config): - # card = widgets[name].copy() links = loads(card.get('links')) self.append('links', { diff --git a/frappe/public/js/frappe/views/wiki.js b/frappe/public/js/frappe/views/wiki.js index ec4f2a49e8..057ae25902 100644 --- a/frappe/public/js/frappe/views/wiki.js +++ b/frappe/public/js/frappe/views/wiki.js @@ -404,6 +404,7 @@ frappe.views.Wiki = class Wiki { this.dirty = true; }, readOnly: true, + logLevel: 'ERROR' }); } @@ -440,22 +441,19 @@ frappe.views.Wiki = class Wiki { frappe.dom.unfreeze(); if (res.message) { let cur_page = res.message; - if (!$.isEmptyObject(new_widgets)) { - frappe.call('frappe.desk.desktop.save_new_widget', { - page: me.title, - new_widgets: new_widgets - }).then(res => { - if (res.message) { - me.reload(); - } - }); - } - frappe.show_alert({ message: __("Page Saved Successfully"), indicator: "green" }); - me.title = ''; - me.parent = ''; - me.sorted_sidebar_items = []; - me.new_page = cur_page; - me.reload(); + frappe.call('frappe.desk.desktop.save_new_widget', { + page: me.title, + new_widgets: new_widgets + }).then(res => { + if (res.message) { + frappe.show_alert({ message: __("Page Saved Successfully"), indicator: "green" }); + me.title = ''; + me.parent = ''; + me.sorted_sidebar_items = []; + me.new_page = cur_page; + me.reload(); + } + }); } } }); diff --git a/frappe/public/js/frappe/widgets/base_widget.js b/frappe/public/js/frappe/widgets/base_widget.js index 9bbfb916e5..821df609ef 100644 --- a/frappe/public/js/frappe/widgets/base_widget.js +++ b/frappe/public/js/frappe/widgets/base_widget.js @@ -170,6 +170,7 @@ export default class Widget { data.name = this.name; this.refresh(); + this.options.on_edit && this.options.on_edit(data); }, primary_action_label: __("Save") }); diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js index 2b85359330..913a362932 100644 --- a/frappe/public/js/frappe/widgets/widget_group.js +++ b/frappe/public/js/frappe/widgets/widget_group.js @@ -208,7 +208,8 @@ export class SingleWidgetGroup { }); widget_object.options = { ...this.options, - on_delete: (name) => this.on_delete(name) + on_delete: () => this.on_delete(), + on_edit: () => this.on_edit(widget_object) }; this.widgets_list.push(widget_object); this.widgets_dict[widget.name] = widget_object; @@ -216,10 +217,14 @@ export class SingleWidgetGroup { return widget_object; } - on_delete(name, setup_new) { + on_delete() { this.api.blocks.delete(); } + on_edit(widget_object) { + this.block.call("on_edit", widget_object); + } + customize() { this.widgets_list.forEach((wid) => { wid.customize(this.options); diff --git a/frappe/public/js/frappe/wiki_blocks/card.js b/frappe/public/js/frappe/wiki_blocks/card.js index f06e4754e0..89821129e0 100644 --- a/frappe/public/js/frappe/wiki_blocks/card.js +++ b/frappe/public/js/frappe/wiki_blocks/card.js @@ -78,7 +78,7 @@ export default class Card { wid.options = { ...this.options, on_delete: () => this.api.blocks.delete() - } + }; wid.customize(this.options); this.wrapper.setAttribute("card_name", wid.label); this.new_card_widget = wid.get_config(); diff --git a/frappe/public/js/frappe/wiki_blocks/shortcut.js b/frappe/public/js/frappe/wiki_blocks/shortcut.js index 138ff8eac7..f57c9ef7f1 100644 --- a/frappe/public/js/frappe/wiki_blocks/shortcut.js +++ b/frappe/public/js/frappe/wiki_blocks/shortcut.js @@ -11,9 +11,10 @@ export default class Shortcut { return true; } - constructor({data, api, config, readOnly}) { + constructor({data, api, config, readOnly, block}) { this.data = data; this.api = api; + this.block = block; this.config = config; this.readOnly = readOnly; this.col = this.data.col ? this.data.col : "12"; @@ -69,18 +70,19 @@ export default class Shortcut { type: 'shortcut', primary_action: (widget) => { widget.in_customize_mode = 1; - let wid = frappe.widget.make_widget({ + this.shortcut_widget = frappe.widget.make_widget({ ...widget, widget_type: 'shortcut', container: this.wrapper }); - wid.options = { + this.shortcut_widget.options = { ...this.options, on_delete: () => this.api.blocks.delete(), - } - wid.customize(this.options); - this.wrapper.setAttribute("shortcut_name", wid.label); - this.new_shortcut_widget = wid.get_config(); + on_edit: () => this.on_edit(this.shortcut_widget) + }; + this.shortcut_widget.customize(this.options); + this.wrapper.setAttribute("shortcut_name", this.shortcut_widget.label); + this.new_shortcut_widget = this.shortcut_widget.get_config(); }, }); @@ -155,22 +157,30 @@ export default class Shortcut { this.shortcut_field.make(); } + on_edit(shortcut_obj) { + let shortcut = shortcut_obj.get_config(); + this.shortcut_widget.widgets = shortcut; + this.wrapper.setAttribute("shortcut_name", shortcut.label); + this.new_shortcut_widget = shortcut_obj.get_config(); + } + _make_shortcuts(shortcut_name) { let shortcut = this.config.page_data.shortcuts.items.find(obj => { return obj.label == shortcut_name; }); this.wrapper.innerHTML = ''; shortcut.in_customize_mode = !this.readOnly; - let shortcut_widget = new frappe.widget.SingleWidgetGroup({ + this.shortcut_widget = new frappe.widget.SingleWidgetGroup({ container: this.wrapper, type: "shortcut", options: this.options, widgets: shortcut, - api: this.api + api: this.api, + block: this.block }); this.wrapper.setAttribute("shortcut_name", shortcut_name); if (!this.readOnly) { - shortcut_widget.customize(); + this.shortcut_widget.customize(); } } } \ No newline at end of file From 1c4e1bc1df7e0788e235b4f735194d8567de6e58 Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 14 Apr 2021 14:48:15 +0530 Subject: [PATCH 0068/1519] refactor: set amended docname to original docname --- frappe/core/doctype/doctype/doctype.py | 16 ++++++ frappe/model/document.py | 10 +++- frappe/model/naming.py | 53 +++++++++++++++++-- frappe/patches.txt | 1 + ...l_name_docfield_to_submittable_doctypes.py | 11 ++++ frappe/public/js/frappe/form/form.js | 24 +++++---- frappe/public/js/frappe/router.js | 6 +++ 7 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 84673f990a..3890ab3a32 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -74,6 +74,7 @@ class DocType(Document): if not self.istable: validate_permissions(self) + self.make_cancellable() self.make_amendable() self.make_repeatable() self.validate_nestedset() @@ -589,6 +590,21 @@ class DocType(Document): "no_copy": 1 }) + def make_cancellable(self): + """If is_submittable is set, add original_name docfield.""" + if self.is_submittable: + if not frappe.db.sql("""select name from tabDocField + where fieldname = 'original_name' and parent = %s""", self.name): + self.append("fields", { + "label": "Original Name", + "fieldtype": "Text", + "fieldname": "original_name", + "read_only": 1, + "hidden": 1, + "print_hide": 1, + "no_copy": 1 + }) + def make_repeatable(self): """If allow_auto_repeat is set, add auto_repeat custom field.""" if self.allow_auto_repeat: diff --git a/frappe/model/document.py b/frappe/model/document.py index 623916597e..2bb4f1f3c0 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -7,7 +7,7 @@ import time from frappe import _, msgprint, is_whitelisted from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff from frappe.model.base_document import BaseDocument, get_controller -from frappe.model.naming import set_new_name +from frappe.model.naming import set_new_name, rename_cancelled_doc from six import iteritems, string_types from werkzeug.exceptions import NotFound, Forbidden import hashlib, json @@ -919,6 +919,14 @@ class Document(BaseDocument): @whitelist.__func__ def _cancel(self): """Cancel the document. Sets `docstatus` = 2, then saves.""" + + # for backward compatibility + if self.amended_from and not self.original_name: + self.original_name = None + else: + self.original_name = self.name + + self.name = rename_cancelled_doc(self) self.docstatus = 2 self.save() diff --git a/frappe/model/naming.py b/frappe/model/naming.py index b8d6a6f8d7..33a09c659a 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -223,7 +223,16 @@ def revert_series_if_last(key, name, doc=None): * prefix = #### and hashes = 2021 (hash doesn't exist) * will search hash in key then accordingly get prefix = "" """ - if ".#" in key: + + # do not revert if doc is amended, since cancelled docs still exist + if doc.docstatus != 2 and doc.amended_from: + return + + # for first cancelled doc + if doc.docstatus == 2 and not doc.amended_from and doc.original_name: + name = doc.original_name + + if ".#" in key: prefix, hashes = key.rsplit(".", 1) if "#" not in hashes: # get the hash part from the key @@ -306,14 +315,48 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-" def _set_amended_name(doc): + if doc.original_name: + doc.name = doc.original_name + else: + original_name = get_original_name(doc) + doc.name = doc.amended_from + + # rename original doc to next name in series, and set amended doc name as original name + next_name_in_series = get_new_name_from_amended_from(doc) + frappe.rename_doc(doc.doctype, original_name, next_name_in_series, force=True, show_alert=False) + doc.name = original_name + doc.amended_from = next_name_in_series + doc.original_name = original_name + + return doc.name + +def get_original_name(doc): + # get original doc name from chain of amended docs + amended_from = original_name = doc.amended_from + while amended_from: + original_name = amended_from + amended_from = frappe.db.get_value(doc.doctype, amended_from, "amended_from") + + return original_name + +def get_new_name_from_amended_from(doc): am_id = 1 - am_prefix = doc.amended_from - if frappe.db.get_value(doc.doctype, doc.amended_from, "amended_from"): + am_prefix = doc.name + if frappe.db.get_value(doc.doctype, doc.name, "amended_from"): am_id = cint(doc.amended_from.split("-")[-1]) + 1 am_prefix = "-".join(doc.amended_from.split("-")[:-1]) # except the last hyphen - doc.name = am_prefix + "-" + str(am_id) - return doc.name + new_name = am_prefix + "-" + str(am_id) + if new_name == doc.name: + am_id += 1 + new_name = am_prefix + "-" + str(am_id) + return new_name + +def rename_cancelled_doc(doc): + doc = frappe.parse_json(doc) + new_name = get_new_name_from_amended_from(doc) + frappe.rename_doc(doc.doctype, doc.name, new_name, force=True, show_alert=False) + return new_name def _field_autoname(autoname, doc, skip_slicing=None): diff --git a/frappe/patches.txt b/frappe/patches.txt index e70be0a37b..a722bb39fa 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -337,3 +337,4 @@ frappe.patches.v12_0.rename_uploaded_files_with_proper_name frappe.patches.v13_0.queryreport_columns frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty +frappe.patches.v13_0.add_original_name_docfield_to_submittable_doctypes ####### diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py new file mode 100644 index 0000000000..725d37fd4a --- /dev/null +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -0,0 +1,11 @@ +import frappe +from frappe.database.schema import add_column + +def execute(): + for doctype in frappe.db.get_all('DocType'): + doctype = frappe.get_doc("DocType", doctype.name) + if doctype.is_submittable and frappe.db.table_exists(doctype.name): + doctype.make_cancellable() + if not frappe.db.has_column(doctype.name, 'original_name'): + add_column(doctype.name, 'original_name', 'Text') + doctype.db_update_all() diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 6d8a6b1cb4..f9d9de967e 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -764,32 +764,36 @@ frappe.ui.form.Form = class FrappeForm { } _cancel(btn, callback, on_error, skip_confirm) { - const me = this; const cancel_doc = () => { frappe.validated = true; - me.script_manager.trigger("before_cancel").then(() => { + this.script_manager.trigger("before_cancel").then(() => { if (!frappe.validated) { - return me.handle_save_fail(btn, on_error); + return this.handle_save_fail(btn, on_error); } - var after_cancel = function(r) { + const original_name = this.docname; + const after_cancel = (r) => { if (r.exc) { - me.handle_save_fail(btn, on_error); + this.handle_save_fail(btn, on_error); } else { frappe.utils.play_sound("cancel"); - me.refresh(); callback && callback(); - me.script_manager.trigger("after_cancel"); + this.script_manager.trigger("after_cancel"); + frappe.run_serially([ + () => this.rename_notify(this.doctype, original_name, r.docs[0].name), + () => frappe.router.clear_re_route(this.doctype, original_name), + () => this.refresh(), + ]); } }; - frappe.ui.form.save(me, "cancel", after_cancel, btn); + frappe.ui.form.save(this, "cancel", after_cancel, btn); }); } if (skip_confirm) { cancel_doc(); } else { - frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, me.handle_save_fail(btn, on_error)); + frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, this.handle_save_fail(btn, on_error)); } }; @@ -811,7 +815,7 @@ frappe.ui.form.Form = class FrappeForm { 'docname': this.doc.name }).then(is_amended => { if (is_amended) { - frappe.throw(__('This document is already amended, you cannot ammend it again')); + frappe.throw(__('This document is already amended, you cannot amend it again')); } this.validate_form_action("Amend"); var me = this; diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index 5378294855..0d5231260c 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -235,6 +235,12 @@ frappe.router = { } }, + clear_re_route(doctype, docname) { + delete frappe.re_route[ + `${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(docname)}` + ]; + }, + set_title(sub_path) { if (frappe.route_titles[sub_path]) { frappe.utils.set_title(frappe.route_titles[sub_path]); From 251f878009f8f0e5574307ffd4275cd6ff36e3bc Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 15 Apr 2021 11:20:16 +0530 Subject: [PATCH 0069/1519] test: test naming for cancelled and amended docs --- frappe/tests/test_naming.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 66d48e3612..765ead275f 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -117,3 +117,36 @@ class TestNaming(unittest.TestCase): self.assertEqual(current_index.get('current'), 2) frappe.db.sql("""delete from `tabSeries` where name = %s""", series) + + def test_naming_for_cancelled_and_amended_doc(self): + if not frappe.db.exists('DocType', 'Submittable Doctype'): + frappe.get_doc({ + "doctype": "DocType", + "module": "Core", + "custom": 1, + "is_submittable": 1, + "permissions": [{ + "role": "System Manager", + "read": 1 + }], + "name": 'Submittable Doctype' + }).insert() + + doc = frappe.new_doc('Submittable DocType') + doc.save() + original_name = doc.name + + doc.submit() + doc.cancel() + cancelled_name = doc.name + self.assertEqual(cancelled_name, "{}-1".format(original_name)) + + amended_doc = frappe.copy_doc(doc) + amended_doc.docstatus = 0 + amended_doc.amended_from = doc.name + amended_doc.save() + self.assertEqual(amended_doc.name, original_name) + + amended_doc.submit() + amended_doc.cancel() + self.assertEqual(amended_doc.name, "{}-2".format(original_name)) From 304a771ba2221aa15cb9f186799a0603cb5ee425 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 15 Apr 2021 11:21:48 +0530 Subject: [PATCH 0070/1519] fix: check if doc has attribute amended_from --- frappe/model/naming.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 33a09c659a..b685886c58 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -224,13 +224,14 @@ def revert_series_if_last(key, name, doc=None): * will search hash in key then accordingly get prefix = "" """ - # do not revert if doc is amended, since cancelled docs still exist - if doc.docstatus != 2 and doc.amended_from: - return + if hasattr(doc, 'amended_from'): + # do not revert if doc is amended, since cancelled docs still exist + if doc.docstatus != 2 and doc.amended_from: + return - # for first cancelled doc - if doc.docstatus == 2 and not doc.amended_from and doc.original_name: - name = doc.original_name + # for first cancelled doc + if doc.docstatus == 2 and not doc.amended_from and doc.original_name: + name = doc.original_name if ".#" in key: prefix, hashes = key.rsplit(".", 1) From 3614ca2a4d9ed0831cd3bd7080e977a5c2622149 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 15 Apr 2021 12:17:53 +0530 Subject: [PATCH 0071/1519] style: fix indent --- frappe/core/doctype/doctype/doctype.py | 18 +++++++++--------- frappe/tests/test_naming.py | 23 +++++++++++------------ 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 3890ab3a32..0d478015bb 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -580,15 +580,15 @@ class DocType(Document): if self.is_submittable: if not frappe.db.sql("""select name from tabDocField where fieldname = 'amended_from' and parent = %s""", self.name): - self.append("fields", { - "label": "Amended From", - "fieldtype": "Link", - "fieldname": "amended_from", - "options": self.name, - "read_only": 1, - "print_hide": 1, - "no_copy": 1 - }) + self.append("fields", { + "label": "Amended From", + "fieldtype": "Link", + "fieldname": "amended_from", + "options": self.name, + "read_only": 1, + "print_hide": 1, + "no_copy": 1 + }) def make_cancellable(self): """If is_submittable is set, add original_name docfield.""" diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 765ead275f..afaabcc805 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -119,18 +119,17 @@ class TestNaming(unittest.TestCase): frappe.db.sql("""delete from `tabSeries` where name = %s""", series) def test_naming_for_cancelled_and_amended_doc(self): - if not frappe.db.exists('DocType', 'Submittable Doctype'): - frappe.get_doc({ - "doctype": "DocType", - "module": "Core", - "custom": 1, - "is_submittable": 1, - "permissions": [{ - "role": "System Manager", - "read": 1 - }], - "name": 'Submittable Doctype' - }).insert() + frappe.get_doc({ + "doctype": "DocType", + "module": "Core", + "custom": 1, + "is_submittable": 1, + "permissions": [{ + "role": "System Manager", + "read": 1 + }], + "name": 'Submittable Doctype' + }).insert() doc = frappe.new_doc('Submittable DocType') doc.save() From ba267b6e628ea57009aa310645ba2f539708fe76 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 13:03:21 +0530 Subject: [PATCH 0072/1519] fix: update patch --- frappe/patches.txt | 2 +- .../add_original_name_docfield_to_submittable_doctypes.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/frappe/patches.txt b/frappe/patches.txt index a722bb39fa..1db775f17c 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -337,4 +337,4 @@ frappe.patches.v12_0.rename_uploaded_files_with_proper_name frappe.patches.v13_0.queryreport_columns frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty -frappe.patches.v13_0.add_original_name_docfield_to_submittable_doctypes ####### +frappe.patches.v13_0.add_original_name_docfield_to_submittable_doctypes diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py index 725d37fd4a..d870e63305 100644 --- a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -1,11 +1,9 @@ import frappe -from frappe.database.schema import add_column def execute(): for doctype in frappe.db.get_all('DocType'): doctype = frappe.get_doc("DocType", doctype.name) if doctype.is_submittable and frappe.db.table_exists(doctype.name): doctype.make_cancellable() - if not frappe.db.has_column(doctype.name, 'original_name'): - add_column(doctype.name, 'original_name', 'Text') + frappe.reload_doctype(doctype.name) doctype.db_update_all() From 26801f215cf03f9cb921752456ca6117bbb987f3 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 13:10:14 +0530 Subject: [PATCH 0073/1519] fix: use orm --- frappe/core/doctype/doctype/doctype.py | 48 +++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 0d478015bb..aad0321fa0 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -577,33 +577,33 @@ class DocType(Document): def make_amendable(self): """If is_submittable is set, add amended_from docfields.""" - if self.is_submittable: - if not frappe.db.sql("""select name from tabDocField - where fieldname = 'amended_from' and parent = %s""", self.name): - self.append("fields", { - "label": "Amended From", - "fieldtype": "Link", - "fieldname": "amended_from", - "options": self.name, - "read_only": 1, - "print_hide": 1, - "no_copy": 1 - }) + if self.is_submittable and\ + not frappe.db.get_value('DocField', {'fieldname': 'amended_from', 'parent': self.name}): + + self.append("fields", { + "label": "Amended From", + "fieldtype": "Link", + "fieldname": "amended_from", + "options": self.name, + "read_only": 1, + "print_hide": 1, + "no_copy": 1 + }) def make_cancellable(self): """If is_submittable is set, add original_name docfield.""" - if self.is_submittable: - if not frappe.db.sql("""select name from tabDocField - where fieldname = 'original_name' and parent = %s""", self.name): - self.append("fields", { - "label": "Original Name", - "fieldtype": "Text", - "fieldname": "original_name", - "read_only": 1, - "hidden": 1, - "print_hide": 1, - "no_copy": 1 - }) + if self.is_submittable and\ + not frappe.db.get_value('DocField', {'fieldname': 'amended_from', 'parent': self.name}): + + self.append("fields", { + "label": "Original Name", + "fieldtype": "Text", + "fieldname": "original_name", + "read_only": 1, + "hidden": 1, + "print_hide": 1, + "no_copy": 1 + }) def make_repeatable(self): """If allow_auto_repeat is set, add auto_repeat custom field.""" From 413383bf5cbbe27fb2b93c23e761e0a5b8b7f1f4 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 13:38:06 +0530 Subject: [PATCH 0074/1519] fix: condition in make_cancellable --- frappe/core/doctype/doctype/doctype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index aad0321fa0..c6f6b628cc 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -593,7 +593,7 @@ class DocType(Document): def make_cancellable(self): """If is_submittable is set, add original_name docfield.""" if self.is_submittable and\ - not frappe.db.get_value('DocField', {'fieldname': 'amended_from', 'parent': self.name}): + not frappe.db.get_value('DocField', {'fieldname': 'original_name', 'parent': self.name}): self.append("fields", { "label": "Original Name", From 2cf5915eee428e026de8b4ad9b8c75c60d756eee Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Fri, 16 Apr 2021 13:38:33 +0530 Subject: [PATCH 0075/1519] fix: use get_meta Co-authored-by: Faris Ansari --- .../v13_0/add_original_name_docfield_to_submittable_doctypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py index d870e63305..c5080cc692 100644 --- a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -2,7 +2,7 @@ import frappe def execute(): for doctype in frappe.db.get_all('DocType'): - doctype = frappe.get_doc("DocType", doctype.name) + doctype = frappe.get_meta(doctype.name) if doctype.is_submittable and frappe.db.table_exists(doctype.name): doctype.make_cancellable() frappe.reload_doctype(doctype.name) From 209ece8b6cc23fce390d8bb04d5463a85320dd34 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 14:03:36 +0530 Subject: [PATCH 0076/1519] fix: delete test submittable doctype --- frappe/tests/test_naming.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index afaabcc805..7be2cc3ef7 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -119,7 +119,7 @@ class TestNaming(unittest.TestCase): frappe.db.sql("""delete from `tabSeries` where name = %s""", series) def test_naming_for_cancelled_and_amended_doc(self): - frappe.get_doc({ + submittable_doctype = frappe.get_doc({ "doctype": "DocType", "module": "Core", "custom": 1, @@ -149,3 +149,5 @@ class TestNaming(unittest.TestCase): amended_doc.submit() amended_doc.cancel() self.assertEqual(amended_doc.name, "{}-2".format(original_name)) + + submittable_doctype.delete() \ No newline at end of file From 0c603f0d769536d3351386cb2cd7aaccd99797a5 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 17:27:12 +0530 Subject: [PATCH 0077/1519] test: fix cancel docs tests --- frappe/core/doctype/doctype/test_doctype.py | 4 ++++ .../add_original_name_docfield_to_submittable_doctypes.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 9c492d2c36..5a53600fff 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -350,6 +350,7 @@ class TestDocType(unittest.TestCase): dump_docs = json.dumps(docs.get('docs')) cancel_all_linked_docs(dump_docs) data_link_doc.cancel() + data_doc.name = '{}-1'.format(data_doc.name) data_doc.load_from_db() self.assertEqual(data_link_doc.docstatus, 2) self.assertEqual(data_doc.docstatus, 2) @@ -435,7 +436,10 @@ class TestDocType(unittest.TestCase): self.assertRaises(frappe.LinkExistsError, data_link_doc_1.cancel) data_doc.load_from_db() + + data_doc_2.name = '{}-1'.format(data_doc_2.name) data_doc_2.load_from_db() + self.assertEqual(data_link_doc_1.docstatus, 2) #linked doc is canceled diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py index c5080cc692..eccff2334f 100644 --- a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -2,7 +2,7 @@ import frappe def execute(): for doctype in frappe.db.get_all('DocType'): - doctype = frappe.get_meta(doctype.name) + doctype = frappe.get_doc('DocType', doctype.name) if doctype.is_submittable and frappe.db.table_exists(doctype.name): doctype.make_cancellable() frappe.reload_doctype(doctype.name) From b550e54f15d02bbb14d79c70344a89f0f73d9c33 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 29 Apr 2021 17:31:56 +0530 Subject: [PATCH 0078/1519] fix: add column in patch --- .../add_original_name_docfield_to_submittable_doctypes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py index eccff2334f..d17242e90a 100644 --- a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -1,9 +1,11 @@ import frappe +from frappe.database.schema import add_column def execute(): for doctype in frappe.db.get_all('DocType'): doctype = frappe.get_doc('DocType', doctype.name) if doctype.is_submittable and frappe.db.table_exists(doctype.name): doctype.make_cancellable() - frappe.reload_doctype(doctype.name) + if not frappe.db.has_column(doctype.name, 'original_name'): + add_column(doctype.name, 'original_name', 'Text') doctype.db_update_all() From 081677c6c4bf6190c804fb432ee2e83d14c74dda Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Fri, 7 May 2021 11:30:35 +0530 Subject: [PATCH 0079/1519] fix: typo in test --- frappe/tests/test_naming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 7be2cc3ef7..e7933ec7b3 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -131,7 +131,7 @@ class TestNaming(unittest.TestCase): "name": 'Submittable Doctype' }).insert() - doc = frappe.new_doc('Submittable DocType') + doc = frappe.new_doc('Submittable Doctype') doc.save() original_name = doc.name From d3d4b49796c551915b965a1d195787286fc16d90 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 May 2021 23:00:37 +0530 Subject: [PATCH 0080/1519] refactor: Simplify sidebar and breadcrumbs code --- frappe/website/context.py | 303 +++--------------- .../website/page_controllers/template_page.py | 56 ++-- 2 files changed, 83 insertions(+), 276 deletions(-) diff --git a/frappe/website/context.py b/frappe/website/context.py index ed5c89f149..c8dade5b8b 100644 --- a/frappe/website/context.py +++ b/frappe/website/context.py @@ -1,206 +1,28 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt +import json +import os -from __future__ import unicode_literals -import frappe, os, json - -from frappe.website.doctype.website_settings.website_settings import get_website_settings -from frappe.website.router import get_page_context +import frappe from frappe.model.document import Document -def get_context(path, args=None): - if args and args.source: - context = args - else: - context = get_page_context(path) - if args: - context.update(args) - - if hasattr(frappe.local, 'request'): - # for (remove leading slash) - # path could be overriden in render.resolve_from_map - context["path"] = frappe.local.request.path.strip('/ ') - else: - context["path"] = path - - context.canonical = frappe.utils.get_url(frappe.utils.escape_html(context.path)) - context.route = context.path - context = build_context(context) - - # set using frappe.respond_as_web_page - if hasattr(frappe.local, 'response') and frappe.local.response.get('context'): - context.update(frappe.local.response.context) - - # to be able to inspect the context dict - # Use the macro "inspect" from macros.html - context._context_dict = context - - context.developer_mode = frappe.conf.developer_mode - - return context - -def update_controller_context(context, controller): - module = frappe.get_module(controller) - - if module: - # get config fields - for prop in ("base_template_path", "template", "no_cache", "sitemap", - "condition_field"): - if hasattr(module, prop): - context[prop] = getattr(module, prop) - - if hasattr(module, "get_context"): - import inspect - try: - if inspect.getfullargspec(module.get_context).args: - ret = module.get_context(context) - else: - ret = module.get_context() - if ret: - context.update(ret) - except (frappe.PermissionError, frappe.PageDoesNotExistError, frappe.Redirect): - raise - except: - if not any([frappe.flags.in_migrate, frappe.flags.in_website_search_build]): - frappe.errprint(frappe.utils.get_traceback()) - - if hasattr(module, "get_children"): - context.children = module.get_children(context) - - -def build_context(context): - """get_context method of doc or module is supposed to render - content templates and push it into context""" - context = frappe._dict(context) - - if not "url_prefix" in context: - context.url_prefix = "" - - if context.url_prefix and context.url_prefix[-1]!='/': - context.url_prefix += '/' - - # for backward compatibility - context.docs_base_url = '/docs' - - context.update(get_website_settings(context)) - context.update(frappe.local.conf.get("website_context") or {}) - - # provide doc - if context.doc: - context.update(context.doc.as_dict()) - context.update(context.doc.get_website_properties()) - - if not context.template: - context.template = context.doc.meta.get_web_template() - - if hasattr(context.doc, "get_context"): - ret = context.doc.get_context(context) - - if ret: - context.update(ret) - - for prop in ("no_cache", "sitemap"): - if not prop in context: - context[prop] = getattr(context.doc, prop, False) - - elif context.controller: - # controller based context - update_controller_context(context, context.controller) - - # controller context extensions - context_controller_hooks = frappe.get_hooks("extend_website_page_controller_context") or {} - for controller, extension in context_controller_hooks.items(): - if isinstance(extension, list): - for ext in extension: - if controller == context.controller: - update_controller_context(context, ext) - else: - update_controller_context(context, extension) - - add_metatags(context) - add_sidebar_and_breadcrumbs(context) - - # determine templates to be used - if not context.base_template_path: - app_base = frappe.get_hooks("base_template") - context.base_template_path = app_base[-1] if app_base else "templates/base.html" - - if context.title_prefix and context.title and not context.title.startswith(context.title_prefix): - context.title = '{0} - {1}'.format(context.title_prefix, context.title) - - # apply context from hooks - update_website_context = frappe.get_hooks('update_website_context') - for method in update_website_context: - values = frappe.get_attr(method)(context) - if values: - context.update(values) - - return context - -def load_sidebar(context, sidebar_json_path): - with open(sidebar_json_path, 'r') as sidebarfile: - try: - sidebar_json = sidebarfile.read() - context.sidebar_items = json.loads(sidebar_json) - context.show_sidebar = 1 - except json.decoder.JSONDecodeError: - frappe.throw('Invalid Sidebar JSON at ' + sidebar_json_path) - -def get_sidebar_json_path(path, look_for=False): - ''' - Get _sidebar.json path from directory path - - :param path: path of the current diretory - :param look_for: if True, look for _sidebar.json going upwards from given path - - :return: _sidebar.json path - ''' - if os.path.split(path)[1] == 'www' or path == '/' or not path: - return '' - - sidebar_json_path = os.path.join(path, '_sidebar.json') - if os.path.exists(sidebar_json_path): - return sidebar_json_path - else: - if look_for: - return get_sidebar_json_path(os.path.split(path)[0], look_for) - else: - return '' - -def add_sidebar_and_breadcrumbs(context): - '''Add sidebar and breadcrumbs to context''' - from frappe.website.router import get_page_info_from_template - if context.show_sidebar: - context.no_cache = 1 - add_sidebar_data(context) - else: - if context.basepath: - hooks = frappe.get_hooks('look_for_sidebar_json') - look_for_sidebar_json = hooks[0] if hooks else 0 - sidebar_json_path = get_sidebar_json_path( - context.basepath, - look_for_sidebar_json - ) - if sidebar_json_path: - load_sidebar(context, sidebar_json_path) - - if context.add_breadcrumbs and not context.parents: - if context.basepath: - parent_path = os.path.dirname(context.path).rstrip('/') - page_info = get_page_info_from_template(parent_path) - if page_info: - context.parents = [dict(route=parent_path, title=page_info.title)] - -def add_sidebar_data(context): - from frappe.utils.user import get_fullname_and_avatar +def get_sidebar_data(parent_sidebar, basepath): import frappe.www.list + sidebar_data = frappe._dict() - if context.show_sidebar and context.website_sidebar: - context.sidebar_items = frappe.get_all('Website Sidebar Item', - filters=dict(parent=context.website_sidebar), fields=['title', 'route', '`group`'], + hooks = frappe.get_hooks('look_for_sidebar_json') + look_for_sidebar_json = hooks[0] if hooks else 0 + + if basepath and look_for_sidebar_json: + sidebar_items = get_sidebar_items_from_sidebar_file(basepath, look_for_sidebar_json) + sidebar_data['sidebar_items'] = sidebar_items + + if not sidebar_data.sidebar_items and parent_sidebar: + sidebar_data.sidebar_items = frappe.get_all('Website Sidebar Item', + filters=dict(parent=parent_sidebar), fields=['title', 'route', '`group`'], order_by='idx asc') - if not context.sidebar_items: + if not sidebar_data.sidebar_items: sidebar_items = frappe.cache().hget('portal_menu_items', frappe.session.user) if sidebar_items == None: sidebar_items = [] @@ -224,74 +46,41 @@ def add_sidebar_data(context): add_items(sidebar_items, items_via_hooks) frappe.cache().hset('portal_menu_items', frappe.session.user, sidebar_items) + sidebar_data.sidebar_items = sidebar_items - context.sidebar_items = sidebar_items + return sidebar_data - info = get_fullname_and_avatar(frappe.session.user) - context["fullname"] = info.fullname - context["user_image"] = info.avatar - context["user"] = info.name +def get_sidebar_items_from_sidebar_file(basepath, look_for_sidebar_json): + sidebar_items = frappe._dict() + sidebar_json_path = get_sidebar_json_path(basepath, look_for_sidebar_json) + if not sidebar_json_path: return sidebar_items + with open(sidebar_json_path, 'r') as sidebarfile: + try: + sidebar_json = sidebarfile.read() + sidebar_items = json.loads(sidebar_json) + except json.decoder.JSONDecodeError: + frappe.throw('Invalid Sidebar JSON at ' + sidebar_json_path) -def add_metatags(context): - tags = frappe._dict(context.get("metatags") or {}) + return sidebar_items - if "og:type" not in tags: - tags["og:type"] = "article" +def get_sidebar_json_path(path, look_for=False): + ''' + Get _sidebar.json path from directory path - if "title" not in tags and context.title: - tags["title"] = context.title + :param path: path of the current diretory + :param look_for: if True, look for _sidebar.json going upwards from given path - title = tags.get("name") or tags.get("title") - if title: - tags["og:title"] = tags["twitter:title"] = title - tags["twitter:card"] = "summary" + :return: _sidebar.json path + ''' + if os.path.split(path)[1] == 'www' or path == '/' or not path: + return '' - if not tags.get('description') and context.description: - tags["description"] = context.description - - description = tags.get("description") - if description: - tags["og:description"] = tags["twitter:description"] = description - - if "image" not in tags and context.image: - tags["image"] = context.image - - image = tags.get("image") - if image: - tags["og:image"] = tags["twitter:image"] = tags["image"] = frappe.utils.get_url(image) - tags['twitter:card'] = "summary_large_image" - - if "author" not in tags and context.author: - tags["author"] = context.author - - tags["og:url"] = tags["url"] = frappe.utils.get_url(context.path) - - if "published_on" not in tags and context.published_on: - tags["published_on"] = context.published_on - - if "published_on" in tags: - tags["datePublished"] = tags["published_on"] - del tags["published_on"] - - tags["language"] = frappe.local.lang or "en" - - # Get meta tags from Website Route meta - # they can override the defaults set above - route = context.path - if route == '': - # homepage - route = frappe.db.get_single_value('Website Settings', 'home_page') - - route_exists = (route - and not route.endswith(('.js', '.css')) - and frappe.db.exists('Website Route Meta', route)) - - if route_exists: - website_route_meta = frappe.get_doc('Website Route Meta', route) - for meta_tag in website_route_meta.meta_tags: - d = meta_tag.get_meta_dict() - tags.update(d) - - # update tags in context - context.metatags = tags + sidebar_json_path = os.path.join(path, '_sidebar.json') + if os.path.exists(sidebar_json_path): + return sidebar_json_path + else: + if look_for: + return get_sidebar_json_path(os.path.split(path)[0], look_for) + else: + return '' diff --git a/frappe/website/page_controllers/template_page.py b/frappe/website/page_controllers/template_page.py index f2664fb928..5b001a55e7 100644 --- a/frappe/website/page_controllers/template_page.py +++ b/frappe/website/page_controllers/template_page.py @@ -3,7 +3,7 @@ import os import frappe from frappe.website.page_controllers.base_template_page import BaseTemplatePage -from frappe.website.context import add_sidebar_and_breadcrumbs +from frappe.website.context import get_sidebar_data from frappe.website.render import build_response from frappe.website.router import get_base_template from frappe.website.utils import (extract_comment_tag, @@ -12,29 +12,32 @@ from frappe.website.utils import (extract_comment_tag, WEBPAGE_PY_MODULE_PROPERTIES = ("base_template_path", "template", "no_cache", "sitemap", "condition_field") class TemplatePage(BaseTemplatePage): - def validate(self): - for app in frappe.get_installed_apps(frappe_last=True): - if self.find_page_in_app(app): - return True + def __init__(self, path, http_status_code=None): + super().__init__(path=path, http_status_code=http_status_code) + self.set_template_path() - def find_page_in_app(self, app): + def set_template_path(self): ''' Searches for file matching the path in the /www - and /templates/pages folders + and /templates/pages folders and sets path if match is found ''' - app_path = frappe.get_app_path(app) folders = get_start_folders() + for app in frappe.get_installed_apps(frappe_last=True): + app_path = frappe.get_app_path(app) - for dirname in folders: - search_path = os.path.join(app_path, dirname, self.path) - for p in self.get_index_path_options(search_path): - file_path = frappe.as_unicode(p) - if os.path.exists(file_path) and not os.path.isdir(file_path): - self.app = app - self.app_path = app_path - self.template_path = os.path.relpath(file_path, self.app_path) - self.basepath = os.path.dirname(file_path) - return True + for dirname in folders: + search_path = os.path.join(app_path, dirname, self.path) + for p in self.get_index_path_options(search_path): + file_path = frappe.as_unicode(p) + if os.path.exists(file_path) and not os.path.isdir(file_path): + self.app = app + self.app_path = app_path + self.template_path = os.path.relpath(file_path, self.app_path) + self.basepath = os.path.dirname(file_path) + return + + def validate(self): + return hasattr(self, 'template_path') and bool(self.template_path) def get_index_path_options(self, search_path): return (f'{search_path}{d}' for d in ('', '.html', '.md', '/index.html', '/index.md')) @@ -59,12 +62,20 @@ class TemplatePage(BaseTemplatePage): return html def post_process_context(self): + self.set_user_info() self.add_sidebar_and_breadcrumbs() self.set_missing_values() super(TemplatePage, self).post_process_context() def add_sidebar_and_breadcrumbs(self): - add_sidebar_and_breadcrumbs(self.context) + if self.basepath: + sidebar_data = get_sidebar_data(self.context.website_sidebar, self.basepath) or None + self.context.sidebar_items = sidebar_data.sidebar_items + + if self.context.add_breadcrumbs and not self.context.parents: + # TODO: set correct title and route for breadcrumbs + parent_path = os.path.dirname(self.path) + self.context.parents = [dict(route=parent_path, title=extract_title(source='', path=parent_path))] def set_pymodule(self): ''' @@ -229,6 +240,13 @@ class TemplatePage(BaseTemplatePage): # for backward compatibility self.context.docs_base_url = '/docs' + def set_user_info(self): + from frappe.utils.user import get_fullname_and_avatar + info = get_fullname_and_avatar(frappe.session.user) + self.context["fullname"] = info.fullname + self.context["user_image"] = info.avatar + self.context["user"] = info.name + def get_start_folders(): return frappe.local.flags.web_pages_folders or ('www', 'templates/pages') From 19879943102d669ad94b82a9ba89177f2430d8d2 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 May 2021 23:37:28 +0530 Subject: [PATCH 0081/1519] refactor: Move unwanted code from validate - Also, removed unwanted code in render.py --- frappe/website/page_controllers/error_page.py | 5 ++- frappe/website/page_controllers/list_page.py | 11 ++++--- .../page_controllers/not_found_page.py | 8 +++-- .../page_controllers/not_permitted_page.py | 9 +++-- frappe/website/page_controllers/print_page.py | 14 +++++--- .../website/page_controllers/static_page.py | 32 ++++++++++-------- frappe/website/render.py | 33 +------------------ 7 files changed, 50 insertions(+), 62 deletions(-) diff --git a/frappe/website/page_controllers/error_page.py b/frappe/website/page_controllers/error_page.py index 1f54a5f38c..554949684f 100644 --- a/frappe/website/page_controllers/error_page.py +++ b/frappe/website/page_controllers/error_page.py @@ -1,7 +1,10 @@ from frappe.website.page_controllers.template_page import TemplatePage class ErrorPage(TemplatePage): - def __init__(self, path, http_status_code, exception): + def __init__(self, path=None, http_status_code=None, exception=None): path = 'error' super().__init__(path=path, http_status_code=http_status_code) self.http_status_code = getattr(exception, 'http_status_code', None) or http_status_code or 500 + + def validate(self): + return True diff --git a/frappe/website/page_controllers/list_page.py b/frappe/website/page_controllers/list_page.py index 2cb6ed5f26..cc6f150dd2 100644 --- a/frappe/website/page_controllers/list_page.py +++ b/frappe/website/page_controllers/list_page.py @@ -3,8 +3,9 @@ from frappe.website.page_controllers.template_page import TemplatePage class ListPage(TemplatePage): def validate(self): - if frappe.db.get_value('DocType', self.path): - frappe.local.form_dict.doctype = self.path - self.set_standard_path('list') - return True - return False + return frappe.db.exists('DocType', self.path, True) + + def render(self): + frappe.local.form_dict.doctype = self.path + self.set_standard_path('list') + return super().render() diff --git a/frappe/website/page_controllers/not_found_page.py b/frappe/website/page_controllers/not_found_page.py index 1d15877e9d..aebc0006ae 100644 --- a/frappe/website/page_controllers/not_found_page.py +++ b/frappe/website/page_controllers/not_found_page.py @@ -2,7 +2,9 @@ from frappe.website.page_controllers.template_page import TemplatePage class NotFoundPage(TemplatePage): def __init__(self, path, http_status_code): + path = '404' + http_status_code = 404 super().__init__(path=path, http_status_code=http_status_code) - self.path = '404' - self.http_status_code = 404 - self.template_path = '404' + + def validate(self): + return True diff --git a/frappe/website/page_controllers/not_permitted_page.py b/frappe/website/page_controllers/not_permitted_page.py index 61c6b88e09..3ed732d80b 100644 --- a/frappe/website/page_controllers/not_permitted_page.py +++ b/frappe/website/page_controllers/not_permitted_page.py @@ -1,13 +1,18 @@ import frappe from frappe import _ from frappe.website.page_controllers.template_page import TemplatePage +from frappe.utils import cstr class NotPermittedPage(TemplatePage): - def __init__(self, path, http_status_code): + def __init__(self, path=None, http_status_code=None, exception=''): + frappe.local.message = cstr(exception) super().__init__(path=path, http_status_code=http_status_code) self.http_status_code = 403 def validate(self): + return True + + def render(self): frappe.local.message_title = _("Not Permitted") frappe.local.response['context'] = dict( indicator_color = 'red', @@ -16,4 +21,4 @@ class NotPermittedPage(TemplatePage): fullpage=True ) self.set_standard_path('message') - return True + return super().render() diff --git a/frappe/website/page_controllers/print_page.py b/frappe/website/page_controllers/print_page.py index 6daea7fcbc..574f335f5d 100644 --- a/frappe/website/page_controllers/print_page.py +++ b/frappe/website/page_controllers/print_page.py @@ -9,11 +9,15 @@ class PrintPage(TemplatePage): def validate(self): parts = self.path.split('/', 1) if len(parts)==2: - if (frappe.db.get_value('DocType', parts[0]) - and frappe.db.get_value(parts[0], parts[1])): - frappe.form_dict.doctype = parts[0] - frappe.form_dict.name = parts[1] - self.set_standard_path('printview') + if (frappe.db.exists('DocType', parts[0], True) + and frappe.db.exists(parts[0], parts[1], True)): return True return False + + def render(self): + parts = self.path.split('/', 1) + frappe.form_dict.doctype = parts[0] + frappe.form_dict.name = parts[1] + self.set_standard_path('printview') + return super().render() diff --git a/frappe/website/page_controllers/static_page.py b/frappe/website/page_controllers/static_page.py index 8f474e71be..8c07440ebb 100644 --- a/frappe/website/page_controllers/static_page.py +++ b/frappe/website/page_controllers/static_page.py @@ -7,27 +7,31 @@ from werkzeug.wsgi import wrap_file import frappe from frappe.website.page_controllers.web_page import WebPage +UNSUPPORTED_STATIC_PAGE_TYPES = ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json') class StaticPage(WebPage): - def validate(self): - if ('.' not in self.path): - return False - extension = self.path.rsplit('.', 1)[-1] - if extension in ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json'): - return False + def __init__(self, path, http_status_code=None): + super().__init__(path=path, http_status_code=http_status_code) + self.set_file_path() - if self.find_path_in_apps(): - return True - - return False - - def find_path_in_apps(self): + def set_file_path(self): + self.file_path = '' + if not self.is_valid_file_path(): return for app in frappe.get_installed_apps(): file_path = frappe.get_app_path(app, 'www') + '/' + self.path if os.path.exists(file_path): self.file_path = file_path - return True - return False + + def validate(self): + return self.is_valid_file_path() and self.file_path + + def is_valid_file_path(self): + if ('.' not in self.path): + return False + extension = self.path.rsplit('.', 1)[-1] + if extension in UNSUPPORTED_STATIC_PAGE_TYPES: + return False + return True def render(self): f = open(self.file_path, 'rb') diff --git a/frappe/website/render.py b/frappe/website/render.py index 87eddf3463..6a1d5a404e 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -11,9 +11,8 @@ from werkzeug.wrappers import Response import frappe import frappe.sessions -from frappe.website.context import get_context from frappe.website.router import evaluate_dynamic_routes -from frappe.website.utils import (can_cache, get_home_page, get_next_link, get_toc) +from frappe.website.utils import get_home_page def build_response(path, data, http_status_code, headers=None): @@ -54,36 +53,6 @@ def add_preload_headers(response): import traceback traceback.print_exc() - -def build_page(path): - if not getattr(frappe.local, "path", None): - frappe.local.path = path - - context = get_context(path) - - if context.source: - html = frappe.render_template(context.source, context) - elif context.template: - if path.endswith('min.js'): - html = frappe.get_jloader().get_source(frappe.get_jenv(), context.template)[0] - else: - html = frappe.get_template(context.template).render(context) - - if '{index}' in html: - html = html.replace('{index}', get_toc(context.route)) - - if '{next}' in html: - html = html.replace('{next}', get_next_link(context.route)) - - # html = frappe.get_template(context.base_template_path).render(context) - - if can_cache(context.no_cache): - page_cache = frappe.cache().hget("website_page", path) or {} - page_cache[frappe.local.lang] = html - frappe.cache().hset("website_page", path, page_cache) - - return html - def resolve_path(path): if not path: path = "index" From 5318697c0fd74d7fd534e1e645fdd6e34464cb08 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 18 May 2021 08:40:37 +0530 Subject: [PATCH 0082/1519] feat: Add redirect page --- frappe/website/page_controllers/redirect_page.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 frappe/website/page_controllers/redirect_page.py diff --git a/frappe/website/page_controllers/redirect_page.py b/frappe/website/page_controllers/redirect_page.py new file mode 100644 index 0000000000..f3a0ba6e7f --- /dev/null +++ b/frappe/website/page_controllers/redirect_page.py @@ -0,0 +1,16 @@ +import frappe +from frappe.website.render import build_response + +class RedirectPage(object): + def __init__(self, path, http_status_code=301): + self.path = path + self.http_status_code = http_status_code + + def validate(self): + return True + + def render(self): + return build_response(self.path, "", 301, { + "Location": frappe.flags.redirect_location or (frappe.local.response or {}).get('location'), + "Cache-Control": "no-store, no-cache, must-revalidate" + }) From a02e5f766a70258ab05e741afc64263f06970137 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 18 May 2021 08:43:49 +0530 Subject: [PATCH 0083/1519] refactor: Add path_resolver to separate route resolving part --- frappe/website/path_resolver.py | 67 +++++++++++++++++++++++++++++++++ frappe/website/serve.py | 54 +++++++++++--------------- 2 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 frappe/website/path_resolver.py diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py new file mode 100644 index 0000000000..d6e43f4f1f --- /dev/null +++ b/frappe/website/path_resolver.py @@ -0,0 +1,67 @@ +from frappe.website.redirect import resolve_redirect +from frappe.website.render import resolve_path +import frappe + +from frappe.website.page_controllers.document_page import DocumentPage +from frappe.website.page_controllers.list_page import ListPage +from frappe.website.page_controllers.not_found_page import NotFoundPage +from frappe.website.page_controllers.print_page import PrintPage +from frappe.website.page_controllers.template_page import TemplatePage +from frappe.website.page_controllers.static_page import StaticPage +from frappe.website.page_controllers.web_form import WebFormPage + +class PathResolver(): + def __init__(self, path, http_status_code=None): + self._path = path + self.http_status_code = http_status_code + # self.url_map = get_url_map() + + @property + def path(self): + return self._path.strip('/ ') + + def resolve(self): + '''Returns endpoint and a renderer instance that can render the endpoint''' + query_string = frappe.local.request.query_string + resolve_redirect(self.path, query_string) + endpoint = resolve_path(self.path) + # urls = self.url_map.bind_to_environ(frappe.local.request.environ) + + renderers = (StaticPage, WebFormPage, TemplatePage, ListPage, DocumentPage, PrintPage, NotFoundPage) + + for renderer in renderers: + renderer_instance = renderer(endpoint, self.http_status_code) + can_render = renderer_instance.validate() + if can_render: + return endpoint, renderer_instance + + return endpoint, None + +# #> Path > Path > resolve using url_map > path.endpoint +# # +# # +# # +# # pathclass url_map + +# def get_url_map(): +# Map([Rule('/', endpoint=get_home_page, defaults={'renderer': TemplatePage})]) + + +# query_string = None +# response = None + +# if not path: +# path = frappe.local.request.path +# query_string = frappe.local.request.query_string + +# try: +# path = path.strip('/ ') +# resolve_redirect(path, query_string) +# path = resolve_path(path) +# # there is no way to determine the type of the page based on the route +# # so evaluate each type of page sequentially +# renderers = [StaticPage, WebFormPage, TemplatePage, ListPage, DocumentPage, PrintPage, NotFoundPage] +# for renderer in renderers: +# response = renderer(path, http_status_code).get() +# if response: +# break diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 47ca0aa1d2..bdec8a16ba 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -1,49 +1,37 @@ import frappe -from frappe.utils import cstr -from frappe.website.page_controllers.document_page import DocumentPage from frappe.website.page_controllers.error_page import ErrorPage -from frappe.website.page_controllers.list_page import ListPage -from frappe.website.page_controllers.not_found_page import NotFoundPage from frappe.website.page_controllers.not_permitted_page import NotPermittedPage -from frappe.website.page_controllers.print_page import PrintPage -from frappe.website.page_controllers.template_page import TemplatePage -from frappe.website.page_controllers.static_page import StaticPage -from frappe.website.page_controllers.web_form import WebFormPage - -from frappe.website.redirect import resolve_redirect -from frappe.website.render import build_response, resolve_path +from frappe.website.page_controllers.redirect_page import RedirectPage +from frappe.website.path_resolver import PathResolver +from frappe.website.utils import can_cache def get_response(path=None, http_status_code=200): - """render html page""" - query_string = None + """Resolves path and renders page""" response = None + path = path or frappe.local.request.path + endpoint = path + # if can_cache(): + # # return rendered page + # page_cache = frappe.cache().hget("website_page", path) + # if page_cache and frappe.local.lang in page_cache: + # out = page_cache[frappe.local.lang] - if not path: - path = frappe.local.request.path - query_string = frappe.local.request.query_string + # if out: + # frappe.local.response.from_cache = True + # return out try: - path = path.strip('/ ') - resolve_redirect(path, query_string) - path = resolve_path(path) - # there is no way to determine the type of the page based on the route - # so evaluate each type of page sequentially - renderers = [StaticPage, WebFormPage, TemplatePage, ListPage, DocumentPage, PrintPage, NotFoundPage] - for renderer in renderers: - response = renderer(path, http_status_code).get() - if response: - break + path_resolver = PathResolver(path, http_status_code) + endpoint, renderer_instance = path_resolver.resolve() + if renderer_instance: + response = renderer_instance.render() except frappe.Redirect: - return build_response(path, "", 301, { - "Location": frappe.flags.redirect_location or (frappe.local.response or {}).get('location'), - "Cache-Control": "no-store, no-cache, must-revalidate" - }) + return RedirectPage(endpoint or path, http_status_code).render() except frappe.PermissionError as e: - frappe.local.message = cstr(e) - response = NotPermittedPage(path, http_status_code).get() + response = NotPermittedPage(endpoint, http_status_code, exception=e).render() except Exception as e: - response = ErrorPage(path, http_status_code, exception=e).get() + response = ErrorPage(exception=e).render() return response From c8d588819f940b89e8a48a5190ecf22a188b5ece Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 18 May 2021 09:05:02 +0530 Subject: [PATCH 0084/1519] refactor: Move get_sidebar_data method to utils - Also, rename get_sidebar_data to get_sidebar_items & delete context.py - Remove commented text --- frappe/website/context.py | 86 ---------------- .../website/page_controllers/template_page.py | 5 +- frappe/website/path_resolver.py | 39 +------- frappe/website/serve.py | 4 +- frappe/website/utils.py | 97 +++++++++++++++++-- 5 files changed, 99 insertions(+), 132 deletions(-) delete mode 100644 frappe/website/context.py diff --git a/frappe/website/context.py b/frappe/website/context.py deleted file mode 100644 index c8dade5b8b..0000000000 --- a/frappe/website/context.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt -import json -import os - -import frappe -from frappe.model.document import Document - -def get_sidebar_data(parent_sidebar, basepath): - import frappe.www.list - sidebar_data = frappe._dict() - - hooks = frappe.get_hooks('look_for_sidebar_json') - look_for_sidebar_json = hooks[0] if hooks else 0 - - if basepath and look_for_sidebar_json: - sidebar_items = get_sidebar_items_from_sidebar_file(basepath, look_for_sidebar_json) - sidebar_data['sidebar_items'] = sidebar_items - - if not sidebar_data.sidebar_items and parent_sidebar: - sidebar_data.sidebar_items = frappe.get_all('Website Sidebar Item', - filters=dict(parent=parent_sidebar), fields=['title', 'route', '`group`'], - order_by='idx asc') - - if not sidebar_data.sidebar_items: - sidebar_items = frappe.cache().hget('portal_menu_items', frappe.session.user) - if sidebar_items == None: - sidebar_items = [] - roles = frappe.get_roles() - portal_settings = frappe.get_doc('Portal Settings', 'Portal Settings') - - def add_items(sidebar_items, items): - for d in items: - if d.get('enabled') and ((not d.get('role')) or d.get('role') in roles): - sidebar_items.append(d.as_dict() if isinstance(d, Document) else d) - - if not portal_settings.hide_standard_menu: - add_items(sidebar_items, portal_settings.get('menu')) - - if portal_settings.custom_menu: - add_items(sidebar_items, portal_settings.get('custom_menu')) - - items_via_hooks = frappe.get_hooks('portal_menu_items') - if items_via_hooks: - for i in items_via_hooks: i['enabled'] = 1 - add_items(sidebar_items, items_via_hooks) - - frappe.cache().hset('portal_menu_items', frappe.session.user, sidebar_items) - sidebar_data.sidebar_items = sidebar_items - - return sidebar_data - -def get_sidebar_items_from_sidebar_file(basepath, look_for_sidebar_json): - sidebar_items = frappe._dict() - sidebar_json_path = get_sidebar_json_path(basepath, look_for_sidebar_json) - if not sidebar_json_path: return sidebar_items - - with open(sidebar_json_path, 'r') as sidebarfile: - try: - sidebar_json = sidebarfile.read() - sidebar_items = json.loads(sidebar_json) - except json.decoder.JSONDecodeError: - frappe.throw('Invalid Sidebar JSON at ' + sidebar_json_path) - - return sidebar_items - -def get_sidebar_json_path(path, look_for=False): - ''' - Get _sidebar.json path from directory path - - :param path: path of the current diretory - :param look_for: if True, look for _sidebar.json going upwards from given path - - :return: _sidebar.json path - ''' - if os.path.split(path)[1] == 'www' or path == '/' or not path: - return '' - - sidebar_json_path = os.path.join(path, '_sidebar.json') - if os.path.exists(sidebar_json_path): - return sidebar_json_path - else: - if look_for: - return get_sidebar_json_path(os.path.split(path)[0], look_for) - else: - return '' diff --git a/frappe/website/page_controllers/template_page.py b/frappe/website/page_controllers/template_page.py index 5b001a55e7..12f459afd7 100644 --- a/frappe/website/page_controllers/template_page.py +++ b/frappe/website/page_controllers/template_page.py @@ -3,7 +3,7 @@ import os import frappe from frappe.website.page_controllers.base_template_page import BaseTemplatePage -from frappe.website.context import get_sidebar_data +from frappe.website.utils import get_sidebar_items from frappe.website.render import build_response from frappe.website.router import get_base_template from frappe.website.utils import (extract_comment_tag, @@ -69,8 +69,7 @@ class TemplatePage(BaseTemplatePage): def add_sidebar_and_breadcrumbs(self): if self.basepath: - sidebar_data = get_sidebar_data(self.context.website_sidebar, self.basepath) or None - self.context.sidebar_items = sidebar_data.sidebar_items + self.context.sidebar_items = get_sidebar_items(self.context.website_sidebar, self.basepath) if self.context.add_breadcrumbs and not self.context.parents: # TODO: set correct title and route for breadcrumbs diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py index d6e43f4f1f..b9f8972380 100644 --- a/frappe/website/path_resolver.py +++ b/frappe/website/path_resolver.py @@ -1,20 +1,19 @@ -from frappe.website.redirect import resolve_redirect -from frappe.website.render import resolve_path import frappe - from frappe.website.page_controllers.document_page import DocumentPage from frappe.website.page_controllers.list_page import ListPage from frappe.website.page_controllers.not_found_page import NotFoundPage from frappe.website.page_controllers.print_page import PrintPage -from frappe.website.page_controllers.template_page import TemplatePage from frappe.website.page_controllers.static_page import StaticPage +from frappe.website.page_controllers.template_page import TemplatePage from frappe.website.page_controllers.web_form import WebFormPage +from frappe.website.redirect import resolve_redirect +from frappe.website.render import resolve_path + class PathResolver(): def __init__(self, path, http_status_code=None): self._path = path self.http_status_code = http_status_code - # self.url_map = get_url_map() @property def path(self): @@ -25,7 +24,6 @@ class PathResolver(): query_string = frappe.local.request.query_string resolve_redirect(self.path, query_string) endpoint = resolve_path(self.path) - # urls = self.url_map.bind_to_environ(frappe.local.request.environ) renderers = (StaticPage, WebFormPage, TemplatePage, ListPage, DocumentPage, PrintPage, NotFoundPage) @@ -36,32 +34,3 @@ class PathResolver(): return endpoint, renderer_instance return endpoint, None - -# #> Path > Path > resolve using url_map > path.endpoint -# # -# # -# # -# # pathclass url_map - -# def get_url_map(): -# Map([Rule('/', endpoint=get_home_page, defaults={'renderer': TemplatePage})]) - - -# query_string = None -# response = None - -# if not path: -# path = frappe.local.request.path -# query_string = frappe.local.request.query_string - -# try: -# path = path.strip('/ ') -# resolve_redirect(path, query_string) -# path = resolve_path(path) -# # there is no way to determine the type of the page based on the route -# # so evaluate each type of page sequentially -# renderers = [StaticPage, WebFormPage, TemplatePage, ListPage, DocumentPage, PrintPage, NotFoundPage] -# for renderer in renderers: -# response = renderer(path, http_status_code).get() -# if response: -# break diff --git a/frappe/website/serve.py b/frappe/website/serve.py index bdec8a16ba..9b28fd2e3b 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -1,10 +1,10 @@ import frappe - from frappe.website.page_controllers.error_page import ErrorPage from frappe.website.page_controllers.not_permitted_page import NotPermittedPage from frappe.website.page_controllers.redirect_page import RedirectPage from frappe.website.path_resolver import PathResolver -from frappe.website.utils import can_cache + +#from frappe.website.utils import can_cache def get_response(path=None, http_status_code=200): """Resolves path and renders page""" diff --git a/frappe/website/utils.py b/frappe/website/utils.py index a5fe9c3986..723d878fac 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -2,16 +2,21 @@ # MIT License. See license.txt from __future__ import unicode_literals -import functools -import re -import os -import frappe -import yaml -from six import iteritems +import functools +import json +import os +import re + +import yaml from past.builtins import cmp +from six import iteritems + +import frappe +from frappe.model.document import Document from frappe.utils import md_to_html + def delete_page_cache(path): cache = frappe.cache() cache.delete_value('full_index') @@ -439,3 +444,83 @@ def get_frontmatter(string): "attributes": yaml.safe_load(frontmatter), "body": body, } + +def get_sidebar_items(parent_sidebar, basepath): + import frappe.www.list + sidebar_items = [] + + hooks = frappe.get_hooks('look_for_sidebar_json') + look_for_sidebar_json = hooks[0] if hooks else 0 + + if basepath and look_for_sidebar_json: + sidebar_items = get_sidebar_items_from_sidebar_file(basepath, look_for_sidebar_json) + + if not sidebar_items and parent_sidebar: + sidebar_items = frappe.get_all('Website Sidebar Item', + filters=dict(parent=parent_sidebar), fields=['title', 'route', '`group`'], + order_by='idx asc') + + if not sidebar_items: + sidebar_items = get_portal_sidebar_items() + + return sidebar_items + + +def get_portal_sidebar_items(): + sidebar_items = frappe.cache().hget('portal_menu_items', frappe.session.user) + if sidebar_items == None: + sidebar_items = [] + roles = frappe.get_roles() + portal_settings = frappe.get_doc('Portal Settings', 'Portal Settings') + + def add_items(sidebar_items, items): + for d in items: + if d.get('enabled') and ((not d.get('role')) or d.get('role') in roles): + sidebar_items.append(d.as_dict() if isinstance(d, Document) else d) + + if not portal_settings.hide_standard_menu: + add_items(sidebar_items, portal_settings.get('menu')) + + if portal_settings.custom_menu: + add_items(sidebar_items, portal_settings.get('custom_menu')) + + items_via_hooks = frappe.get_hooks('portal_menu_items') + if items_via_hooks: + for i in items_via_hooks: i['enabled'] = 1 + add_items(sidebar_items, items_via_hooks) + + frappe.cache().hset('portal_menu_items', frappe.session.user, sidebar_items) + + return sidebar_items + +def get_sidebar_items_from_sidebar_file(basepath, look_for_sidebar_json): + sidebar_items = frappe._dict() + sidebar_json_path = get_sidebar_json_path(basepath, look_for_sidebar_json) + if not sidebar_json_path: return sidebar_items + + with open(sidebar_json_path, 'r') as sidebarfile: + try: + sidebar_json = sidebarfile.read() + sidebar_items = json.loads(sidebar_json) + except json.decoder.JSONDecodeError: + frappe.throw('Invalid Sidebar JSON at ' + sidebar_json_path) + + return sidebar_items + +def get_sidebar_json_path(path, look_for=False): + '''Get _sidebar.json path from directory path + :param path: path of the current diretory + :param look_for: if True, look for _sidebar.json going upwards from given path + :return: _sidebar.json path + ''' + if os.path.split(path)[1] == 'www' or path == '/' or not path: + return '' + + sidebar_json_path = os.path.join(path, '_sidebar.json') + if os.path.exists(sidebar_json_path): + return sidebar_json_path + else: + if look_for: + return get_sidebar_json_path(os.path.split(path)[0], look_for) + else: + return '' From d6a539362d1a3401f549f6788450116004d6b986 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 18 May 2021 09:13:13 +0530 Subject: [PATCH 0085/1519] style: Fix formatting issue --- frappe/website/page_controllers/static_page.py | 3 ++- frappe/website/utils.py | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frappe/website/page_controllers/static_page.py b/frappe/website/page_controllers/static_page.py index 8c07440ebb..767cc872d1 100644 --- a/frappe/website/page_controllers/static_page.py +++ b/frappe/website/page_controllers/static_page.py @@ -16,7 +16,8 @@ class StaticPage(WebPage): def set_file_path(self): self.file_path = '' - if not self.is_valid_file_path(): return + if not self.is_valid_file_path(): + return for app in frappe.get_installed_apps(): file_path = frappe.get_app_path(app, 'www') + '/' + self.path if os.path.exists(file_path): diff --git a/frappe/website/utils.py b/frappe/website/utils.py index 723d878fac..2d16658efb 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -468,7 +468,7 @@ def get_sidebar_items(parent_sidebar, basepath): def get_portal_sidebar_items(): sidebar_items = frappe.cache().hget('portal_menu_items', frappe.session.user) - if sidebar_items == None: + if sidebar_items is None: sidebar_items = [] roles = frappe.get_roles() portal_settings = frappe.get_doc('Portal Settings', 'Portal Settings') @@ -486,7 +486,8 @@ def get_portal_sidebar_items(): items_via_hooks = frappe.get_hooks('portal_menu_items') if items_via_hooks: - for i in items_via_hooks: i['enabled'] = 1 + for i in items_via_hooks: + i['enabled'] = 1 add_items(sidebar_items, items_via_hooks) frappe.cache().hset('portal_menu_items', frappe.session.user, sidebar_items) @@ -494,9 +495,10 @@ def get_portal_sidebar_items(): return sidebar_items def get_sidebar_items_from_sidebar_file(basepath, look_for_sidebar_json): - sidebar_items = frappe._dict() + sidebar_items = [] sidebar_json_path = get_sidebar_json_path(basepath, look_for_sidebar_json) - if not sidebar_json_path: return sidebar_items + if not sidebar_json_path: + return sidebar_items with open(sidebar_json_path, 'r') as sidebarfile: try: From 0c575f02eae7c66861a12b9c264d8ae05c11e760 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 18 May 2021 11:43:42 +0530 Subject: [PATCH 0086/1519] fix: Handle encoding issue and some edge cases --- frappe/website/page_controllers/template_page.py | 2 +- frappe/website/path_resolver.py | 2 +- frappe/website/serve.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/website/page_controllers/template_page.py b/frappe/website/page_controllers/template_page.py index 12f459afd7..9f3bf66701 100644 --- a/frappe/website/page_controllers/template_page.py +++ b/frappe/website/page_controllers/template_page.py @@ -234,7 +234,7 @@ class TemplatePage(BaseTemplatePage): self.context.url_prefix += '/' self.context.path = self.path - self.context.pathname = frappe.local.path + self.context.pathname = frappe.local.path if hasattr(frappe, 'local') else self.path # for backward compatibility self.context.docs_base_url = '/docs' diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py index b9f8972380..1368932792 100644 --- a/frappe/website/path_resolver.py +++ b/frappe/website/path_resolver.py @@ -21,7 +21,7 @@ class PathResolver(): def resolve(self): '''Returns endpoint and a renderer instance that can render the endpoint''' - query_string = frappe.local.request.query_string + query_string = frappe.local.request.query_string if hasattr(frappe.local, 'request') else None resolve_redirect(self.path, query_string) endpoint = resolve_path(self.path) diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 9b28fd2e3b..7a93ada825 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -37,4 +37,4 @@ def get_response(path=None, http_status_code=200): def get_response_content(path=None, http_status_code=200): response = get_response(path, http_status_code) - return response.data + return str(response.data, 'utf-8') From ca89fa6a32a7bfb4488bb5af28f6af29aa0cf915 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 18 May 2021 14:31:34 +0530 Subject: [PATCH 0087/1519] fix: Edit & Delete Card --- frappe/desk/desktop.py | 19 +++++++-- frappe/desk/doctype/workspace/workspace.py | 13 ++++-- .../public/js/frappe/widgets/links_widget.js | 12 ++++-- .../public/js/frappe/widgets/widget_dialog.js | 6 +-- .../public/js/frappe/widgets/widget_group.js | 10 ++--- frappe/public/js/frappe/wiki_blocks/card.js | 40 ++++++++++++------- .../public/js/frappe/wiki_blocks/shortcut.js | 12 +++--- 7 files changed, 74 insertions(+), 38 deletions(-) diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 2a09978744..3713a489e3 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -550,10 +550,23 @@ def save_new_widget(page, new_widgets): if widgets.card: original_page.build_links_table_from_card(widgets.card) + # remove duplicate and unwanted widgets content = frappe.db.get_value("Internal Wiki Page", page, "content") - for wid in ['shortcut']: - widd = [x['data'][ wid + '_name'] for x in json.loads(content) if x['type'] == wid] - original_page.set(wid+'s', [ele for ele in original_page.get(wid+'s') if ele.label in widd]) + page_widgets = {} + for wid in ['shortcut', 'card', 'chart']: + # get list of widget's name from internal wiki page + page_widgets[wid] = [x['data'][wid + '_name'] for x in json.loads(content) if x['type'] == wid] + + updated_widgets = [] + original_page.get('shortcuts').reverse() + for w in original_page.get('shortcuts'): + if w.label in page_widgets['shortcut'] and w.label not in [x.label for x in updated_widgets]: + updated_widgets.append(w) + original_page.set('shortcuts', updated_widgets) + + for i, v in enumerate(original_page.links): + if v.type == 'Card Break' and v.label not in page_widgets['card']: + del original_page.links[ i : i+v.link_count+1] try: original_page.save(ignore_permissions=True) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 18aa383455..7e50b8db70 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -105,11 +105,18 @@ class Workspace(Document): for idx, card in enumerate(config): links = loads(card.get('links')) + # remove duplicate before adding + duplicate_links = [x for x in self.links if(x.label == card.get('label') and x.type == 'Card Break')] + for v in duplicate_links: + del self.links[ v.idx-1 : v.idx+v.link_count] + self.append('links', { "label": card.get('label'), "type": "Card Break", "icon": card.get('icon'), - "hidden": card.get('hidden') or False + "hidden": card.get('hidden') or False, + "link_count": card.get('link_count'), + "idx": self.links[-1].idx + 1 }) for link in links: @@ -121,10 +128,10 @@ class Workspace(Document): "onboard": link.get('onboard'), "only_for": link.get('only_for'), "dependencies": link.get('dependencies'), - "is_query_report": link.get('is_query_report') + "is_query_report": link.get('is_query_report'), + "idx": self.links[-1].idx + 1 }) - def disable_saving_as_standard(): return frappe.flags.in_install or \ frappe.flags.in_patch or \ diff --git a/frappe/public/js/frappe/widgets/links_widget.js b/frappe/public/js/frappe/widgets/links_widget.js index 05280a22c8..26d52c1fcc 100644 --- a/frappe/public/js/frappe/widgets/links_widget.js +++ b/frappe/public/js/frappe/widgets/links_widget.js @@ -12,14 +12,18 @@ export default class LinksWidget extends Widget { return { name: this.name, links: JSON.stringify(this.links), + link_count: this.links.length, label: this.label, hidden: this.hidden, }; } set_body() { - this.options = {}; - this.options.links = this.links; + + if (!this.options) { + this.options = {}; + this.options.links = this.links; + } this.widget.addClass("links-widget-box"); const is_link_disabled = item => { return item.dependencies && item.incomplete_dependencies; @@ -74,7 +78,9 @@ export default class LinksWidget extends Widget { ${get_link_for_item(item)} `); }); - + if (this.in_customize_mode) { + this.body.empty(); + } this.link_list.forEach(link => link.appendTo(this.body)); } diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 5bcaaf9e3e..02be3bd15a 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -142,9 +142,9 @@ class CardDialog extends WidgetDialog { fieldtype: 'Table', label: __('Card Links'), editable_grid: 1, - data: this.data || [], + data: me.values ? JSON.parse(me.values.links) : [], get_data: () => { - return this.data || []; + return me.values ? JSON.parse(me.values.links) : []; }, fields: [ { @@ -529,7 +529,7 @@ export default function get_dialog_constructor(type) { chart: ChartDialog, shortcut: ShortcutDialog, number_card: NumberCardDialog, - card: CardDialog, + links: CardDialog, }; return widget_map[type] || WidgetDialog; diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js index 913a362932..d8f92edc5d 100644 --- a/frappe/public/js/frappe/widgets/widget_group.js +++ b/frappe/public/js/frappe/widgets/widget_group.js @@ -205,12 +205,12 @@ export class SingleWidgetGroup { widget_type: this.type, container: this.container, height: this.height || null, + options: { + ...this.options, + on_delete: () => this.on_delete(), + on_edit: () => this.on_edit(widget_object) + } }); - widget_object.options = { - ...this.options, - on_delete: () => this.on_delete(), - on_edit: () => this.on_edit(widget_object) - }; this.widgets_list.push(widget_object); this.widgets_dict[widget.name] = widget_object; diff --git a/frappe/public/js/frappe/wiki_blocks/card.js b/frappe/public/js/frappe/wiki_blocks/card.js index 89821129e0..5426fd24ce 100644 --- a/frappe/public/js/frappe/wiki_blocks/card.js +++ b/frappe/public/js/frappe/wiki_blocks/card.js @@ -11,9 +11,10 @@ export default class Card { return true; } - constructor({data, api, config, readOnly}) { + constructor({data, api, config, readOnly, block}) { this.data = data; this.api = api; + this.block = block; this.config = config; this.readOnly = readOnly; this.sections = {}; @@ -28,7 +29,7 @@ export default class Card { allow_create: this.allow_customization, allow_delete: this.allow_customization, allow_hiding: false, - allow_edit: false, + allow_edit: true, }; } @@ -64,24 +65,25 @@ export default class Card { } _new_card() { - const dialog_class = get_dialog_constructor('card'); + const dialog_class = get_dialog_constructor('links'); this.dialog = new dialog_class({ label: this.label, type: 'card', primary_action: (widget) => { widget.in_customize_mode = 1; - let wid = frappe.widget.make_widget({ + this.card_widget = frappe.widget.make_widget({ ...widget, widget_type: 'links', - container: this.wrapper + container: this.wrapper, + options: { + ...this.options, + on_delete: () => this.api.blocks.delete(), + on_edit: () => this.on_edit(this.card_widget) + } }); - wid.options = { - ...this.options, - on_delete: () => this.api.blocks.delete() - }; - wid.customize(this.options); - this.wrapper.setAttribute("card_name", wid.label); - this.new_card_widget = wid.get_config(); + this.card_widget.customize(this.options); + this.wrapper.setAttribute("card_name", this.card_widget.label); + this.new_card_widget = this.card_widget.get_config(); }, }); @@ -156,22 +158,30 @@ export default class Card { this.card_field.make(); } + on_edit(card_obj) { + let card = card_obj.get_config(); + this.card_widget.widgets = card; + this.wrapper.setAttribute("card_name", card.label); + this.new_card_widget = card_obj.get_config(); + } + _make_cards(card_name) { let card = this.config.page_data.cards.items.find(obj => { return obj.label == card_name; }); this.wrapper.innerHTML = ''; card.in_customize_mode = !this.readOnly; - let card_widget = new frappe.widget.SingleWidgetGroup({ + this.card_widget = new frappe.widget.SingleWidgetGroup({ container: this.wrapper, type: "links", options: this.options, widgets: card, - api: this.api + api: this.api, + block: this.block }); this.wrapper.setAttribute("card_name", card_name); if (!this.readOnly) { - card_widget.customize(); + this.card_widget.customize(); } } } \ No newline at end of file diff --git a/frappe/public/js/frappe/wiki_blocks/shortcut.js b/frappe/public/js/frappe/wiki_blocks/shortcut.js index f57c9ef7f1..ba98cbc382 100644 --- a/frappe/public/js/frappe/wiki_blocks/shortcut.js +++ b/frappe/public/js/frappe/wiki_blocks/shortcut.js @@ -73,13 +73,13 @@ export default class Shortcut { this.shortcut_widget = frappe.widget.make_widget({ ...widget, widget_type: 'shortcut', - container: this.wrapper + container: this.wrapper, + options: { + ...this.options, + on_delete: () => this.api.blocks.delete(), + on_edit: () => this.on_edit(this.shortcut_widget) + } }); - this.shortcut_widget.options = { - ...this.options, - on_delete: () => this.api.blocks.delete(), - on_edit: () => this.on_edit(this.shortcut_widget) - }; this.shortcut_widget.customize(this.options); this.wrapper.setAttribute("shortcut_name", this.shortcut_widget.label); this.new_shortcut_widget = this.shortcut_widget.get_config(); From c217b32fa91c1a8e252a343bcb21f9c7283ca2e5 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 18 May 2021 15:41:23 +0530 Subject: [PATCH 0088/1519] fix: Add cache_html decorator to cache HTML --- .../website/page_controllers/template_page.py | 5 +++-- frappe/website/serve.py | 10 --------- frappe/website/utils.py | 22 +++++++++++++++++++ 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/frappe/website/page_controllers/template_page.py b/frappe/website/page_controllers/template_page.py index 9f3bf66701..635b8a56fe 100644 --- a/frappe/website/page_controllers/template_page.py +++ b/frappe/website/page_controllers/template_page.py @@ -6,8 +6,8 @@ from frappe.website.page_controllers.base_template_page import BaseTemplatePage from frappe.website.utils import get_sidebar_items from frappe.website.render import build_response from frappe.website.router import get_base_template -from frappe.website.utils import (extract_comment_tag, - extract_title, get_next_link, get_toc, get_frontmatter) +from frappe.website.utils import (extract_comment_tag, extract_title, + get_next_link, get_toc, get_frontmatter, cache_html) WEBPAGE_PY_MODULE_PROPERTIES = ("base_template_path", "template", "no_cache", "sitemap", "condition_field") @@ -45,6 +45,7 @@ class TemplatePage(BaseTemplatePage): def render(self): return build_response(self.path, self.get_html(), self.http_status_code, self.headers) + @cache_html def get_html(self): # context object should be separate from self for security # because it will be accessed via the user defined template diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 7a93ada825..7010b768d8 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -4,22 +4,12 @@ from frappe.website.page_controllers.not_permitted_page import NotPermittedPage from frappe.website.page_controllers.redirect_page import RedirectPage from frappe.website.path_resolver import PathResolver -#from frappe.website.utils import can_cache def get_response(path=None, http_status_code=200): """Resolves path and renders page""" response = None path = path or frappe.local.request.path endpoint = path - # if can_cache(): - # # return rendered page - # page_cache = frappe.cache().hget("website_page", path) - # if page_cache and frappe.local.lang in page_cache: - # out = page_cache[frappe.local.lang] - - # if out: - # frappe.local.response.from_cache = True - # return out try: path_resolver = PathResolver(path, http_status_code) diff --git a/frappe/website/utils.py b/frappe/website/utils.py index 2d16658efb..576d1d911c 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -7,6 +7,7 @@ import functools import json import os import re +from functools import wraps import yaml from past.builtins import cmp @@ -526,3 +527,24 @@ def get_sidebar_json_path(path, look_for=False): return get_sidebar_json_path(os.path.split(path)[0], look_for) else: return '' + +def cache_html(func): + @wraps(func) + def cache_html_decorator(*args, **kwargs): + if can_cache(): + html = None + page_cache = frappe.cache().hget("website_page", args[0].path) + if page_cache and frappe.local.lang in page_cache: + html = page_cache[frappe.local.lang] + if html: + frappe.local.response.from_cache = True + return html + html = func(*args, **kwargs) + if can_cache(): + page_cache = frappe.cache().hget("website_page", args[0].path) or {} + page_cache[frappe.local.lang] = html + frappe.cache().hset("website_page", args[0].path, page_cache) + + return html + + return cache_html_decorator From 7ce6a776d04bfa0b52c562275f6c8198bfebce07 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 18 May 2021 16:44:14 +0530 Subject: [PATCH 0089/1519] fix: Remove duplicate card fix --- frappe/desk/doctype/workspace/workspace.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 7e50b8db70..d9bf5fa618 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -106,9 +106,9 @@ class Workspace(Document): links = loads(card.get('links')) # remove duplicate before adding - duplicate_links = [x for x in self.links if(x.label == card.get('label') and x.type == 'Card Break')] - for v in duplicate_links: - del self.links[ v.idx-1 : v.idx+v.link_count] + for idx, link in enumerate(self.links): + if link.label == card.get('label') and link.type == 'Card Break': + del self.links[idx : idx + link.link_count + 1] self.append('links', { "label": card.get('label'), From 60454fafc84ef126f972cff41960f9a592bc8a1e Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 29 Apr 2021 14:57:02 +0530 Subject: [PATCH 0090/1519] feat: Tab Break fieldtype --- frappe/core/doctype/docfield/docfield.json | 4 +- frappe/public/build.json | 3 + frappe/public/js/frappe/form/column.js | 41 +++ frappe/public/js/frappe/form/dashboard.js | 233 +++++-------- frappe/public/js/frappe/form/form.js | 21 +- frappe/public/js/frappe/form/layout.js | 363 ++++++++------------- frappe/public/js/frappe/form/section.js | 158 +++++++++ frappe/public/js/frappe/form/tab.js | 76 +++++ frappe/public/js/frappe/ui/field_group.js | 16 +- frappe/public/scss/desk/form.scss | 22 +- 10 files changed, 542 insertions(+), 395 deletions(-) create mode 100644 frappe/public/js/frappe/form/column.js create mode 100644 frappe/public/js/frappe/form/section.js create mode 100644 frappe/public/js/frappe/form/tab.js diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index ca134665b8..4ce553b44a 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -90,7 +90,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", "reqd": 1, "search_index": 1 }, @@ -487,7 +487,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-29 06:09:26.454990", + "modified": "2021-04-15 12:59:35.484572", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/public/build.json b/frappe/public/build.json index 942871ee9b..192deab9b1 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -140,6 +140,9 @@ "public/js/frappe/ui/find.js", "public/js/frappe/ui/iconbar.js", "public/js/frappe/form/layout.js", + "public/js/frappe/form/section.js", + "public/js/frappe/form/tab.js", + "public/js/frappe/form/column.js", "public/js/frappe/ui/field_group.js", "public/js/frappe/form/link_selector.js", "public/js/frappe/form/multi_select_dialog.js", diff --git a/frappe/public/js/frappe/form/column.js b/frappe/public/js/frappe/form/column.js new file mode 100644 index 0000000000..8ceb2d029e --- /dev/null +++ b/frappe/public/js/frappe/form/column.js @@ -0,0 +1,41 @@ +frappe.ui.form.Column = class Column { + constructor(section, df) { + if (!df) df = {}; + + this.df = df; + this.section = section; + this.make(); + this.resize_all_columns(); + } + + make() { + this.wrapper = $(`
+
+
+
`).appendTo(this.section.body) + .find("form") + .on("submit", function () { + return false; + }); + + if (this.df.label) { + $(``).appendTo(this.wrapper); + } + } + + resize_all_columns() { + // distribute all columns equally + let colspan = cint(12 / this.section.wrapper.find(".form-column").length); + + this.section.wrapper.find(".form-column").removeClass() + .addClass("form-column") + .addClass("col-sm-" + colspan); + + } + + refresh() { + this.section.refresh(); + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index c1c95d94cf..c983f612e0 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -4,57 +4,79 @@ frappe.ui.form.Dashboard = class FormDashboard { constructor(opts) { $.extend(this, opts); + this.setup_dashboard_tabs(); this.setup_dashboard_sections(); } - setup_dashboard_sections() { - this.progress_area = new Section(this.parent, { - css_class: 'progress-area', + setup_dashboard_tabs() { + this.overview_tab = new frappe.ui.form.Tab(this.frm.layout, { + label: __("Overview"), hidden: 1, - collapsible: 1 + fieldname: 'dashboard-overview' }); - this.heatmap_area = new Section(this.parent, { - title: __("Overview"), + this.connections_tab = new frappe.ui.form.Tab(this.frm.layout, { + label: __("Connections"), + hidden: 1, + fieldname: 'dashboard-connection' + }); + } + + setup_dashboard_sections() { + this.progress_area = this.make_section({ + css_class: 'progress-area', + hidden: 1, + is_dashboard_section: 1, + }, this.overview_tab); + + this.heatmap_area = this.make_section({ + label: __("Overview"), css_class: 'form-heatmap', hidden: 1, - collapsible: 1, + is_dashboard_section: 1, body_html: `
` - }); + }, this.overview_tab); - this.chart_area = new Section(this.parent, { - title: __("Graph"), + this.chart_area = this.make_section({ + label: __("Graph"), css_class: 'form-graph', hidden: 1, - collapsible: 1 - }); + is_dashboard_section: 1 + }, this.overview_tab); this.stats_area_row = $(`
`); - this.stats_area = new Section(this.parent, { - title: __("Stats"), + this.stats_area = this.make_section({ + label: __("Stats"), css_class: 'form-stats', hidden: 1, - collapsible: 1, + is_dashboard_section: 1, body_html: this.stats_area_row - }); + }, this.overview_tab); this.transactions_area = $(`
') + let progress_chart = $('
') .appendTo(this.progress_area.body); return progress_chart; } @@ -169,7 +196,7 @@ frappe.ui.form.Dashboard = class FormDashboard { this.init_data(); } - var show = false; + let show = false; if (this.data && ((this.data.transactions || []).length || (this.data.reports || []).length)) { @@ -198,11 +225,10 @@ frappe.ui.form.Dashboard = class FormDashboard { } after_refresh() { - var me = this; // show / hide new buttons (if allowed) - this.links_area.body.find('.btn-new').each(function() { - if (me.frm.can_create($(this).attr('data-doctype'))) { - $(this).removeClass('hidden'); + this.links_area.body.find('.btn-new').each((i, el) => { + if (this.frm.can_create($(this).attr('data-doctype'))) { + $(el).removeClass('hidden'); } }); } @@ -269,7 +295,7 @@ frappe.ui.form.Dashboard = class FormDashboard { } render_links() { - var me = this; + let me = this; this.links_area.show(); this.links_area.body.find('.btn-new').addClass('hidden'); if (this.data_rendered) { @@ -324,7 +350,7 @@ frappe.ui.form.Dashboard = class FormDashboard { open_document_list($link, show_open) { // show document list with filters - var doctype = $link.attr('data-doctype'), + let doctype = $link.attr('data-doctype'), names = $link.attr('data-names') || []; if (this.data.internal_links[doctype]) { @@ -346,8 +372,8 @@ frappe.ui.form.Dashboard = class FormDashboard { get_document_filter(doctype) { // return the default filter for the given document // like {"customer": frm.doc.name} - var filter = {}; - var fieldname = this.data.non_standard_fieldnames + let filter = {}; + let fieldname = this.data.non_standard_fieldnames ? (this.data.non_standard_fieldnames[doctype] || this.data.fieldname) : this.data.fieldname; @@ -366,7 +392,7 @@ frappe.ui.form.Dashboard = class FormDashboard { } // list all items from the transaction list - var items = [], + let items = [], me = this; this.data.transactions.forEach(function(group) { @@ -375,7 +401,7 @@ frappe.ui.form.Dashboard = class FormDashboard { }); }); - var method = this.data.method || 'frappe.desk.notifications.get_open_count'; + let method = this.data.method || 'frappe.desk.notifications.get_open_count'; frappe.call({ type: "GET", method: method, @@ -424,7 +450,7 @@ frappe.ui.form.Dashboard = class FormDashboard { } set_badge_count(doctype, open_count, count, names) { - var $link = $(this.transactions_area) + let $link = $(this.transactions_area) .find('.document-link[data-doctype="'+doctype+'"]'); if (open_count) { @@ -471,7 +497,7 @@ frappe.ui.form.Dashboard = class FormDashboard { this.heatmap_area.body.find('svg').css({'margin': 'auto'}); // message - var heatmap_message = this.heatmap_area.body.find('.heatmap-message'); + let heatmap_message = this.heatmap_area.body.find('.heatmap-message'); if (this.data.heatmap_message) { heatmap_message.removeClass('hidden').html(this.data.heatmap_message); } else { @@ -486,9 +512,9 @@ frappe.ui.form.Dashboard = class FormDashboard { // set colspan - var indicators = this.stats_area_row.find('.indicator-column'); - var n_indicators = indicators.length + 1; - var colspan; + let indicators = this.stats_area_row.find('.indicator-column'); + let n_indicators = indicators.length + 1; + let colspan; if (n_indicators > 4) { colspan = 3; } else { @@ -500,7 +526,7 @@ frappe.ui.form.Dashboard = class FormDashboard { indicators.removeClass().addClass('col-sm-'+colspan).addClass('indicator-column'); } - var indicator = $('
' + let indicator = $('
' +label+'
').appendTo(this.stats_area_row); return indicator; @@ -508,9 +534,9 @@ frappe.ui.form.Dashboard = class FormDashboard { // graphs setup_graph() { - var me = this; - var method = this.data.graph_method; - var args = { + let me = this; + let method = this.data.graph_method; + let args = { doctype: this.frm.doctype, docname: this.frm.doc.name, }; @@ -574,11 +600,10 @@ frappe.ui.form.Dashboard = class FormDashboard { } add_comment(text, alert_class, permanent) { - var me = this; this.set_headline_alert(text, alert_class); if (!permanent) { - setTimeout(function() { - me.clear_headline(); + setTimeout(() => { + this.clear_headline(); }, 10000); } } @@ -595,109 +620,3 @@ frappe.ui.form.Dashboard = class FormDashboard { } } }; - -class Section { - constructor(parent, options) { - this.parent = parent; - this.df = options || {}; - this.make(); - - if (this.df.title && this.df.collapsible && localStorage.getItem(options.css_class + '-closed')) { - this.collapse(); - } - this.refresh(); - } - - make() { - this.wrapper = $(`
`) - .appendTo(this.parent); - - if (this.df) { - if (this.df.title) { - this.make_head(); - } - if (this.df.description) { - this.description_wrapper = $( - `
- ${__(this.df.description)} -
` - ); - - this.wrapper.append(this.description_wrapper); - } - if (this.df.css_class) { - this.wrapper.addClass(this.df.css_class); - } - if (this.df.hide_border) { - this.wrapper.toggleClass("hide-border", true); - } - } - - this.body = $('
').appendTo(this.wrapper); - - if (this.df.body_html) { - this.body.append(this.df.body_html); - } - } - - make_head() { - this.head = $(` -
- ${__(this.df.title)} - -
- `); - - this.head.appendTo(this.wrapper); - this.indicator = this.head.find('.collapse-indicator'); - this.indicator.hide(); - - if (this.df.collapsible) { - // show / hide based on status - this.collapse_link = this.head.on("click", () => { - this.collapse(); - }); - this.set_icon(); - this.indicator.show(); - } - } - - refresh() { - if (!this.df) return; - - // hide if explicitly hidden - let hide = this.df.hidden; - this.wrapper.toggle(!hide); - } - - collapse(hide) { - if (hide === undefined) { - hide = !this.body.hasClass("hide"); - } - - this.body.toggleClass("hide", hide); - this.head && this.head.toggleClass("collapsed", hide); - - this.set_icon(hide); - - // save state for next reload ('' is falsy) - localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : ''); - } - - set_icon(hide) { - let indicator_icon = hide ? 'down' : 'up-line'; - this.indicator && this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); - } - - is_collapsed() { - return this.body.hasClass('hide'); - } - - hide() { - this.wrapper.hide(); - } - - show() { - this.wrapper.show(); - } -} diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index a188c42950..83d38b323b 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -93,6 +93,11 @@ frappe.ui.form.Form = class FrappeForm { this.watch_model_updates(); if (!this.meta.hide_toolbar && frappe.boot.desk_settings.timeline) { + // this.footer_tab = new frappe.ui.form.Tab(this.layout, { + // label: __("Activity"), + // fieldname: 'timeline' + // }); + this.footer = new frappe.ui.form.Footer({ frm: this, parent: $('
').appendTo(this.page.main.parent()) @@ -141,6 +146,7 @@ frappe.ui.form.Form = class FrappeForm { frm: this, with_dashboard: true, card_layout: true, + tabbed_layout: true, }); this.layout.make(); @@ -149,7 +155,7 @@ frappe.ui.form.Form = class FrappeForm { this.dashboard = new frappe.ui.form.Dashboard({ frm: this, - parent: $('
').insertAfter(this.layout.wrapper.find('.form-message')) + parent: this.layout.wrapper, }); // workflow state @@ -453,7 +459,7 @@ frappe.ui.form.Form = class FrappeForm { }, () => this.cscript.is_onload && this.is_new() && this.focus_on_first_input(), () => this.run_after_load_hook(), - () => this.dashboard.after_refresh() + () => this.dashboard.after_refresh(), ]); } else { @@ -462,6 +468,7 @@ frappe.ui.form.Form = class FrappeForm { this.$wrapper.trigger('render_complete'); + this.cscript.is_onload && this.set_first_tab_as_active(); if(!this.hidden) { this.layout.show_empty_form_message(); } @@ -469,6 +476,11 @@ frappe.ui.form.Form = class FrappeForm { this.scroll_to_element(); } + set_first_tab_as_active() { + this.layout.tabs[0] + && this.layout.tabs[0].set_active(); + } + focus_on_first_input() { let first = this.form_wrapper.find('.form-layout :input:visible:first'); if (!in_list(["Date", "Datetime"], first.attr("data-fieldtype"))) { @@ -1580,6 +1592,11 @@ frappe.ui.form.Form = class FrappeForm { let $el = field.$wrapper; + // set tab as active + if (field.tab && !field.tab.is_active()) { + field.tab.set_active(); + } + // uncollapse section if (field.section.is_collapsed()) { field.section.collapse(false); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 282655b589..2ffba7b15a 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -1,27 +1,38 @@ -import '../class'; - frappe.ui.form.Layout = class Layout { constructor (opts) { this.views = {}; this.pages = []; + this.tabs = []; this.sections = []; this.fields_list = []; this.fields_dict = {}; $.extend(this, opts); } + make() { if (!this.parent && this.body) { this.parent = this.body; } this.wrapper = $('
').appendTo(this.parent); this.message = $('').appendTo(this.wrapper); + + this.page = $('
').appendTo(this.wrapper); + $(`
+ +
`).appendTo(this.page); + this.tabs_list = this.page.find('.form-tabs'); + this.tabs_content = $(`
`).appendTo(this.page); + if (!this.fields) { this.fields = this.get_doctype_fields(); } + + this.setup_events(); this.setup_tabbing(); this.render(); } + show_empty_form_message() { if (!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) { this.show_message(__("This form does not have any input")); @@ -87,9 +98,9 @@ frappe.ui.form.Layout = class Layout { this.message.empty().addClass('hidden'); } } - render (new_fields) { - var me = this; - var fields = new_fields || this.fields; + + render(new_fields) { + let fields = new_fields || this.fields; this.section = null; this.column = null; @@ -98,38 +109,45 @@ frappe.ui.form.Layout = class Layout { this.setup_dashboard_section(); } + if (this.tabbed_layout) { + this.first_tab = this.make_tab({label: __('Details'), fieldname: 'details'}) + } + if (this.no_opening_section()) { this.make_section(); } - $.each(fields, function (i, df) { + + fields.forEach(df => { switch (df.fieldtype) { case "Fold": - me.make_page(df); + this.make_page(df); break; case "Section Break": - me.make_section(df); + this.make_section(df); break; case "Column Break": - me.make_column(df); + this.make_column(df); + break; + case "Tab Break": + this.make_tab(df); break; default: - me.make_field(df); + this.make_field(df); } }); - } - no_opening_section () { + no_opening_section() { return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length; } - setup_dashboard_section () { + setup_dashboard_section() { if (this.no_opening_section()) { this.fields.unshift({fieldtype: 'Section Break'}); } } - replace_field (fieldname, df, render) { + replace_field(fieldname, df, render) { df.fieldname = fieldname; // change of fieldname is avoided if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) { const fieldobj = this.init_field(df, render); @@ -145,7 +163,7 @@ frappe.ui.form.Layout = class Layout { } } - make_field (df, colspan, render) { + make_field(df, colspan, render) { !this.section && this.make_section(); !this.column && this.make_column(); @@ -159,9 +177,10 @@ frappe.ui.form.Layout = class Layout { this.section.fields_list.push(fieldobj); this.section.fields_dict[df.fieldname] = fieldobj; fieldobj.section = this.section; + fieldobj.tab = this.tab; } - init_field (df, render = false) { + init_field(df, render=false) { const fieldobj = frappe.ui.form.make_control({ df: df, doctype: this.doctype, @@ -176,8 +195,8 @@ frappe.ui.form.Layout = class Layout { return fieldobj; } - make_page (df) { // eslint-disable-line no-unused-vars - var me = this, + make_page(df) { // eslint-disable-line no-unused-vars + let me = this, head = $('').appendTo(this.wrapper); @@ -185,7 +204,7 @@ frappe.ui.form.Layout = class Layout { this.page = $('
').appendTo(this.wrapper); this.fold_btn = head.find(".btn-fold").on("click", function () { - var page = $(this).parent().next(); + let page = $(this).parent().next(); if (page.hasClass("hide")) { $(this).removeClass("btn-fold").html(__("Hide details")); page.removeClass("hide"); @@ -202,12 +221,12 @@ frappe.ui.form.Layout = class Layout { this.folded = true; } - unfold () { + unfold() { this.fold_btn.trigger('click'); } - make_section (df) { - this.section = new frappe.ui.form.Section(this, df); + make_section(df) { + this.section = new frappe.ui.form.Section(this, df, this.tab || null); // append to layout fields if (df) { @@ -218,15 +237,25 @@ frappe.ui.form.Layout = class Layout { this.column = null; } - make_column (df) { + make_column(df) { this.column = new frappe.ui.form.Column(this.section, df); if (df && df.fieldname) { this.fields_list.push(this.column); } } - refresh (doc) { - var me = this; + make_tab(df) { + this.tab = new frappe.ui.form.Tab(this, df); + + if (df) { + this.fields_dict[df.fieldname] = this.tab; + this.fields_list.push(this.tab); + } + + return this.tab; + } + + refresh(doc) { if (doc) this.doc = doc; if (this.frm) { @@ -234,7 +263,7 @@ frappe.ui.form.Layout = class Layout { } // NOTE this might seem redundant at first, but it needs to be executed when frm.refresh_fields is called - me.attach_doc_and_docfields(true); + this.attach_doc_and_docfields(true); if (this.frm && this.frm.wrapper) { $(this.frm.wrapper).trigger("refresh-fields"); @@ -246,6 +275,9 @@ frappe.ui.form.Layout = class Layout { // refresh sections this.refresh_sections(); + // refresh tabs + this.refresh_tabs(); + if (this.frm) { // collapse sections this.refresh_section_collapse(); @@ -265,10 +297,26 @@ frappe.ui.form.Layout = class Layout { }); this.frm && this.frm.dashboard.refresh(); - } - refresh_fields (fields) { + refresh_tabs() { + this.tabs.forEach(tab => { + if (!tab.wrapper.hasClass('hide') && !tab.parent.hasClass('hide')) { + tab.parent.removeClass('show hide'); + tab.wrapper.removeClass('show hide'); + if (tab.wrapper.find( + ".form-section:not(.hide-control, .empty-section), .form-dashboard-section:not(.hide-control, .empty-section)" + ).length + ) { + tab.toggle(true) + } else { + tab.toggle(false) + } + } + }); + } + + refresh_fields(fields) { let fieldnames = fields.map((field) => { if (field.fieldname) return field.fieldname; }); @@ -283,7 +331,7 @@ frappe.ui.form.Layout = class Layout { }); } - add_fields (fields) { + add_fields(fields) { this.render(fields); this.refresh_fields(fields); } @@ -291,11 +339,11 @@ frappe.ui.form.Layout = class Layout { refresh_section_collapse () { if (!(this.sections && this.sections.length)) return; - for (var i = 0; i < this.sections.length; i++) { - var section = this.sections[i]; - var df = section.df; + for (let i = 0; i < this.sections.length; i++) { + let section = this.sections[i]; + let df = section.df; if (df && df.collapsible) { - var collapse = true; + let collapse = true; if (df.collapsible_depends_on) { collapse = !this.evaluate_depends_on_value(df.collapsible_depends_on); @@ -310,10 +358,10 @@ frappe.ui.form.Layout = class Layout { } } - attach_doc_and_docfields (refresh) { - var me = this; - for (var i = 0, l = this.fields_list.length; i < l; i++) { - var fieldobj = this.fields_list[i]; + attach_doc_and_docfields(refresh) { + let me = this; + for (let i = 0, l = this.fields_list.length; i < l; i++) { + let fieldobj = this.fields_list[i]; if (me.doc) { fieldobj.doc = me.doc; fieldobj.doctype = me.doc.doctype; @@ -330,41 +378,49 @@ frappe.ui.form.Layout = class Layout { } } - refresh_section_count () { + refresh_section_count() { this.wrapper.find(".section-count-label:visible").each(function (i) { $(this).html(i + 1); }); } - setup_tabbing () { - var me = this; - this.wrapper.on("keydown", function (ev) { + + setup_events() { + this.tabs_list.off('click').on('click', '.nav-link', (e) => { + e.preventDefault() + e.stopImmediatePropagation(); + $(e.currentTarget).tab('show'); + // this.$current_tab = $(e.currentTarget); + }); + } + + setup_tabbing() { + this.wrapper.on("keydown", (ev) => { if (ev.which == 9) { - var current = $(ev.target), + let current = $(ev.target), doctype = current.attr("data-doctype"), fieldname = current.attr("data-fieldname"); if (doctype) - return me.handle_tab(doctype, fieldname, ev.shiftKey); + return this.handle_tab(doctype, fieldname, ev.shiftKey); } }); } - handle_tab (doctype, fieldname, shift) { - var me = this, - grid_row = null, + + handle_tab(doctype, fieldname, shift) { + let grid_row = null, prev = null, - fields = me.fields_list, - in_grid = false, + fields = this.fields_list, focused = false; // in grid - if (doctype != me.doctype) { - grid_row = me.get_open_grid_row(); + if (doctype != this.doctype) { + grid_row = this.get_open_grid_row(); if (!grid_row || !grid_row.layout) { return; } fields = grid_row.layout.fields_list; } - for (var i = 0, len = fields.length; i < len; i++) { + for (let i = 0, len = fields.length; i < len; i++) { if (fields[i].df.fieldname == fieldname) { if (shift) { if (prev) { @@ -375,7 +431,7 @@ frappe.ui.form.Layout = class Layout { break; } if (i < len - 1) { - focused = me.focus_on_next_field(i, fields); + focused = this.focus_on_next_field(i, fields); } if (focused) { @@ -406,10 +462,11 @@ frappe.ui.form.Layout = class Layout { return false; } - focus_on_next_field (start_idx, fields) { + + focus_on_next_field(start_idx, fields) { // loop to find next eligible fields - for (var i = start_idx + 1, len = fields.length; i < len; i++) { - var field = fields[i]; + for (let i = start_idx + 1, len = fields.length; i < len; i++) { + let field = fields[i]; if (this.is_visible(field)) { if (field.df.fieldtype === "Table") { // open table grid @@ -428,10 +485,12 @@ frappe.ui.form.Layout = class Layout { } } } - is_visible (field) { + + is_visible(field) { return field.disp_status === "Write" && (field.$wrapper && field.$wrapper.is(":visible")); } - set_focus (field) { + + set_focus(field) { // next is table, show the table if (field.df.fieldtype=="Table") { if (!field.grid.grid_rows.length) { @@ -445,18 +504,19 @@ frappe.ui.form.Layout = class Layout { field.$input.focus(); } } - get_open_grid_row () { + + get_open_grid_row() { return $(".grid-row-open").data("grid_row"); } - refresh_dependency () { + + refresh_dependency() { // Resolve "depends_on" and show / hide accordingly - var me = this; // build dependants' dictionary - var has_dep = false; + let has_dep = false; - for (var fkey in this.fields_list) { - var f = this.fields_list[fkey]; + for (let fkey in this.fields_list) { + let f = this.fields_list[fkey]; f.dependencies_clear = true; if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) { has_dep = true; @@ -466,8 +526,8 @@ frappe.ui.form.Layout = class Layout { if (!has_dep) return; // show / hide based on values - for (var i = me.fields_list.length - 1; i >= 0; i--) { - var f = me.fields_list[i]; + for (let i = this.fields_list.length - 1; i >= 0; i--) { + let f = this.fields_list[i]; f.guardian_has_value = true; if (f.df.depends_on) { // evaluate guardian @@ -499,7 +559,8 @@ frappe.ui.form.Layout = class Layout { this.refresh_section_count(); } - set_dependant_property (condition, fieldname, property) { + + set_dependant_property(condition, fieldname, property) { let set_property = this.evaluate_depends_on_value(condition); let value = set_property ? 1 : 0; let form_obj; @@ -521,19 +582,20 @@ frappe.ui.form.Layout = class Layout { } } } - evaluate_depends_on_value (expression) { - var out = null; - var doc = this.doc; + + evaluate_depends_on_value(expression) { + let out = null; + let doc = this.doc; if (!doc && this.get_values) { - var doc = this.get_values(true); + let doc = this.get_values(true); } if (!doc) { return; } - var parent = this.frm ? this.frm.doc : this.doc || null; + let parent = this.frm ? this.frm.doc : this.doc || null; if (typeof (expression) === 'boolean') { out = expression; @@ -564,161 +626,4 @@ frappe.ui.form.Layout = class Layout { return out; } -}; - -frappe.ui.form.Section = class FormSection { - constructor(layout, df) { - this.layout = layout; - this.df = df || {}; - this.fields_list = []; - this.fields_dict = {}; - - this.make(); - // if (this.frm) - // this.section.body.css({"padding":"0px 3%"}) - this.row = { - wrapper: this.wrapper - }; - - this.refresh(); - } - make() { - if (!this.layout.page) { - this.layout.page = $('
').appendTo(this.layout.wrapper); - } - let make_card = this.layout.card_layout; - this.wrapper = $(`
`) - .appendTo(this.layout.page); - this.layout.sections.push(this); - - if (this.df) { - if (this.df.label) { - this.make_head(); - } - if (this.df.description) { - $('
' + __(this.df.description) + '
') - .appendTo(this.wrapper); - } - if (this.df.cssClass) { - this.wrapper.addClass(this.df.cssClass); - } - if (this.df.hide_border) { - this.wrapper.toggleClass("hide-border", true); - } - } - - // for bc - this.body = $('
').appendTo(this.wrapper); - } - - make_head () { - this.head = $(`
- ${__(this.df.label)} - - -
`); - this.head.appendTo(this.wrapper); - this.indicator = this.head.find('.collapse-indicator'); - this.indicator.hide(); - if (this.df.collapsible) { - // show / hide based on status - this.collapse_link = this.head.on("click", () => { - this.collapse(); - }); - - this.indicator.show(); - } - } - refresh() { - if (!this.df) - return; - - // hide if explictly hidden - var hide = this.df.hidden || this.df.hidden_due_to_dependency; - - // hide if no perm - if (!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) { - hide = true; - } - - this.wrapper.toggleClass("hide-control", !!hide); - } - collapse (hide) { - // unknown edge case - if (!(this.head && this.body)) { - return; - } - - if (hide===undefined) { - hide = !this.body.hasClass("hide"); - } - - this.body.toggleClass("hide", hide); - this.head.toggleClass("collapsed", hide); - - let indicator_icon = hide ? 'down' : 'up-line'; - - this.indicator & this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); - - // refresh signature fields - this.fields_list.forEach((f) => { - if (f.df.fieldtype == 'Signature') { - f.refresh(); - } - }); - } - - is_collapsed() { - return this.body.hasClass('hide'); - } - - has_missing_mandatory () { - var missing_mandatory = false; - for (var j = 0, l = this.fields_list.length; j < l; j++) { - var section_df = this.fields_list[j].df; - if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) { - missing_mandatory = true; - break; - } - } - return missing_mandatory; - } -}; - -frappe.ui.form.Column = class FormColumn { - constructor(section, df) { - if (!df) df = {}; - - this.df = df; - this.section = section; - this.make(); - this.resize_all_columns(); - } - make () { - this.wrapper = $('
\ -
\ -
\ -
').appendTo(this.section.body) - .find("form") - .on("submit", function () { - return false; - }); - - if (this.df.label) { - $('').appendTo(this.wrapper); - } - } - resize_all_columns () { - // distribute all columns equally - var colspan = cint(12 / this.section.wrapper.find(".form-column").length); - - this.section.wrapper.find(".form-column").removeClass() - .addClass("form-column") - .addClass("col-sm-" + colspan); - - } - refresh () { - this.section.refresh(); - } -}; +} diff --git a/frappe/public/js/frappe/form/section.js b/frappe/public/js/frappe/form/section.js new file mode 100644 index 0000000000..f84dd6a58b --- /dev/null +++ b/frappe/public/js/frappe/form/section.js @@ -0,0 +1,158 @@ +// import '../class'; + +frappe.ui.form.Section = class Section { + constructor(layout, df, tab) { + this.layout = layout; + this.tab = tab; + this.parent = this.tab && this.tab.wrapper || null; + this.df = df || {}; + this.fields_list = []; + this.fields_dict = {}; + + this.make(); + + if (this.df.label && this.df.collapsible && localStorage.getItem(df.css_class + '-closed')) { + this.collapse(); + } + + this.row = { + wrapper: this.wrapper + }; + + this.refresh(); + } + + make() { + if (!this.layout.page) { + this.layout.page = $('
').appendTo(this.layout.wrapper); + } + + let make_card = this.layout.card_layout; + + this.wrapper = $(`
+ `).appendTo(this.parent || this.layout.page); + + this.layout.sections.push(this); + + if (this.df) { + if (this.df.label) { + this.make_head(); + } + if (this.df.description) { + this.description_wrapper = $( + `
+ ${__(this.df.description)} +
` + ); + + this.wrapper.append(this.description_wrapper); + } + if (this.df.css_class) { + this.wrapper.addClass(this.df.css_class); + } + if (this.df.hide_border) { + this.wrapper.toggleClass("hide-border", true); + } + } + + this.body = $('
').appendTo(this.wrapper); + + if (this.df.body_html) { + this.body.append(this.df.body_html); + } + } + + make_head() { + this.head = $(` +
+ ${__(this.df.label)} + +
+ `); + + this.head.appendTo(this.wrapper); + this.indicator = this.head.find('.collapse-indicator'); + this.indicator.hide(); + + if (this.df.collapsible) { + // show / hide based on status + this.collapse_link = this.head.on("click", () => { + this.collapse(); + }); + this.set_icon(); + this.indicator.show(); + } + } + + refresh() { + if (!this.df) return; + + // hide if explicitly hidden + let hide = this.df.hidden || this.df.hidden_due_to_dependency; + if (!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) { + hide = true; + } + + this.wrapper.toggleClass("hide-control", !!hide); + // this.tab && this.tab.refresh(); + } + + collapse(hide) { + // unknown edge case + if (!(this.head && this.body)) { + return; + } + + if (hide === undefined) { + hide = !this.body.hasClass("hide"); + } + + this.body.toggleClass("hide", hide); + this.head && this.head.toggleClass("collapsed", hide); + + this.set_icon(hide); + + // refresh signature fields + this.fields_list.forEach((f) => { + if (f.df.fieldtype == 'Signature') { + f.refresh(); + } + }); + + // save state for next reload ('' is falsy) + if (this.df.css_class) + localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : ''); + } + + set_icon(hide) { + let indicator_icon = hide ? 'down' : 'up-line'; + this.indicator && this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); + } + + is_collapsed() { + return this.body.hasClass('hide'); + } + + has_missing_mandatory () { + var missing_mandatory = false; + for (var j = 0, l = this.fields_list.length; j < l; j++) { + var section_df = this.fields_list[j].df; + if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) { + missing_mandatory = true; + break; + } + } + return missing_mandatory; + } + + hide() { + this.wrapper.toggleClass("hide-control", true); + } + + show() { + this.wrapper.toggleClass("hide-control", false); + this.tab && this.tab.toggle(true); + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/form/tab.js b/frappe/public/js/frappe/form/tab.js new file mode 100644 index 0000000000..bcd532061f --- /dev/null +++ b/frappe/public/js/frappe/form/tab.js @@ -0,0 +1,76 @@ +frappe.ui.form.Tab = class Tab { + constructor(layout, df) { + this.layout = layout; + this.df = df || {}; + this.label = this.df && this.df.label || 'Details'; + this.fields_list = []; + this.fields_dict = {}; + this.make(); + this.refresh(); + } + + make() { + if (!this.layout.page) { + this.layout.page = $('
').appendTo(this.layout.wrapper); + } + + const id = `${frappe.scrub(this.layout.doctype, '-')}-${this.df.fieldname}`; + this.parent = $(``).appendTo(this.layout.tabs_list); + + this.wrapper = $(`
+ `).appendTo(this.layout.tabs_content); + + this.layout.tabs.push(this); + } + + set_content() { + + } + + refresh() { + if (!this.df) return; + + // hide if explicitly hidden + let hide = this.df.hidden || this.df.hidden_due_to_dependency; + if (!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) { + hide = true; + } + this.toggle(!hide); + } + + toggle(show) { + this.parent.toggleClass('hide', !show); + this.wrapper.toggleClass('hide', !show); + this.parent.toggleClass('show', show); + this.wrapper.toggleClass('show', show); + } + + show() { + this.parent.show(); + } + + hide() { + this.parent.hide(); + } + + set_active() { + this.parent.find('.nav-link').tab('show'); + this.wrapper.addClass('show'); + } + + is_active() { + return this.wrapper.hasClass('active'); + } + + is_hidden() { + this.wrapper.hasClass('hidden') + && this.parent.hasClass('hidden'); + } +} diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index b8b908eb95..9fc357e9c4 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -5,7 +5,6 @@ frappe.provide('frappe.ui'); frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { constructor(opts) { super(opts); - this.first_button = false; this.dirty = false; $.each(this.fields || [], function(i, f) { if(!f.fieldname && f.label) { @@ -16,6 +15,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { this.set_values(this.values); } } + make() { var me = this; if(this.fields) { @@ -61,6 +61,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } }); } + catch_enter_as_submit() { var me = this; $(this.body).find('input[type="text"], input[type="password"], select').keypress(function(e) { @@ -72,13 +73,16 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } }); } + get_input(fieldname) { var field = this.fields_dict[fieldname]; return $(field.txt ? field.txt : field.input); } + get_field(fieldname) { return this.fields_dict[fieldname]; } + get_values(ignore_errors) { var ret = {}; var errors = []; @@ -111,10 +115,12 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } return ret; } + get_value(key) { var f = this.fields_dict[key]; return f && (f.get_value ? f.get_value() : null); } + set_value(key, val){ return new Promise(resolve => { var f = this.fields_dict[key]; @@ -129,9 +135,11 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } }); } + set_input(key, val) { return this.set_value(key, val); } + set_values(dict) { let promises = []; for(var key in dict) { @@ -142,6 +150,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { return Promise.all(promises); } + clear() { for(var key in this.fields_dict) { var f = this.fields_dict[key]; @@ -150,9 +159,10 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } } } + set_df_property (fieldname, prop, value) { - const field = this.get_field(fieldname); + const field = this.get_field(fieldname); field.df[prop] = value; field.refresh(); } -}; +} diff --git a/frappe/public/scss/desk/form.scss b/frappe/public/scss/desk/form.scss index 0bb686f045..a7f9fd5ab6 100644 --- a/frappe/public/scss/desk/form.scss +++ b/frappe/public/scss/desk/form.scss @@ -50,11 +50,11 @@ @extend .frappe-card; } -.form-dashboard { +.form-dashboard-section { .section-body:first-child { margin-top: 0; } - .form-dashboard-section .section-body { + .section-body { display: block; padding-left: var(--padding-md); padding-right: var(--padding-md); @@ -302,6 +302,24 @@ } } +.form-tabs { + .nav-item { + .nav-link { + padding-bottom: 15px; + color: var(--gray-700); + padding-left: 0; + padding-right: 0; + margin-right: 30px; + + &.active { + font-weight: 500; + border-bottom: 1px solid var(--primary); + color: var(--text-color); + } + } + } +} + .progress-area { padding-top: var(--padding-md); padding-bottom: var(--padding-md); From 6e9555ca815ea3b0670834a0135911b5b654083d Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 29 Apr 2021 15:02:47 +0530 Subject: [PATCH 0091/1519] feat: add Tab Break to Custom Field fieldtype --- frappe/custom/doctype/custom_field/custom_field.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 2f0819ab68..4f987e3d14 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -120,7 +120,7 @@ "label": "Field Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", "reqd": 1 }, { @@ -417,7 +417,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-29 06:14:43.073329", + "modified": "2021-04-29 15:02:06.442612", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", From 4be537d287125aade9c357efc393d245fdb3ec5c Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 30 Apr 2021 12:58:47 +0530 Subject: [PATCH 0092/1519] fix: add bottom margin to tabs --- frappe/public/scss/desk/form.scss | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/frappe/public/scss/desk/form.scss b/frappe/public/scss/desk/form.scss index a7f9fd5ab6..c2a7c00e40 100644 --- a/frappe/public/scss/desk/form.scss +++ b/frappe/public/scss/desk/form.scss @@ -302,19 +302,23 @@ } } -.form-tabs { - .nav-item { - .nav-link { - padding-bottom: 15px; - color: var(--gray-700); - padding-left: 0; - padding-right: 0; - margin-right: 30px; +.form-tabs-list { + margin-bottom: var(--margin-lg); - &.active { - font-weight: 500; - border-bottom: 1px solid var(--primary); - color: var(--text-color); + .form-tabs { + .nav-item { + .nav-link { + padding-bottom: var(--padding-md); + color: var(--gray-700); + padding-left: 0; + padding-right: 0; + margin-right: var(--margin-xl); + + &.active { + font-weight: 500; + border-bottom: 1px solid var(--primary); + color: var(--text-color); + } } } } @@ -373,7 +377,4 @@ .form-column:not(:first-child) { padding-top: var(--padding-md); } - - - } From 022f7e1c06c8a076fe1c36e2a60059242b077015 Mon Sep 17 00:00:00 2001 From: prssanna Date: Sat, 8 May 2021 17:59:25 +0530 Subject: [PATCH 0093/1519] fix: label for Tab Break fields --- frappe/core/doctype/doctype/doctype.py | 2 ++ frappe/custom/doctype/custom_field/custom_field.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 84673f990a..3cd336377c 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -277,6 +277,8 @@ class DocType(Document): d.fieldname = d.fieldname + '_section' elif d.fieldtype=='Column Break': d.fieldname = d.fieldname + '_column' + elif d.fieldtype=='Tab Break': + d.fieldname = d.fieldname + '_tab' else: d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) else: diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index 39aff8b4a7..dc09173595 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -19,7 +19,7 @@ class CustomField(Document): if not self.fieldname: label = self.label if not label: - if self.fieldtype in ["Section Break", "Column Break"]: + if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]: label = self.fieldtype + "_" + str(self.idx) else: frappe.throw(_("Label is mandatory")) From ea3ca571ad768e0d6e1b368b6ffdfe21d76bb75a Mon Sep 17 00:00:00 2001 From: prssanna Date: Sat, 8 May 2021 18:00:30 +0530 Subject: [PATCH 0094/1519] fix: add Tab Break to no value fields --- frappe/custom/doctype/property_setter/property_setter.py | 2 +- frappe/model/__init__.py | 2 ++ .../page/print_format_builder/print_format_builder.js | 6 +++--- .../print_format_builder/print_format_builder_sidebar.html | 2 +- frappe/public/js/frappe/form/toolbar.js | 2 +- frappe/public/js/frappe/model/model.js | 4 ++-- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index 56e5829271..f7e8650489 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -35,7 +35,7 @@ class PropertySetter(Document): fields=['fieldname', 'label', 'fieldtype'], filters={ 'parent': dt, - 'fieldtype': ['not in', ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields], + 'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields], 'fieldname': ['!=', ''] }, order_by='label asc', diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 205b451336..356275e7b8 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -41,6 +41,7 @@ data_fieldtypes = ( no_value_fields = ( 'Section Break', 'Column Break', + 'Tab Break', 'HTML', 'Table', 'Table MultiSelect', @@ -53,6 +54,7 @@ no_value_fields = ( display_fieldtypes = ( 'Section Break', 'Column Break', + 'Tab Break', 'HTML', 'Button', 'Image', diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index ca2a8bc378..51cac66026 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -261,7 +261,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { } else if(f.fieldtype==="Column Break") { set_column(); - } else if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype) + } else if(!in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype) && f.label) { if(!column) set_column(); @@ -298,7 +298,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { init_visible_columns(f) { f.visible_columns = [] $.each(frappe.get_meta(f.options).fields, function(i, _f) { - if(!in_list(["Section Break", "Column Break"], _f.fieldtype) && + if(!in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) && !_f.print_hide && f.label) { // column names set as fieldname|width @@ -606,7 +606,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { // add remaining fields $.each(doc_fields, function(j, f) { if (f && !in_list(column_names, f.fieldname) - && !in_list(["Section Break", "Column Break"], f.fieldtype) && f.label) { + && !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) && f.label) { fields.push(f); } }) diff --git a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html index 1ebb87ac31..c608eecbbd 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html +++ b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html @@ -4,7 +4,7 @@