diff --git a/cypress/integration/form.js b/cypress/integration/form.js
index 20ed7a61cd..909955c1df 100644
--- a/cypress/integration/form.js
+++ b/cypress/integration/form.js
@@ -18,6 +18,7 @@ context('Form', () => {
cy.get('.primary-action').click();
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
cy.visit('/app/todo');
+ cy.wait(300);
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
cy.get('.list-row').should('contain', 'this is a test todo');
});
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index 5154adb634..efa1959969 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -258,12 +258,17 @@ function get_watch_config() {
async function clean_dist_folders(apps) {
for (let app of apps) {
let public_path = get_public_path(app);
- await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
- recursive: true
- });
- await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
- recursive: true
- });
+ let paths = [
+ path.resolve(public_path, "dist", "js"),
+ path.resolve(public_path, "dist", "css")
+ ];
+ for (let target of paths) {
+ if (fs.existsSync(target)) {
+ // rmdir is deprecated in node 16, this will work in both node 14 and 16
+ let rmdir = fs.promises.rm || fs.promises.rmdir;
+ await rmdir(target, { recursive: true });
+ }
+ }
}
}
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 9d8c5d3607..01c7879a06 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -1491,7 +1491,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_content
from frappe.utils.pdf import get_pdf
local.form_dict.doctype = doctype
@@ -1506,7 +1506,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_content("printview")
if as_pdf:
return get_pdf(html, output = output, options = options)
diff --git a/frappe/app.py b/frappe/app.py
index 6f5023be93..920628dda4 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -16,9 +16,9 @@ import frappe.handler
import frappe.auth
import frappe.api
import frappe.utils.response
-import frappe.website.render
from frappe.utils import get_site_name, sanitize_html
from frappe.middlewares import StaticDataMiddleware
+from frappe.website.serve import get_response
from frappe.utils.error import make_error_snapshot
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe import _
@@ -72,7 +72,7 @@ def application(request):
response = frappe.utils.response.download_private_file(request.path)
elif request.method in ('GET', 'HEAD', 'POST'):
- response = frappe.website.render.render()
+ response = get_response()
else:
raise NotFound
@@ -266,8 +266,7 @@ def handle_exception(e):
make_error_snapshot(e)
if return_as_message:
- response = frappe.website.render.render("message",
- http_status_code=http_status_code)
+ response = get_response("message", http_status_code=http_status_code)
return response
diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py
index 998e73a42c..d2afda1553 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.py
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py
@@ -333,7 +333,7 @@ class AutoRepeat(Document):
if self.reference_doctype and self.reference_document:
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
- email_ids = list(set([d.email_id for d in res]))
+ email_ids = {d.email_id for d in res}
if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True)
else:
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 52fba4568d..9f09f26be8 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -53,7 +53,7 @@ def clear_domain_cache(user=None):
cache.delete_value(domain_cache_keys)
def clear_global_cache():
- from frappe.website.render import clear_cache as clear_website_cache
+ from frappe.website.utils import clear_website_cache
clear_doctype_cache()
clear_website_cache()
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index 8ef70d739c..c16de497ec 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -69,14 +69,14 @@ def watch(apps=None):
def clear_cache(context):
"Clear cache, doctype cache and defaults"
import frappe.sessions
- import frappe.website.render
+ from frappe.website.utils import clear_website_cache
from frappe.desk.notifications import clear_notifications
for site in context.sites:
try:
frappe.connect(site)
frappe.clear_cache()
clear_notifications()
- frappe.website.render.clear_cache()
+ clear_website_cache()
finally:
frappe.destroy()
if not context.sites:
@@ -86,12 +86,12 @@ def clear_cache(context):
@pass_context
def clear_website_cache(context):
"Clear website cache"
- import frappe.website.render
+ from frappe.website.utils import clear_website_cache
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
- frappe.website.render.clear_cache()
+ clear_website_cache()
finally:
frappe.destroy()
if not context.sites:
diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py
index f21819ad98..77305168c1 100644
--- a/frappe/contacts/address_and_contact.py
+++ b/frappe/contacts/address_and_contact.py
@@ -153,7 +153,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil
doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"],
distinct=True, as_list=True)
- doctypes = tuple([d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)])
+ doctypes = tuple(d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE))
filters.update({
"dt": ("not in", [d[0] for d in doctypes])
diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py
index bfcf91427d..755bc63064 100644
--- a/frappe/contacts/doctype/address/address.py
+++ b/frappe/contacts/doctype/address/address.py
@@ -257,7 +257,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
def get_condensed_address(doc):
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"]
- return ", ".join([doc.get(d) for d in fields if doc.get(d)])
+ return ", ".join(doc.get(d) for d in fields if doc.get(d))
def update_preferred_address(address, field):
frappe.db.set_value('Address', address, field, 0)
diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py
index e29bae25a2..2706ab1c30 100644
--- a/frappe/core/doctype/comment/comment.py
+++ b/frappe/core/doctype/comment/comment.py
@@ -9,7 +9,7 @@ from frappe.core.doctype.user.user import extract_mentions
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\
get_title, get_title_html
from frappe.utils import get_fullname
-from frappe.website.render import clear_cache
+from frappe.website.utils import clear_cache
from frappe.database.schema import add_column
from frappe.exceptions import ImplicitCommitError
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index fed90b75ce..bb922f1f5d 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -449,7 +449,7 @@ class ImportFile:
for row in data_without_first_row:
row_values = row.get_values(parent_column_indexes)
# if the row is blank, it's a child row doc
- if all([v in INVALID_VALUES for v in row_values]):
+ if all(v in INVALID_VALUES for v in row_values):
rows.append(row)
continue
# if we encounter a row which has values in parent columns,
@@ -606,7 +606,7 @@ class Row:
if df.fieldtype == "Select":
select_options = get_select_options(df)
if select_options and value not in select_options:
- options_string = ", ".join([frappe.bold(d) for d in select_options])
+ options_string = ", ".join(frappe.bold(d) for d in select_options)
msg = _("Value must be one of {0}").format(options_string)
self.warnings.append(
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
@@ -902,7 +902,7 @@ class Column:
if self.df.fieldtype == "Link":
# find all values that dont exist
- values = list(set([cstr(v) for v in self.column_values[1:] if v]))
+ values = list({cstr(v) for v in self.column_values[1:] if v})
exists = [
d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})
]
@@ -935,11 +935,11 @@ class Column:
elif self.df.fieldtype == "Select":
options = get_select_options(self.df)
if options:
- values = list(set([cstr(v) for v in self.column_values[1:] if v]))
- invalid = list(set(values) - set(options))
+ values = {cstr(v) for v in self.column_values[1:] if v}
+ invalid = values - set(options)
if invalid:
- valid_values = ", ".join([frappe.bold(o) for o in options])
- invalid_values = ", ".join([frappe.bold(i) for i in invalid])
+ valid_values = ", ".join(frappe.bold(o) for o in options)
+ invalid_values = ", ".join(frappe.bold(i) for i in invalid)
self.warnings.append(
{
"col": self.column_number,
diff --git a/frappe/core/doctype/data_import_legacy/importer.py b/frappe/core/doctype/data_import_legacy/importer.py
index 4080e70418..ceefff4410 100644
--- a/frappe/core/doctype/data_import_legacy/importer.py
+++ b/frappe/core/doctype/data_import_legacy/importer.py
@@ -177,7 +177,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
if d.get("name") and d["name"].startswith('"'):
d["name"] = d["name"][1:-1]
- if sum([0 if not val else 1 for val in d.values()]):
+ if sum(0 if not val else 1 for val in d.values()):
d['doctype'] = dt
if dt == doctype:
doc.update(d)
@@ -533,6 +533,6 @@ def get_parent_field(doctype, parenttype):
def delete_child_rows(rows, doctype):
"""delete child rows for all parents"""
- for p in list(set([r[1] for r in rows])):
+ for p in list(set(r[1] for r in rows)):
if p:
frappe.db.sql("""delete from `tab{0}` where parent=%s""".format(doctype), p)
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 8a96fc89f6..3cdc45ea08 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -8,7 +8,6 @@ from frappe.cache_manager import clear_user_cache, clear_controller_cache
# imports - module imports
import frappe
-import frappe.website.render
from frappe import _
from frappe.utils import now, cint
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options
@@ -23,6 +22,7 @@ from frappe.model.docfield import supports_translation
from frappe.modules.import_file import get_file_path
from frappe.model.meta import Meta
from frappe.desk.utils import validate_route_conflict
+from frappe.website.utils import clear_cache
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
@@ -193,7 +193,7 @@ class DocType(Document):
self.flags.update_fields_to_fetch_queries = []
- if set(old_fields_to_fetch) != set([df.fieldname for df in new_meta.get_fields_to_fetch()]):
+ if set(old_fields_to_fetch) != set(df.fieldname for df in new_meta.get_fields_to_fetch()):
for df in new_meta.get_fields_to_fetch():
if df.fieldname not in old_fields_to_fetch:
link_fieldname, source_fieldname = df.fetch_from.split('.', 1)
@@ -248,7 +248,7 @@ class DocType(Document):
frappe.throw(_('Field "route" is mandatory for Web Views'), title='Missing Field')
# clear website cache
- frappe.website.render.clear_cache()
+ clear_cache()
def change_modified_of_parent(self):
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
@@ -550,11 +550,6 @@ class DocType(Document):
from frappe.modules.export_file import export_to_files
export_to_files(record_list=[['DocType', self.name]], create_init=True)
- def import_doc(self):
- """Import from standard folder `[module]/doctype/[name]/[name].json`."""
- from frappe.modules.import_module import import_from_files
- import_from_files(record_list=[[self.module, 'doctype', self.name]])
-
def make_controller_template(self):
"""Make boilerplate controller template."""
make_boilerplate("controller._py", self)
@@ -762,7 +757,7 @@ def validate_fields(meta):
invalid_fields = ('doctype',)
if fieldname in invalid_fields:
frappe.throw(_("{0}: Fieldname cannot be one of {1}")
- .format(docname, ", ".join([frappe.bold(d) for d in invalid_fields])))
+ .format(docname, ", ".join(frappe.bold(d) for d in invalid_fields)))
def check_unique_fieldname(docname, fieldname):
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
@@ -996,7 +991,7 @@ def validate_fields(meta):
if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label))
text_str = _("{0} is an invalid Data field.").format(df_str) + "
" * 2 + _("Only Options allowed for Data field are:") + "
"
- df_options_str = "
- " + "
- ".join([_(x) for x in data_field_options]) + "
"
+ df_options_str = "- " + "
- ".join(_(x) for x in data_field_options) + "
"
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py
index 681824bb02..bbd20f3b70 100644
--- a/frappe/core/doctype/domain/domain.py
+++ b/frappe/core/doctype/domain/domain.py
@@ -110,7 +110,7 @@ class Domain(Document):
# enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=1
- where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.allow_sidebar_items])))
+ where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.allow_sidebar_items)))
if self.data.remove_sidebar_items:
# disable all
@@ -118,4 +118,4 @@ class Domain(Document):
# enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=0
- where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.remove_sidebar_items])))
+ where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.remove_sidebar_items)))
diff --git a/frappe/core/doctype/feedback/__init__.py b/frappe/core/doctype/feedback/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/feedback/feedback.js b/frappe/core/doctype/feedback/feedback.js
new file mode 100644
index 0000000000..131f0e19d8
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Feedback', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/feedback/feedback.json b/frappe/core/doctype/feedback/feedback.json
new file mode 100644
index 0000000000..cf8a180e27
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.json
@@ -0,0 +1,86 @@
+{
+ "actions": [],
+ "creation": "2021-06-03 19:02:55.328423",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_doctype",
+ "reference_name",
+ "column_break_3",
+ "email",
+ "rating",
+ "section_break_6",
+ "feedback"
+ ],
+ "fields": [
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "email",
+ "fieldtype": "Data",
+ "label": "Email",
+ "reqd": 1
+ },
+ {
+ "fieldname": "rating",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Rating",
+ "precision": "1",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "feedback",
+ "fieldtype": "Small Text",
+ "label": "Feedback",
+ "reqd": 1
+ },
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Reference Document Type",
+ "options": "\nBlog Post"
+ },
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "options": "reference_doctype",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-06-14 15:11:26.005805",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Feedback",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "reference_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/feedback/feedback.py b/frappe/core/doctype/feedback/feedback.py
new file mode 100644
index 0000000000..655bed6eb1
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class Feedback(Document):
+ pass
diff --git a/frappe/core/doctype/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py
new file mode 100644
index 0000000000..702f9d8ac1
--- /dev/null
+++ b/frappe/core/doctype/feedback/test_feedback.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+import frappe
+import unittest
+
+class TestFeedback(unittest.TestCase):
+ def test_feedback_creation_updation(self):
+ from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
+ test_blog = make_test_blog()
+
+ frappe.db.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'")
+
+ from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback
+ feedback = add_feedback('Blog Post', test_blog.name, 5, 'New feedback','test@test.com')
+
+ self.assertEqual(feedback.feedback, 'New feedback')
+ self.assertEqual(feedback.rating, 5)
+
+ updated_feedback = update_feedback('Blog Post', test_blog.name, 6, 'Updated feedback', 'test@test.com')
+
+ self.assertEqual(updated_feedback.feedback, 'Updated feedback')
+ self.assertEqual(updated_feedback.rating, 6)
+
+ frappe.db.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'")
+
+ test_blog.delete()
\ No newline at end of file
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 3fa31cbf80..5b605504e8 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -13,7 +13,7 @@ from frappe.utils.password import update_password as _update_password, check_pas
from frappe.desk.notifications import clear_notifications
from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, toggle_notifications
from frappe.utils.user import get_system_managers
-from frappe.website.utils import is_signup_enabled
+from frappe.website.utils import is_signup_disabled
from frappe.rate_limiter import rate_limit
from frappe.utils.background_jobs import enqueue
from frappe.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype
@@ -839,7 +839,7 @@ def verify_password(password):
@frappe.whitelist(allow_guest=True)
def sign_up(email, full_name, redirect_to):
- if not is_signup_enabled():
+ if is_signup_disabled():
frappe.throw(_('Sign Up is disabled'), title='Not Allowed')
user = frappe.db.get("User", {"email": email})
@@ -931,7 +931,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
LIMIT %(page_len)s OFFSET %(start)s
""".format(
user_type_condition = user_type_condition,
- standard_users=", ".join([frappe.db.escape(u) for u in STANDARD_USERS]),
+ standard_users=", ".join(frappe.db.escape(u) for u in STANDARD_USERS),
key=searchfield,
fcond=get_filters_cond(doctype, filters, conditions),
mcond=get_match_cond(doctype)
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index 42ca4d7a14..4aa5797c7f 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -16,11 +16,11 @@ class UserPermission(Document):
self.validate_default_permission()
def on_update(self):
- frappe.cache().delete_value('user_permissions')
+ frappe.cache().hdel('user_permissions', self.user)
frappe.publish_realtime('update_user_permissions')
def on_trash(self): # pylint: disable=no-self-use
- frappe.cache().delete_value('user_permissions')
+ frappe.cache().hdel('user_permissions', self.user)
frappe.publish_realtime('update_user_permissions')
def validate_user_permission(self):
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index e7d06c45f2..82ffb090f1 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -112,7 +112,7 @@ class UserType(Document):
self.select_doctypes = []
select_doctypes = []
- user_doctypes = tuple([row.document_type for row in self.user_doctypes])
+ user_doctypes = [row.document_type for row in self.user_doctypes]
for doctype in user_doctypes:
doc = frappe.get_meta(doctype)
@@ -265,4 +265,4 @@ def apply_permissions_for_non_standard_user_type(doc, method=None):
user_doc.update_children()
add_user_permission(doc.doctype, doc.name, doc.get(data[1]))
else:
- frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1]))
\ No newline at end of file
+ frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1]))
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 8bcc6cf059..1b8977acc4 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -355,9 +355,9 @@ class CustomizeForm(Document):
def delete_custom_fields(self):
meta = frappe.get_meta(self.doc_type)
- fields_to_remove = (set([df.fieldname for df in meta.get("fields")])
- - set(df.fieldname for df in self.get("fields")))
-
+ fields_to_remove = (
+ {df.fieldname for df in meta.get("fields")} - {df.fieldname for df in self.get("fields")}
+ )
for fieldname in fields_to_remove:
df = meta.get("fields", {"fieldname": fieldname})[0]
if df.get("is_custom_field"):
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 7e8d2da43b..81e24cc7ad 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -335,7 +335,7 @@ class Database(object):
values[key] = value[1]
if isinstance(value[1], (tuple, list)):
# value is a list in tuple ("in", ("A", "B"))
- _rhs = " ({0})".format(", ".join([self.escape(v) for v in value[1]]))
+ _rhs = " ({0})".format(", ".join(self.escape(v) for v in value[1]))
del values[key]
if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]:
@@ -1010,7 +1010,7 @@ class Database(object):
:params values: list of list of values
"""
insert_list = []
- fields = ", ".join(["`"+field+"`" for field in fields])
+ fields = ", ".join("`"+field+"`" for field in fields)
for idx, value in enumerate(values):
insert_list.append(tuple(value))
diff --git a/frappe/desk/doctype/form_tour/__init__.py b/frappe/desk/doctype/form_tour/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
new file mode 100644
index 0000000000..94c6806b50
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -0,0 +1,24 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Form Tour', {
+ setup: function(frm) {
+ frm.set_query("reference_doctype", function() {
+ return {
+ filters: {
+ istable: 0
+ }
+ };
+ });
+
+ frm.set_query("field", "steps", function() {
+ return {
+ query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
+ filters: {
+ doctype: frm.doc.reference_doctype,
+ hidden: 0
+ }
+ };
+ });
+ }
+});
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
new file mode 100644
index 0000000000..8e09a5d63a
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -0,0 +1,75 @@
+{
+ "actions": [],
+ "autoname": "field:title",
+ "creation": "2021-05-21 23:02:52.242721",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "reference_doctype",
+ "completed",
+ "section_break_3",
+ "steps"
+ ],
+ "fields": [
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Reference Document",
+ "options": "DocType",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "steps",
+ "fieldtype": "Table",
+ "label": "Steps",
+ "options": "Form Tour Step",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.__islocal != 1",
+ "fieldname": "completed",
+ "fieldtype": "Check",
+ "label": "Mark as Completed"
+ },
+ {
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 1,
+ "unique": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-05-26 19:36:59.093753",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Form Tour",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
new file mode 100644
index 0000000000..dd762395c4
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+
+class FormTour(Document):
+ pass
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_docfield_list(doctype, txt, searchfield, start, page_len, filters):
+ or_filters = [
+ ['fieldname', 'like', '%' + txt + '%'],
+ ['label', 'like', '%' + txt + '%'],
+ ['fieldtype', 'like', '%' + txt + '%']
+ ]
+
+ parent_doctype = filters.pop('doctype')
+ excluded_fieldtypes = ['Column Break']
+ excluded_fieldtypes += filters.get('excluded_fieldtypes', [])
+
+ docfields = frappe.get_all(
+ doctype,
+ fields=["name as value", "label", "fieldtype"],
+ filters={'parent': parent_doctype, 'fieldtype': ['not in', excluded_fieldtypes]},
+ or_filters=or_filters,
+ limit_start=start,
+ limit_page_length=page_len,
+ as_list=1,
+ )
+ return docfields
diff --git a/frappe/desk/doctype/form_tour/test_form_tour.py b/frappe/desk/doctype/form_tour/test_form_tour.py
new file mode 100644
index 0000000000..a4a796ce41
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/test_form_tour.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestFormTour(unittest.TestCase):
+ pass
diff --git a/frappe/desk/doctype/form_tour_step/__init__.py b/frappe/desk/doctype/form_tour_step/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json
new file mode 100644
index 0000000000..a772a2498a
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json
@@ -0,0 +1,85 @@
+{
+ "actions": [],
+ "creation": "2021-05-21 23:05:45.342114",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "field",
+ "title",
+ "description",
+ "column_break_2",
+ "position",
+ "fieldname",
+ "label",
+ "condition"
+ ],
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "columns": 4,
+ "fieldname": "description",
+ "fieldtype": "HTML Editor",
+ "in_list_view": 1,
+ "label": "Description",
+ "reqd": 1
+ },
+ {
+ "fieldname": "field",
+ "fieldtype": "Link",
+ "label": "Field",
+ "options": "DocField",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "field.fieldname",
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Fieldname",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "field.label",
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Bottom",
+ "fieldname": "position",
+ "fieldtype": "Select",
+ "label": "Position",
+ "options": "Left\nLeft Center\nLeft Bottom\nTop\nTop Center\nTop Right\nRight\nRight Center\nRight Bottom\nBottom\nBottom Center\nBottom Right\nMid Center"
+ },
+ {
+ "fieldname": "next_step_condition",
+ "fieldtype": "Code",
+ "label": "Next Step Condition",
+ "options": "JS"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-05-26 19:44:48.737453",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Form Tour Step",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.py b/frappe/desk/doctype/form_tour_step/form_tour_step.py
new file mode 100644
index 0000000000..0df5665c63
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class FormTourStep(Document):
+ pass
diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py
index 28a1ed8239..9112349c1b 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.py
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py
@@ -21,7 +21,7 @@ class GlobalSearchSettings(Document):
dts.append(dt.document_type)
if core_dts:
- core_dts = (", ".join([frappe.bold(dt) for dt in core_dts]))
+ core_dts = ", ".join(frappe.bold(dt) for dt in core_dts)
frappe.throw(_("Core Modules {0} cannot be searched in Global Search.").format(core_dts))
if repeated_dts:
@@ -60,7 +60,7 @@ def update_global_search_doctypes():
if search_doctypes.get(domain):
global_search_doctypes.extend(search_doctypes.get(domain))
- doctype_list = set([dt.name for dt in frappe.get_all("DocType")])
+ doctype_list = {dt.name for dt in frappe.get_all("DocType")}
allowed_in_global_search = []
for dt in global_search_doctypes:
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index 3c67bb4668..4ea5c9cd7e 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -131,7 +131,7 @@ def update_tags(doc, tags):
:param doc: Document to be added to global tags
"""
- new_tags = list(set([tag.strip() for tag in tags.split(",") if tag]))
+ new_tags = {tag.strip() for tag in tags.split(",") if tag}
for tag in new_tags:
if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}):
@@ -186,4 +186,4 @@ def get_documents_for_tag(tag):
@frappe.whitelist()
def get_tags_list_for_awesomebar():
- return [t.name for t in frappe.get_list("Tag")]
\ No newline at end of file
+ return [t.name for t in frappe.get_list("Tag")]
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 0329e0f7d2..0b5babc8d9 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -55,8 +55,7 @@ class Workspace(Document):
for link in self.links:
link = link.as_dict()
if link.type == "Card Break":
-
- if card_links:
+ if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')):
current_card['links'] = card_links
cards.append(current_card)
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 7ca4e6f99c..ecd59f42bb 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -1,32 +1,25 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-import frappe
+import email.utils
+import functools
import imaplib
-import re
-import json
import socket
import time
-import functools
-
-import email.utils
-
-from frappe import _, are_emails_muted
-from frappe.model.document import Document
-from frappe.utils import (validate_email_address, cint, cstr, get_datetime,
- DATE_FORMAT, strip, comma_or, sanitize_html, add_days, parse_addr)
-from frappe.utils.user import is_system_user
-from frappe.utils.jinja import render_template
-from frappe.email.smtp import SMTPServer
-from frappe.email.receive import EmailServer, InboundMail, SentEmailInInboxError
-from poplib import error_proto
-from dateutil.relativedelta import relativedelta
from datetime import datetime, timedelta
+from poplib import error_proto
+
+import frappe
+from frappe import _, are_emails_muted, safe_encode
from frappe.desk.form import assign_to
-from frappe.utils.user import get_system_managers
-from frappe.utils.background_jobs import enqueue, get_jobs
-from frappe.utils.html_utils import clean_email_html
-from frappe.utils.error import raise_error_on_no_output
+from frappe.email.receive import EmailServer, InboundMail, SentEmailInInboxError
+from frappe.email.smtp import SMTPServer
from frappe.email.utils import get_port
+from frappe.model.document import Document
+from frappe.utils import cint, comma_or, cstr, parse_addr, validate_email_address
+from frappe.utils.background_jobs import enqueue, get_jobs
+from frappe.utils.error import raise_error_on_no_output
+from frappe.utils.jinja import render_template
+from frappe.utils.user import get_system_managers
OUTGOING_EMAIL_ACCOUNT_MISSING = _("Please setup default Email Account from Setup > Email > Email Account")
@@ -577,8 +570,8 @@ class EmailAccount(Document):
email_server.update_flag(uid_list=uid_list)
# mark communication as read
- docnames = ",".join([ "'%s'"%flag.get("communication") for flag in flags \
- if flag.get("action") == "Read" ])
+ docnames = ",".join("'%s'"%flag.get("communication") for flag in flags \
+ if flag.get("action") == "Read")
self.set_communication_seen_status(docnames, seen=1)
# mark communication as unread
@@ -608,7 +601,6 @@ class EmailAccount(Document):
def append_email_to_sent_folder(self, message):
-
email_server = None
try:
email_server = self.get_incoming_server(in_receive=True)
@@ -622,7 +614,8 @@ class EmailAccount(Document):
if email_server.imap:
try:
- email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message.encode())
+ message = safe_encode(message)
+ email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
except Exception:
frappe.log_error()
diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json
index c49de841e6..cb74249143 100644
--- a/frappe/email/doctype/email_group/email_group.json
+++ b/frappe/email/doctype/email_group/email_group.json
@@ -1,6 +1,7 @@
{
"actions": [],
"allow_import": 1,
+ "allow_rename": 1,
"autoname": "field:title",
"creation": "2015-03-18 06:08:32.729800",
"doctype": "DocType",
@@ -50,7 +51,7 @@
"link_fieldname": "email_group"
}
],
- "modified": "2020-09-24 16:41:55.286377",
+ "modified": "2021-06-15 11:25:13.556201",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Group",
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index dad473b8aa..e1e332f978 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -179,7 +179,14 @@ class SendMailContext:
else:
email_status = self.is_mail_sent_to_all() and 'Sent'
email_status = email_status or (self.sent_to and 'Partially Sent') or 'Not Sent'
- self.queue_doc.update_status(status = email_status, commit = True)
+
+ update_fields = {'status': email_status}
+ if self.email_account_doc.is_exists_in_db():
+ update_fields['email_account'] = self.email_account_doc.name
+ else:
+ update_fields['email_account'] = None
+
+ self.queue_doc.update_status(**update_fields, commit = True)
def log_exception(self, exc_type, exc_val, exc_tb):
if exc_type:
diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py
index cfd0df53a9..3abd339ed9 100644
--- a/frappe/email/doctype/newsletter/test_newsletter.py
+++ b/frappe/email/doctype/newsletter/test_newsletter.py
@@ -42,7 +42,7 @@ class TestNewsletter(unittest.TestCase):
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")]
self.assertEqual(len(email_queue_list), 4)
- recipients = set([e.recipients[0].recipient for e in email_queue_list])
+ recipients = {e.recipients[0].recipient for e in email_queue_list}
self.assertTrue(set(emails).issubset(recipients))
def test_unsubscribe(self):
diff --git a/frappe/email/inbox.py b/frappe/email/inbox.py
index 5f8f516772..c6020e14e4 100644
--- a/frappe/email/inbox.py
+++ b/frappe/email/inbox.py
@@ -18,7 +18,7 @@ def get_email_accounts(user=None):
"all_accounts": ""
}
- all_accounts = ",".join([ account.get("email_account") for account in accounts ])
+ all_accounts = ",".join(account.get("email_account") for account in accounts)
if len(accounts) > 1:
email_accounts.append({
"email_account": all_accounts,
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index ca96981aa8..885a306cfb 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -6,15 +6,61 @@ from frappe import msgprint, _
from frappe.utils.verified_command import get_signed_params, verify_request
from frappe.utils import get_url, now_datetime, cint
-def get_emails_sent_this_month():
- return frappe.db.sql("""
- SELECT COUNT(*) FROM `tabEmail Queue`
- WHERE `status`='Sent' AND EXTRACT(YEAR_MONTH FROM `creation`) = EXTRACT(YEAR_MONTH FROM NOW())
- """)[0][0]
+def get_emails_sent_this_month(email_account=None):
+ """Get count of emails sent from a specific email account.
-def get_emails_sent_today():
- return frappe.db.sql("""SELECT COUNT(`name`) FROM `tabEmail Queue` WHERE
- `status` in ('Sent', 'Not Sent', 'Sending') AND `creation` > (NOW() - INTERVAL '24' HOUR)""")[0][0]
+ :param email_account: name of the email account used to send mail
+
+ if email_account=None, email account filter is not applied while counting
+ """
+ q = """
+ SELECT
+ COUNT(*)
+ FROM
+ `tabEmail Queue`
+ WHERE
+ `status`='Sent'
+ AND
+ EXTRACT(YEAR_MONTH FROM `creation`) = EXTRACT(YEAR_MONTH FROM NOW())
+ """
+
+ q_args = {}
+ if email_account is not None:
+ if email_account:
+ q += " AND email_account = %(email_account)s"
+ q_args['email_account'] = email_account
+ else:
+ q += " AND (email_account is null OR email_account='')"
+
+ return frappe.db.sql(q, q_args)[0][0]
+
+def get_emails_sent_today(email_account=None):
+ """Get count of emails sent from a specific email account.
+
+ :param email_account: name of the email account used to send mail
+
+ if email_account=None, email account filter is not applied while counting
+ """
+ q = """
+ SELECT
+ COUNT(`name`)
+ FROM
+ `tabEmail Queue`
+ WHERE
+ `status` in ('Sent', 'Not Sent', 'Sending')
+ AND
+ `creation` > (NOW() - INTERVAL '24' HOUR)
+ """
+
+ q_args = {}
+ if email_account is not None:
+ if email_account:
+ q += " AND email_account = %(email_account)s"
+ q_args['email_account'] = email_account
+ else:
+ q += " AND (email_account is null OR email_account='')"
+
+ return frappe.db.sql(q, q_args)[0][0]
def get_unsubscribe_message(unsubscribe_message, expose_recipients):
if unsubscribe_message:
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index 9ad560aa4a..2e42008951 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -802,7 +802,7 @@ class InboundMail(Email):
except frappe.DuplicateEntryError:
# try and find matching parent
parent_name = frappe.db.get_value(self.email_account.append_to,
- {email_fileds.sender_field: email.from_email}
+ {email_fileds.sender_field: self.from_email}
)
if parent_name:
parent.name = parent_name
diff --git a/frappe/installer.py b/frappe/installer.py
index d7d885d60e..d4d8117fcb 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -282,10 +282,10 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
def post_install(rebuild_website=False):
- from frappe.website import render
+ from frappe.website.utils import clear_website_cache
if rebuild_website:
- render.clear_cache()
+ clear_website_cache()
init_singles()
frappe.db.commit()
@@ -537,7 +537,7 @@ def is_downgrade(sql_file_path, verbose=False):
def is_partial(sql_file_path):
with open(sql_file_path) as f:
- header = " ".join([f.readline() for _ in range(5)])
+ header = " ".join(f.readline() for _ in range(5))
if "Partial Backup" in header:
return True
return False
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
index 122096cf6f..acc8b96679 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
@@ -79,7 +79,7 @@ class LDAPSettings(Document):
def sync_roles(self, user, additional_groups=None):
- current_roles = set([d.role for d in user.get("roles")])
+ current_roles = set(d.role for d in user.get("roles"))
needed_roles = set()
needed_roles.add(self.default_role)
diff --git a/frappe/migrate.py b/frappe/migrate.py
index c984371927..d4060e6067 100644
--- a/frappe/migrate.py
+++ b/frappe/migrate.py
@@ -16,7 +16,7 @@ from frappe.utils.dashboard import sync_dashboards
from frappe.utils.background_jobs import enqueue
from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
-from frappe.website import render
+from frappe.website.utils import clear_website_cache
from frappe.core.doctype.language.language import sync_languages
from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
@@ -79,7 +79,7 @@ Otherwise, check the server logs and ensure that all the required services are r
frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()
# syncs statics
- render.clear_cache()
+ clear_website_cache()
# updating installed applications data
frappe.get_single('Installed Applications').update_versions()
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index dd93fbcc18..75122f5aba 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -165,7 +165,7 @@ def delete_fields(args_dict, delete=0):
frappe.db.sql("""
DELETE FROM `tabSingles`
WHERE doctype='%s' AND field IN (%s)
- """ % (dt, ", ".join(["'{}'".format(f) for f in fields])))
+ """ % (dt, ", ".join("'{}'".format(f) for f in fields)))
else:
existing_fields = frappe.db.multisql({
"mariadb": "DESC `tab%s`" % dt,
@@ -188,7 +188,7 @@ def delete_fields(args_dict, delete=0):
frappe.db.commit()
query = "ALTER TABLE `tab%s` " % dt + \
- ", ".join(["DROP COLUMN `%s`" % f for f in fields_need_to_delete])
+ ", ".join("DROP COLUMN `%s`" % f for f in fields_need_to_delete)
frappe.db.sql(query)
if frappe.db.db_type == 'postgres':
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 2f5154cfd9..af696e116d 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -354,7 +354,7 @@ class BaseDocument(object):
frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns})
VALUES ({values})""".format(
doctype = self.doctype,
- columns = ", ".join(["`"+c+"`" for c in columns]),
+ columns = ", ".join("`"+c+"`" for c in columns),
values = ", ".join(["%s"] * len(columns))
), list(d.values()))
except Exception as e:
@@ -397,7 +397,7 @@ class BaseDocument(object):
frappe.db.sql("""UPDATE `tab{doctype}`
SET {values} WHERE `name`=%s""".format(
doctype = self.doctype,
- values = ", ".join(["`"+c+"`=%s" for c in columns])
+ values = ", ".join("`"+c+"`=%s" for c in columns)
), list(d.values()) + [name])
except Exception as e:
if frappe.db.is_unique_key_violation(e):
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 1acccdc142..7ed681644f 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -43,8 +43,14 @@ class DatabaseQuery(object):
# filters and fields swappable
# its hard to remember what comes first
- if (isinstance(fields, dict)
- or (isinstance(fields, list) and fields and isinstance(fields[0], list))):
+ if (
+ isinstance(fields, dict)
+ or (
+ fields
+ and isinstance(fields, list)
+ and isinstance(fields[0], list)
+ )
+ ):
# if fields is given as dict/list of list, its probably filters
filters, fields = fields, filters
@@ -56,10 +62,7 @@ class DatabaseQuery(object):
if fields:
self.fields = fields
else:
- if pluck:
- self.fields = ["`tab{0}`.`{1}`".format(self.doctype, pluck)]
- else:
- self.fields = ["`tab{0}`.`name`".format(self.doctype)]
+ self.fields = [f"`tab{self.doctype}`.`{pluck or 'name'}`"]
if start: limit_start = start
if page_length: limit_page_length = page_length
@@ -70,7 +73,7 @@ class DatabaseQuery(object):
self.docstatus = docstatus or []
self.group_by = group_by
self.order_by = order_by
- self.limit_start = 0 if (limit_start is False) else cint(limit_start)
+ self.limit_start = cint(limit_start)
self.limit_page_length = cint(limit_page_length) if limit_page_length else None
self.with_childnames = with_childnames
self.debug = debug
@@ -157,11 +160,10 @@ class DatabaseQuery(object):
# left join parent, child tables
for child in self.tables[1:]:
- args.tables += " {join} {child} on ({child}.parent = {main}.name)".format(join=self.join,
- child=child, main=self.tables[0])
+ args.tables += f" {self.join} {child} on ({child}.parent = {self.tables[0]}.name)"
if self.grouped_or_conditions:
- self.conditions.append("({0})".format(" or ".join(self.grouped_or_conditions)))
+ self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})")
args.conditions = ' and '.join(self.conditions)
@@ -186,9 +188,9 @@ class DatabaseQuery(object):
fields.append(field)
elif "as" in field.lower().split(" "):
col, _, new = field.split()
- fields.append("`{0}` as {1}".format(col, new))
+ fields.append(f"`{col}` as {new}")
else:
- fields.append("`{0}`".format(field))
+ fields.append(f"`{field}`")
args.fields = ", ".join(fields)
@@ -260,10 +262,10 @@ class DatabaseQuery(object):
if any(keyword in field.lower().split() for keyword in blacklisted_keywords):
_raise_exception()
- if any("({0}".format(keyword) in field.lower() for keyword in blacklisted_keywords):
+ if any(f"({keyword}" in field.lower() for keyword in blacklisted_keywords):
_raise_exception()
- if any("{0}(".format(keyword) in field.lower() for keyword in blacklisted_functions):
+ if any(f"{keyword}(" in field.lower() for keyword in blacklisted_functions):
_raise_exception()
if '@' in field.lower():
@@ -287,22 +289,30 @@ class DatabaseQuery(object):
def extract_tables(self):
"""extract tables from fields"""
- self.tables = ['`tab' + self.doctype + '`']
-
+ self.tables = [f"`tab{self.doctype}`"]
+ sql_functions = [
+ "dayofyear(",
+ "extract(",
+ "locate(",
+ "strpos(",
+ "count(",
+ "sum(",
+ "avg(",
+ ]
# add tables from fields
if self.fields:
- for f in self.fields:
- if ( not ("tab" in f and "." in f) ) or ("locate(" in f) or ("strpos(" in f) or \
- ("count(" in f) or ("avg(" in f) or ("sum(" in f) or ("extract(" in f) or ("dayofyear(" in f):
+ for field in self.fields:
+ if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field):
continue
- table_name = f.split('.')[0]
+ table_name = field.split('.')[0]
+
if table_name.lower().startswith('group_concat('):
table_name = table_name[13:]
if table_name.lower().startswith('ifnull('):
table_name = table_name[7:]
if not table_name[0]=='`':
- table_name = '`' + table_name + '`'
+ table_name = f"`{table_name}`"
if not table_name in self.tables:
self.append_table(table_name)
@@ -311,8 +321,7 @@ class DatabaseQuery(object):
doctype = table_name[4:-1]
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
- if (not self.flags.ignore_permissions) and\
- (not frappe.has_permission(doctype, ptype=ptype)):
+ if not self.flags.ignore_permissions and not frappe.has_permission(doctype, ptype=ptype):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype))
raise frappe.PermissionError(doctype)
@@ -326,7 +335,7 @@ class DatabaseQuery(object):
if len(self.tables) > 1:
for idx, field in enumerate(self.fields):
if '.' not in field and not _in_standard_sql_methods(field):
- self.fields[idx] = '{0}.{1}'.format(self.tables[0], field)
+ self.fields[idx] = f"{self.tables[0]}.{field}"
def get_table_columns(self):
try:
@@ -375,7 +384,7 @@ class DatabaseQuery(object):
if not self.flags.ignore_permissions:
match_conditions = self.build_match_conditions()
if match_conditions:
- self.conditions.append("(" + match_conditions + ")")
+ self.conditions.append(f"({match_conditions})")
def build_filter_conditions(self, filters, conditions, ignore_permissions=None):
"""build conditions from user filters"""
@@ -407,8 +416,7 @@ class DatabaseQuery(object):
if 'ifnull(' in f.fieldname:
column_name = f.fieldname
else:
- column_name = '{tname}.{fname}'.format(tname=tname,
- fname=f.fieldname)
+ column_name = f"{tname}.{f.fieldname}"
can_be_null = True
@@ -450,7 +458,7 @@ class DatabaseQuery(object):
fallback = "''"
value = [frappe.db.escape((v.name or '').strip(), percent=False) for v in result]
if len(value):
- value = "({0})".format(", ".join(value))
+ value = f"({', '.join(value)})"
else:
value = "('')"
# changing operator to IN as the above code fetches all the parent / child values and convert into tuple
@@ -466,7 +474,7 @@ class DatabaseQuery(object):
fallback = "''"
value = [frappe.db.escape((v or '').strip(), percent=False) for v in values]
if len(value):
- value = "({0})".format(", ".join(value))
+ value = f"({', '.join(value)})"
else:
value = "('')"
else:
@@ -503,7 +511,7 @@ class DatabaseQuery(object):
can_be_null = True
if 'ifnull' not in column_name:
- column_name = 'ifnull({}, {})'.format(column_name, fallback)
+ column_name = f'ifnull({column_name}, {fallback})'
elif df and df.fieldtype=="Date":
value = frappe.db.format_date(f.value)
@@ -540,21 +548,19 @@ class DatabaseQuery(object):
# escape value
if isinstance(value, str) and not f.operator.lower() == 'between':
- value = "{0}".format(frappe.db.escape(value, percent=False))
+ value = f"{frappe.db.escape(value, percent=False)}"
- if (self.ignore_ifnull
+ if (
+ self.ignore_ifnull
or not can_be_null
or (f.value and f.operator.lower() in ('=', 'like'))
- or 'ifnull(' in column_name.lower()):
+ or 'ifnull(' in column_name.lower()
+ ):
if f.operator.lower() == 'like' and frappe.conf.get('db_type') == 'postgres':
f.operator = 'ilike'
- condition = '{column_name} {operator} {value}'.format(
- column_name=column_name, operator=f.operator,
- value=value)
+ condition = f'{column_name} {f.operator} {value}'
else:
- condition = 'ifnull({column_name}, {fallback}) {operator} {value}'.format(
- column_name=column_name, fallback=fallback, operator=f.operator,
- value=value)
+ condition = f'ifnull({column_name}, {fallback}) {f.operator} {value}'
return condition
@@ -572,10 +578,12 @@ class DatabaseQuery(object):
role_permissions = frappe.permissions.get_role_permissions(meta, user=self.user)
self.shared = frappe.share.get_shared(self.doctype, self.user)
- if (not meta.istable and
+ if (
+ not meta.istable and
not (role_permissions.get("select") or role_permissions.get("read")) and
not self.flags.ignore_permissions and
- not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)):
+ not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)
+ ):
only_if_shared = True
if not self.shared:
frappe.throw(_("No permission to read {0}").format(self.doctype), frappe.PermissionError)
@@ -585,8 +593,10 @@ class DatabaseQuery(object):
else:
#if has if_owner permission skip user perm check
if role_permissions.get("has_if_owner_enabled") and role_permissions.get("if_owner", {}):
- self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype,
- frappe.db.escape(self.user, percent=False)))
+ self.match_conditions.append(
+ f"`tab{self.doctype}`.`owner` = {frappe.db.escape(self.user, percent=False)}"
+ )
+
# add user permission only if role has read perm
elif role_permissions.get("read") or role_permissions.get("select"):
# get user permissions
@@ -605,8 +615,7 @@ class DatabaseQuery(object):
# share is an OR condition, if there is a role permission
if not only_if_shared and self.shared and conditions:
- conditions = "({conditions}) or ({shared_condition})".format(
- conditions=conditions, shared_condition=self.get_share_condition())
+ conditions = f"({conditions}) or ({self.get_share_condition()})"
return conditions
@@ -614,8 +623,7 @@ class DatabaseQuery(object):
return self.match_filters
def get_share_condition(self):
- return """`tab{0}`.name in ({1})""".format(self.doctype, ", ".join(["%s"] * len(self.shared))) % \
- tuple([frappe.db.escape(s, percent=False) for s in self.shared])
+ return f"`tab{self.doctype}`.name in ({', '.join(frappe.db.escape(s, percent=False) for s in self.shared)})"
def add_user_permissions(self, user_permissions):
meta = frappe.get_meta(self.doctype)
@@ -640,9 +648,7 @@ class DatabaseQuery(object):
if frappe.get_system_settings("apply_strict_user_permissions"):
condition = ""
else:
- empty_value_condition = "ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(
- doctype=self.doctype, fieldname=df.get('fieldname')
- )
+ empty_value_condition = f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''"
condition = empty_value_condition + " or "
for permission in user_permission_values:
@@ -650,9 +656,7 @@ class DatabaseQuery(object):
docs.append(permission.get('doc'))
# append docs based on user permission applicable on reference doctype
-
# this is useful when getting list of docs from a link field
-
# in this case parent doctype of the link
# will be the reference doctype
@@ -664,14 +668,9 @@ class DatabaseQuery(object):
docs.append(permission.get('doc'))
if docs:
- condition += "`tab{doctype}`.`{fieldname}` in ({values})".format(
- doctype=self.doctype,
- fieldname=df.get('fieldname'),
- values=", ".join(
- [(frappe.db.escape(doc, percent=False)) for doc in docs])
- )
-
- match_conditions.append("({condition})".format(condition=condition))
+ values = ", ".join(frappe.db.escape(doc, percent=False) for doc in docs)
+ condition += f"`tab{self.doctype}`.`{df.get('fieldname')}` in ({values})"
+ match_conditions.append(f"({condition})")
match_filters[df.get('options')] = docs
if match_conditions:
@@ -721,17 +720,17 @@ class DatabaseQuery(object):
# `idx desc, modified desc`
# will covert to
# `tabItem`.`idx` desc, `tabItem`.`modified` desc
- args.order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(self.doctype,
- f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
+ args.order_by = ', '.join(
+ f"`tab{self.doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" for f in meta.sort_field.split(',')
+ )
else:
sort_field = meta.sort_field or 'modified'
sort_order = (meta.sort_field and meta.sort_order) or 'desc'
-
- args.order_by = "`tab{0}`.`{1}` {2}".format(self.doctype, sort_field or "modified", sort_order or "desc")
+ args.order_by = f"`tab{self.doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}"
# draft docs always on top
if hasattr(meta, 'is_submittable') and meta.is_submittable:
- args.order_by = "`tab{0}`.docstatus asc, {1}".format(self.doctype, args.order_by)
+ args.order_by = f"`tab{self.doctype}`.docstatus asc, {args.order_by}"
def validate_order_by_and_group_by(self, parameters):
"""Check order by, group by so that atleast one column is selected and does not have subquery"""
@@ -802,17 +801,16 @@ def get_order_by(doctype, meta):
# `idx desc, modified desc`
# will covert to
# `tabItem`.`idx` desc, `tabItem`.`modified` desc
- order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(doctype,
- f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
+ order_by = ', '.join(f"`tab{doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" for f in meta.sort_field.split(','))
+
else:
sort_field = meta.sort_field or 'modified'
sort_order = (meta.sort_field and meta.sort_order) or 'desc'
-
- order_by = "`tab{0}`.`{1}` {2}".format(doctype, sort_field or "modified", sort_order or "desc")
+ order_by = f"`tab{doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}"
# draft docs always on top
if meta.is_submittable:
- order_by = "`tab{0}`.docstatus asc, {1}".format(doctype, order_by)
+ order_by = f"`tab{doctype}`.docstatus asc, {order_by}"
return order_by
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index b67c41c990..b212324208 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -664,7 +664,7 @@ def trim_tables(doctype=None):
and not f.startswith("_")]
if columns_to_remove:
print(doctype, "columns removed:", columns_to_remove)
- columns_to_remove = ", ".join(["drop `{0}`".format(c) for c in columns_to_remove])
+ columns_to_remove = ", ".join("drop `{0}`".format(c) for c in columns_to_remove)
query = """alter table `tab{doctype}` {columns}""".format(
doctype=doctype, columns=columns_to_remove)
frappe.db.sql_ddl(query)
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index fc5b3ca9fe..9b8ac2574d 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -141,7 +141,7 @@ def update_user_settings(old, new, link_fields):
if not link_fields: return
# find the user settings for the linked doctypes
- linked_doctypes = set([d.parent for d in link_fields if not d.issingle])
+ linked_doctypes = {d.parent for d in link_fields if not d.issingle}
user_settings_details = frappe.db.sql('''SELECT `user`, `doctype`, `data`
FROM `__UserSettings`
WHERE `data` like %s
diff --git a/frappe/permissions.py b/frappe/permissions.py
index c25a7c3947..07b4a2e68f 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -308,7 +308,7 @@ def has_controller_permissions(doc, ptype, user=None):
return None
def get_doctypes_with_read():
- return list(set([p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()]))
+ return list({p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()})
def get_valid_perms(doctype=None, user=None):
'''Get valid permissions for the current user from DocPerm and Custom DocPerm'''
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/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js
index 059b0f76f8..99e87c5f21 100644
--- a/frappe/public/js/frappe/form/controls/text_editor.js
+++ b/frappe/public/js/frappe/form/controls/text_editor.js
@@ -1,7 +1,10 @@
import Quill from 'quill';
import ImageResize from 'quill-image-resize';
+import MagicUrl from 'quill-magic-url';
+
Quill.register('modules/imageResize', ImageResize);
+Quill.register('modules/magicUrl', MagicUrl);
const CodeBlockContainer = Quill.import('formats/code-block-container');
CodeBlockContainer.tagName = 'PRE';
Quill.register(CodeBlockContainer, true);
@@ -148,7 +151,8 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
modules: {
toolbar: this.get_toolbar_options(),
table: true,
- imageResize: {}
+ imageResize: {},
+ magicUrl: true
},
theme: 'snow'
};
diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js
index c1c95d94cf..eb7a6edc5d 100644
--- a/frappe/public/js/frappe/form/dashboard.js
+++ b/frappe/public/js/frappe/form/dashboard.js
@@ -5,6 +5,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
constructor(opts) {
$.extend(this, opts);
this.setup_dashboard_sections();
+ this.set_open_count = frappe.utils.throttle(this.set_open_count, 500);
}
setup_dashboard_sections() {
diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js
index ab83ed2f71..115a62e098 100644
--- a/frappe/public/js/frappe/form/footer/form_timeline.js
+++ b/frappe/public/js/frappe/form/footer/form_timeline.js
@@ -190,6 +190,7 @@ class FormTimeline extends BaseTimeline {
}
doc.owner = doc.sender;
doc.user_full_name = doc.sender_full_name;
+ doc.content = frappe.dom.remove_script_and_style(doc.content);
let communication_content = $(frappe.render_template('timeline_message_box', { doc }));
if (allow_reply) {
this.setup_reply(communication_content, doc);
@@ -248,6 +249,7 @@ class FormTimeline extends BaseTimeline {
}
get_comment_timeline_content(doc) {
+ doc.content = frappe.dom.remove_script_and_style(doc.content);
const comment_content = $(frappe.render_template('timeline_message_box', { doc }));
this.setup_comment_actions(comment_content, doc);
return comment_content;
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 35ebf9274d..a24c6ab0d6 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -1607,7 +1607,9 @@ frappe.ui.form.Form = class FrappeForm {
}
show_tour(on_finish) {
- if (!Array.isArray(frappe.tour[this.doctype])) {
+ const tour_info = frappe.tour[this.doctype];
+
+ if (!Array.isArray(tour_info)) {
return;
}
@@ -1619,23 +1621,29 @@ frappe.ui.form.Form = class FrappeForm {
keyboardControl: true,
nextBtnText: 'Next',
prevBtnText: 'Previous',
- opacity: 0.25,
- onNext: () => {
- if (!driver.hasNextStep()) {
- on_finish && on_finish();
- }
- }
+ opacity: 0.25
});
this.layout.sections.forEach(section => section.collapse(false));
- let steps = frappe.tour[this.doctype].map(step => {
+ let steps = tour_info.map(step => {
let field = this.get_docfield(step.fieldname);
return {
element: `.frappe-control[data-fieldname='${step.fieldname}']`,
popover: {
title: step.title || field.label,
- description: step.description
+ description: step.description,
+ position: step.position || 'bottom'
+ },
+ onNext: () => {
+ const next_condition_satisfied = this.layout.evaluate_depends_on_value(step.next_step_condition || true);
+ if (!next_condition_satisfied) {
+ driver.preventMove();
+ }
+
+ if (!driver.hasNextStep()) {
+ on_finish && on_finish();
+ }
}
};
});
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 89c34ed80c..b9a838688d 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -221,9 +221,13 @@ frappe.form.formatters = {
Tag: function(value) {
var html = "";
$.each((value || "").split(","), function(i, v) {
- if(v) html+= ''+v +'';
+ if (v) html += `
+
+ ${v}
+ `;
});
return html;
},
@@ -310,6 +314,7 @@ frappe.form.get_formatter = function(fieldtype) {
frappe.format = function(value, df, options, doc) {
if(!df) df = {"fieldtype":"Data"};
+ if (df.fieldname == '_user_tags') df.fieldtype = 'Tag';
var fieldtype = df.fieldtype || "Data";
// format Dynamic Link as a Link
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index a77791d0a2..ebc3fa19f5 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -210,9 +210,9 @@ export default class Grid {
delete_all_rows() {
frappe.confirm(__("Are you sure you want to delete all rows?"), () => {
- this.frm.doc[this.df.fieldname] = [];
- $(this.parent).find('.rows').empty();
- this.grid_rows = [];
+ this.grid_rows.forEach(row => {
+ row.remove();
+ });
this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0);
@@ -236,6 +236,10 @@ export default class Grid {
}
refresh_remove_rows_button() {
+ if (this.df.cannot_delete_rows) {
+ return;
+ }
+
this.remove_rows_button.toggleClass('hidden',
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
this.remove_all_rows_button.toggleClass('hidden',
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index 33db00dede..8c51314066 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -569,6 +569,9 @@ export default class GridRow {
.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row, .grid-append-row')
.toggle(!cannot_add_rows);
+ this.wrapper.find('.grid-delete-row')
+ .toggle(!(this.grid.df && this.grid.df.cannot_delete_rows));
+
frappe.dom.freeze("", "dark");
if (cur_frm) cur_frm.cur_grid = this;
this.wrapper.addClass("grid-row-open");
diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js
index 611ab024bf..72312d7f13 100644
--- a/frappe/public/js/frappe/ui/filters/filter_list.js
+++ b/frappe/public/js/frappe/ui/filters/filter_list.js
@@ -323,9 +323,12 @@ frappe.ui.FilterGroup = class {
}
add_filters_to_filter_group(filters) {
- filters.forEach((filter) => {
- this.add_filter(filter[0], filter[1], filter[2], filter[3]);
- });
+ if (filters.length) {
+ this.toggle_empty_filters(false);
+ filters.forEach((filter) => {
+ this.add_filter(filter[0], filter[1], filter[2], filter[3]);
+ });
+ }
}
add(filters, refresh = true) {
diff --git a/frappe/public/js/frappe/ui/group_by/group_by.js b/frappe/public/js/frappe/ui/group_by/group_by.js
index 3ebf9c9d3d..692d675c62 100644
--- a/frappe/public/js/frappe/ui/group_by/group_by.js
+++ b/frappe/public/js/frappe/ui/group_by/group_by.js
@@ -381,10 +381,11 @@ frappe.ui.GroupBy = class {
this.group_by_fields = {};
this.all_fields = {};
- let fields = this.report_view.meta.fields.filter((f) =>
+ const fields = this.report_view.meta.fields.filter((f) =>
['Select', 'Link', 'Data', 'Int', 'Check'].includes(f.fieldtype)
);
- this.group_by_fields[this.doctype] = fields;
+ const tag_field = {fieldname: '_user_tags', fieldtype: 'Data', label: __('Tags')};
+ this.group_by_fields[this.doctype] = fields.concat(tag_field);
this.all_fields[this.doctype] = this.report_view.meta.fields;
const standard_fields_filter = (df) =>
diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js
index 2e8ba7d206..067fed233c 100644
--- a/frappe/public/js/frappe/ui/messages.js
+++ b/frappe/public/js/frappe/ui/messages.js
@@ -26,13 +26,13 @@ frappe.throw = function(msg) {
frappe.confirm = function(message, confirm_action, reject_action) {
var d = new frappe.ui.Dialog({
- title: __("Confirm"),
- primary_action_label: __("Yes"),
+ title: __("Confirm", null, "Title of confirmation dialog"),
+ primary_action_label: __("Yes", null, "Approve confirmation dialog"),
primary_action: () => {
confirm_action && confirm_action();
d.hide();
},
- secondary_action_label: __("No"),
+ secondary_action_label: __("No", null, "Dismiss confirmation dialog"),
secondary_action: () => d.hide(),
});
@@ -88,9 +88,9 @@ frappe.prompt = function(fields, callback, title, primary_label) {
if(!$.isArray(fields)) fields = [fields];
var d = new frappe.ui.Dialog({
fields: fields,
- title: title || __("Enter Value"),
+ title: title || __("Enter Value", null, "Title of prompt dialog"),
});
- d.set_primary_action(primary_label || __("Submit"), function() {
+ d.set_primary_action(primary_label || __("Submit", null, "Primary action of prompt dialog"), function() {
var values = d.get_values();
if(!values) {
return;
diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js
index c5ef0d0184..22fdf476b8 100644
--- a/frappe/public/js/frappe/ui/page.js
+++ b/frappe/public/js/frappe/ui/page.js
@@ -377,11 +377,12 @@ frappe.ui.Page = class Page {
});
}
- add_actions_menu_item(label, click, standard) {
+ add_actions_menu_item(label, click, standard, shortcut) {
return this.add_dropdown_item({
label,
click,
standard,
+ shortcut,
parent: this.actions,
show_parent: false
});
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index b29b6b87e6..6a324f6034 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -410,7 +410,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
x_fields.push({
label: col.content,
fieldname: col.id,
- value: col.id,
+ value: col.id,
});
// numeric values in y
@@ -1024,8 +1024,12 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
return docfield.fieldtype === 'Date' ? 'right' : 'left';
})();
+ let id = fieldname;
+
// child table column
- const id = doctype !== this.doctype ? `${doctype}:${fieldname}` : fieldname;
+ if (doctype !== this.doctype && fieldname !== '_aggregate_column') {
+ id = `${doctype}:${fieldname}`;
+ }
let width = (docfield ? cint(docfield.width) : null) || null;
if (this.report_doc) {
diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js
index eefb78c29a..3f5a4acd73 100644
--- a/frappe/public/js/frappe/widgets/widget_dialog.js
+++ b/frappe/public/js/frappe/widgets/widget_dialog.js
@@ -271,18 +271,19 @@ class ShortcutDialog extends WidgetDialog {
}
process_data(data) {
- let stats_filter = {};
if (this.dialog.get_value("type") == "DocType" && this.filter_group) {
let filters = this.filter_group.get_filters();
+ let stats_filter = null;
if (filters.length) {
+ stats_filter = {};
filters.forEach((arr) => {
stats_filter[arr[1]] = [arr[2], arr[3]];
});
-
- data.stats_filter = JSON.stringify(stats_filter);
+ stats_filter = JSON.stringify(stats_filter);
}
+ data.stats_filter = stats_filter;
}
data.label = data.label
diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss
index 83fc4461d6..c939c6de39 100644
--- a/frappe/public/scss/common/controls.scss
+++ b/frappe/public/scss/common/controls.scss
@@ -241,6 +241,7 @@ textarea.form-control {
// rating
.rating {
+ cursor: pointer;
--star-fill: var(--gray-300);
.star-hover {
--star-fill: var(--yellow-100);
@@ -248,6 +249,24 @@ textarea.form-control {
.star-click {
--star-fill: var(--yellow-300);
}
+
+ .rating-box {
+ background-color: var(--gray-300);
+ border-radius: 5px;
+ font-size: 14px;
+ text-align: center;
+ padding: 2px;
+ cursor: pointer;
+ width: 25px;
+ height: 25px;
+ margin: 4px 2px;
+ }
+ .rating-hover {
+ background-color: var(--yellow-100);
+ }
+ .rating-click {
+ background-color: var(--yellow-300);
+ }
}
.frappe-control .control-value {
diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss
index aac949b1bf..57d0583b35 100644
--- a/frappe/public/scss/common/grid.scss
+++ b/frappe/public/scss/common/grid.scss
@@ -53,7 +53,12 @@
display: none;
}
+.form-grid .grid-heading-row .template-row {
+ margin-left: 20px;
+}
+
.form-grid .template-row {
+ width: calc(100% - 30px);
padding: 8px 15px;
}
diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss
index 6f6e09dc70..12706d6b7f 100644
--- a/frappe/public/scss/common/quill.scss
+++ b/frappe/public/scss/common/quill.scss
@@ -176,6 +176,7 @@
}
.ql-editor.read-mode {
+ height: unset;
padding: 0;
.mention {
--user-mention-bg-color: var(--control-bg);
diff --git a/frappe/public/scss/website/blog.scss b/frappe/public/scss/website/blog.scss
index 9918b490c5..ea82efed21 100644
--- a/frappe/public/scss/website/blog.scss
+++ b/frappe/public/scss/website/blog.scss
@@ -14,6 +14,10 @@
position: relative;
width: 100%;
+ .card {
+ border: 1px solid var(--border-color)
+ }
+
.card-body {
display: flex;
flex-direction: column;
diff --git a/frappe/public/scss/website/markdown.scss b/frappe/public/scss/website/markdown.scss
index c5f44d20d8..6f009df393 100644
--- a/frappe/public/scss/website/markdown.scss
+++ b/frappe/public/scss/website/markdown.scss
@@ -48,7 +48,6 @@ $font-sizes-mobile: (
}
li {
- text-indent: 0.25rem;
padding-top: 1px;
padding-bottom: 1px;
}
diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py
index 566c0f6a6d..1fb84531b0 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_content
INDEX_NAME = "web_routes"
@@ -63,7 +63,7 @@ class WebsiteSearch(FullTextSearch):
try:
set_request(method="GET", path=route)
- content = render_page(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/sessions.py b/frappe/sessions.py
index 1bc78448e7..4d922d6769 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -167,7 +167,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/templates/includes/comments/comments.html b/frappe/templates/includes/comments/comments.html
index c490bedd72..935fa5367e 100644
--- a/frappe/templates/includes/comments/comments.html
+++ b/frappe/templates/includes/comments/comments.html
@@ -49,8 +49,10 @@
{% endif %}
\ No newline at end of file
diff --git a/frappe/templates/includes/feedback/feedback.py b/frappe/templates/includes/feedback/feedback.py
new file mode 100644
index 0000000000..1830a3e09e
--- /dev/null
+++ b/frappe/templates/includes/feedback/feedback.py
@@ -0,0 +1,63 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+from __future__ import unicode_literals
+
+import frappe
+
+from frappe import _
+
+@frappe.whitelist(allow_guest=True)
+def add_feedback(reference_doctype, reference_name, rating, feedback, feedback_email):
+ doc = frappe.get_doc(reference_doctype, reference_name)
+ if doc.disable_feedback == 1:
+ return
+
+ doc = frappe.new_doc('Feedback')
+ doc.reference_doctype = reference_doctype
+ doc.reference_name = reference_name
+ doc.rating = rating
+ doc.feedback = feedback
+ doc.email = feedback_email
+ doc.save(ignore_permissions=True)
+
+ subject = _('New Feedback on {0}: {1}').format(reference_doctype, reference_name)
+ send_mail(doc, subject)
+ return doc
+
+@frappe.whitelist()
+def update_feedback(reference_doctype, reference_name, rating, feedback, feedback_email):
+ doc = frappe.get_doc(reference_doctype, reference_name)
+ if doc.disable_feedback == 1:
+ return
+
+ filters = {
+ "email": feedback_email,
+ "reference_doctype": reference_doctype,
+ "reference_name": reference_name
+ }
+ d = frappe.get_all('Feedback', filters=filters, limit=1)
+ doc = frappe.get_doc('Feedback', d[0].name)
+ doc.rating = rating
+ doc.feedback = feedback
+ doc.save(ignore_permissions=True)
+
+ subject = _('Feedback updated on {0}: {1}').format(reference_doctype, reference_name)
+ send_mail(doc, subject)
+ return doc
+
+def send_mail(feedback, subject):
+ doc = frappe.get_doc(feedback.reference_doctype, feedback.reference_name)
+
+ message = ("{0} ({1})
".format(feedback.feedback, feedback.rating)
+ + "{2}
".format(frappe.utils.get_request_site_address(),
+ feedback.name,
+ _("View Feedback")))
+
+ # notify creator
+ frappe.sendmail(
+ recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner,
+ subject=subject,
+ message=message,
+ reference_doctype=doc.doctype,
+ reference_name=doc.name
+ )
diff --git a/frappe/templates/includes/full_index.html b/frappe/templates/includes/full_index.html
index a7443c482a..eb8fb322f6 100644
--- a/frappe/templates/includes/full_index.html
+++ b/frappe/templates/includes/full_index.html
@@ -3,11 +3,6 @@
{% for item in children_map[route] %}
{{ item.title }}
- {#
- {% if children_map[item.route] %}
- {{ make_item_list(item.route, children_map) }}
- {% endif %}
- #}
{% endfor %}
diff --git a/frappe/templates/test/_test_base.html b/frappe/templates/test/_test_base.html
index a0b1a83c97..17caf8df1b 100644
--- a/frappe/templates/test/_test_base.html
+++ b/frappe/templates/test/_test_base.html
@@ -1,8 +1,20 @@
-
+
+ {%- block style %}
+ {% if colocated_css -%}
+
+ {%- endif %}
+ {%- endblock -%}
+
+ {% include "templates/includes/breadcrumbs.html" %}
This is for testing
{% block content %}{% endblock %}
+ {%- block script %}
+ {% if colocated_js -%}
+
+ {%- endif %}
+ {%- endblock %}
diff --git a/frappe/test_runner.py b/frappe/test_runner.py
index 1f99e55fb8..0c30fbbd00 100644
--- a/frappe/test_runner.py
+++ b/frappe/test_runner.py
@@ -175,6 +175,7 @@ def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_x
for doctype in module.test_dependencies:
make_test_records(doctype, verbose=verbose)
+ frappe.db.commit()
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False):
diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py
index 7e77aab779..29939fea1c 100644
--- a/frappe/tests/test_api.py
+++ b/frappe/tests/test_api.py
@@ -39,6 +39,11 @@ class TestResourceAPI(unittest.TestCase):
for name in self.GENERATED_DOCUMENTS:
frappe.delete_doc_if_exists(self.DOCTYPE, name)
+ def setUp(self):
+ # commit to ensure consistency in session (postgres CI randomly fails)
+ if frappe.conf.db_type == "postgres":
+ frappe.db.commit()
+
@property
def sid(self):
if not getattr(self, "_sid", None):
diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py
index b6cd0b575c..07bdf8791e 100644
--- a/frappe/tests/test_commands.py
+++ b/frappe/tests/test_commands.py
@@ -80,7 +80,7 @@ def exists_in_backup(doctypes, file):
)
with gzip.open(file, "rb") as f:
content = f.read().decode("utf8")
- return all([predicate.format(doctype).lower() in content.lower() for doctype in doctypes])
+ return all(predicate.format(doctype).lower() in content.lower() for doctype in doctypes)
class BaseTestCommands(unittest.TestCase):
@@ -355,12 +355,12 @@ class TestCommands(BaseTestCommands):
# test 2: bare functionality for single site
self.execute("bench --site {site} list-apps")
self.assertEqual(self.returncode, 0)
- list_apps = set([
+ list_apps = set(
_x.split()[0] for _x in self.stdout.split("\n")
- ])
+ )
doctype = frappe.get_single("Installed Applications").installed_applications
if doctype:
- installed_apps = set([x.app_name for x in doctype])
+ installed_apps = set(x.app_name for x in doctype)
else:
installed_apps = set(frappe.get_installed_apps())
self.assertSetEqual(list_apps, installed_apps)
diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py
index 42ebd05b67..89975b46d6 100644
--- a/frappe/tests/test_db_query.py
+++ b/frappe/tests/test_db_query.py
@@ -21,6 +21,18 @@ class TestReportview(unittest.TestCase):
def test_basic(self):
self.assertTrue({"name":"DocType"} in DatabaseQuery("DocType").execute(limit_page_length=None))
+ def test_extract_tables(self):
+ db_query = DatabaseQuery("DocType")
+ add_custom_field("DocType", 'test_tab_field', 'Data')
+
+ db_query.fields = ["tabNote.creation", "test_tab_field", "tabDocType.test_tab_field"]
+ db_query.extract_tables()
+ self.assertIn("`tabNote`", db_query.tables)
+ self.assertIn("`tabDocType`", db_query.tables)
+ self.assertNotIn("test_tab_field", db_query.tables)
+
+ clear_custom_fields("DocType")
+
def test_build_match_conditions(self):
clear_user_permissions_for_doctype('Blog Post', 'test2@example.com')
diff --git a/frappe/tests/test_recorder.py b/frappe/tests/test_recorder.py
index 08dbde0144..d9386ca25b 100644
--- a/frappe/tests/test_recorder.py
+++ b/frappe/tests/test_recorder.py
@@ -7,7 +7,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_content
import sqlparse
@@ -121,5 +121,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_content("error")
self.assertIn("Error", content)
diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py
index 52ddc5ef71..6f265d9b94 100644
--- a/frappe/tests/test_website.py
+++ b/frappe/tests/test_website.py
@@ -1,38 +1,41 @@
import unittest
import frappe
-from frappe.website import render
-from frappe.website.utils import get_home_page
from frappe.utils import set_request
+from frappe.website.serve import get_response, get_response_content
+from frappe.website.utils import (build_response, clear_website_cache, get_home_page)
class TestWebsite(unittest.TestCase):
+ def setUp(self):
+ frappe.set_user('Guest')
- def test_home_page_for_role(self):
- 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')
+ def tearDown(self):
+ frappe.set_user('Administrator')
+
+ def test_home_page(self):
+ frappe.set_user('Administrator')
+ # test home page via role
user = frappe.get_doc(dict(
doctype='User',
email='test-user-for-home-page@example.com',
- first_name='test')).insert()
+ first_name='test')).insert(ignore_if_duplicate=True)
role = frappe.get_doc(dict(
doctype = 'Role',
role_name = 'home-page-test',
desk_access = 0,
- home_page = '/home-page-test'
- )).insert()
+ )).insert(ignore_if_duplicate=True)
user.add_roles(role.name)
user.save()
+ frappe.db.set_value('Role', 'home-page-test', 'home_page', 'home-page-test')
frappe.set_user('test-user-for-home-page@example.com')
self.assertEqual(get_home_page(), 'home-page-test')
frappe.set_user('Administrator')
- role.home_page = ''
- role.save()
+ frappe.db.set_value('Role', 'home-page-test', 'home_page', '')
# home page via portal settings
frappe.db.set_value('Portal Settings', None, 'default_portal_home', 'test-portal-home')
@@ -41,10 +44,45 @@ class TestWebsite(unittest.TestCase):
frappe.cache().hdel('home_page', frappe.session.user)
self.assertEqual(get_home_page(), 'test-portal-home')
- def test_page_load(self):
+ frappe.db.set_value("Portal Settings", None, "default_portal_home", '')
+ clear_website_cache()
+
+ # home page via website settings
+ frappe.db.set_value("Website Settings", None, "home_page", 'contact')
+ self.assertEqual(get_home_page(), 'contact')
+
+ frappe.db.set_value("Website Settings", None, "home_page", None)
+ clear_website_cache()
+
+ # fallback homepage
+ self.assertEqual(get_home_page(), 'me')
+
+ # fallback homepage for guest
frappe.set_user('Guest')
+ self.assertEqual(get_home_page(), 'login')
+ frappe.set_user('Administrator')
+
+ # test homepage via hooks
+ clear_website_cache()
+ set_home_page_hook('get_website_user_home_page', 'frappe.www._test._test_home_page.get_website_user_home_page')
+ self.assertEqual(get_home_page(), '_test/_test_folder')
+
+ clear_website_cache()
+ set_home_page_hook('website_user_home_page', 'login')
+ self.assertEqual(get_home_page(), 'login')
+
+ clear_website_cache()
+ set_home_page_hook('home_page', 'about')
+ self.assertEqual(get_home_page(), 'about')
+
+ clear_website_cache()
+ set_home_page_hook('role_home_page', {'home-page-test': 'home-page-test'})
+ self.assertEqual(get_home_page(), 'home-page-test')
+
+
+ def test_page_load(self):
set_request(method='POST', path='login')
- response = render.render()
+ response = get_response()
self.assertEqual(response.status_code, 200)
@@ -52,14 +90,52 @@ 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 = get_response()
+ self.assertEqual(response.status_code, 200)
+
+ def test_error_page(self):
+ set_request(method='GET', path='/_test/problematic_page')
+ response = get_response()
+ self.assertEqual(response.status_code, 500)
+
+ def test_login(self):
+ set_request(method='GET', path='/login')
+ response = get_response()
+ self.assertEqual(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 = get_response()
+ self.assertEqual(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 = get_response()
+ self.assertEqual(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'),
- 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')
@@ -69,32 +145,82 @@ class TestWebsite(unittest.TestCase):
})
website_settings.save()
- frappe.cache().delete_key('app_hooks')
- frappe.cache().delete_key('website_redirects')
-
set_request(method='GET', path='/testfrom')
- response = render.render()
+ 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 = render.render()
+ 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 = render.render()
+ 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 = render.render()
+ response = get_response()
self.assertEqual(response.status_code, 404)
set_request(method='GET', path='/testsource')
- response = render.render()
+ 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 = get_response()
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual(response.headers.get('Location'), '/courses/data')
+
delattr(frappe.hooks, 'website_redirects')
frappe.cache().delete_key('app_hooks')
+
+ def test_custom_page_renderer(self):
+ import frappe.hooks
+ frappe.hooks.page_renderer = ['frappe.tests.test_website.CustomPageRenderer']
+ frappe.cache().delete_key('app_hooks')
+ set_request(method='GET', path='/custom')
+ response = get_response()
+ self.assertEqual(response.status_code, 3984)
+
+ set_request(method='GET', path='/new')
+ content = get_response_content()
+ self.assertIn("Custom Page Response
", content)
+
+ set_request(method='GET', path='/random')
+ response = get_response()
+ self.assertEqual(response.status_code, 404)
+
+ delattr(frappe.hooks, 'page_renderer')
+ frappe.cache().delete_key('app_hooks')
+
+ def test_printview_page(self):
+ content = get_response_content('/Language/en')
+ self.assertIn('', content)
+ self.assertIn('
Language
', content)
+
+
+def set_home_page_hook(key, value):
+ from frappe import hooks
+ # reset home_page hooks
+ for hook in ('get_website_user_home_page','website_user_home_page','role_home_page','home_page'):
+ if hasattr(hooks, hook):
+ delattr(hooks, hook)
+
+ setattr(hooks, key, value)
+ frappe.cache().delete_key('app_hooks')
+
+class CustomPageRenderer():
+ def __init__(self, path, status_code=None):
+ self.path = path
+ # custom status code
+ self.status_code = 3984
+
+ def can_render(self):
+ if self.path in ('new', 'custom'):
+ return True
+
+ def render(self):
+ return build_response(self.path, """
Custom Page Response
""", self.status_code)
diff --git a/frappe/translate.py b/frappe/translate.py
index a407ad9579..4ff50d3fd0 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -284,8 +284,8 @@ def clear_cache():
def get_messages_for_app(app, deduplicate=True):
"""Returns all messages (list) for a specified `app`"""
messages = []
- modules = ", ".join(['"{}"'.format(m.title().replace("_", " ")) \
- for m in frappe.local.app_modules[app]])
+ modules = ", ".join('"{}"'.format(m.title().replace("_", " ")) \
+ for m in frappe.local.app_modules[app])
# doctypes
if modules:
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index 969e8194e4..af9d5de1ee 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -189,7 +189,7 @@ def random_string(length):
"""generate a random string"""
import string
from random import choice
- return ''.join([choice(string.ascii_letters + string.digits) for i in range(length)])
+ return ''.join(choice(string.ascii_letters + string.digits) for i in range(length))
def has_gravatar(email):
@@ -311,7 +311,7 @@ def make_esc(esc_chars):
"""
Function generator for Escaping special characters
"""
- return lambda s: ''.join(['\\' + c if c in esc_chars else c for c in s])
+ return lambda s: ''.join('\\' + c if c in esc_chars else c for c in s)
# esc / unescape characters -- used for command line
def esc(s, esc_chars):
@@ -748,9 +748,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/utils/backups.py b/frappe/utils/backups.py
index b21efc5e89..908be52452 100644
--- a/frappe/utils/backups.py
+++ b/frappe/utils/backups.py
@@ -307,8 +307,8 @@ class BackupGenerator:
backup_summary = self.get_summary()
print("Backup Summary for {0} at {1}".format(frappe.local.site, now()))
- title = max([len(x) for x in backup_summary])
- path = max([len(x["path"]) for x in backup_summary.values()])
+ title = max(len(x) for x in backup_summary)
+ path = max(len(x["path"]) for x in backup_summary.values())
for _type, info in backup_summary.items():
template = "{{0:{0}}}: {{1:{1}}} {{2}}".format(title, path)
@@ -381,7 +381,7 @@ class BackupGenerator:
"",
])
- generated_header = "\n".join([f"-- {x}" for x in database_header_content]) + "\n"
+ generated_header = "\n".join(f"-- {x}" for x in database_header_content) + "\n"
with gzip.open(args.backup_path_db, "wt") as f:
f.write(generated_header)
diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py
index e80410393d..8dab9b748f 100755
--- a/frappe/utils/boilerplate.py
+++ b/frappe/utils/boilerplate.py
@@ -359,7 +359,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/utils/bot.py b/frappe/utils/bot.py
index 572723c056..c75b48ab49 100644
--- a/frappe/utils/bot.py
+++ b/frappe/utils/bot.py
@@ -38,10 +38,10 @@ class BotParser(object):
def format_list(self, data):
'''Format list as markdown'''
- return _('I found these: ') + ', '.join([' [{title}](/app/Form/{doctype}/{name})'.format(
+ return _('I found these:') + ' ' + ', '.join(' [{title}](/app/Form/{doctype}/{name})'.format(
title = d.title or d.name,
doctype=self.get_doctype(),
- name=d.name) for d in data])
+ name=d.name) for d in data)
def get_doctype(self):
'''returns the doctype name from self.tables'''
@@ -56,8 +56,8 @@ class ShowNotificationBot(BotParser):
if open_items:
return ("Following items need your attention:\n\n"
- + "\n\n".join(["{0} [{1}](/app/List/{1})".format(d[1], d[0])
- for d in open_items if d[1] > 0]))
+ + "\n\n".join("{0} [{1}](/app/List/{1})".format(d[1], d[0])
+ for d in open_items if d[1] > 0))
else:
return 'Take it easy, nothing urgent needs your attention'
diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py
index 3cf10cd9e0..efe92232d9 100644
--- a/frappe/utils/global_search.py
+++ b/frappe/utils/global_search.py
@@ -307,14 +307,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_content
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_content(route)
soup = BeautifulSoup(content, 'html.parser')
page_content = soup.find(class_='page_content')
text_content = page_content.text if page_content else ''
@@ -329,7 +329,7 @@ def add_route_to_global_search(route):
route=route
)
sync_value_in_queue(value)
- except (frappe.PermissionError, frappe.DoesNotExistError, frappe.ValidationError, Exception):
+ except Exception:
pass
frappe.set_user('Administrator')
diff --git a/frappe/utils/jinja_globals.py b/frappe/utils/jinja_globals.py
index 924b8bb8b9..2c14249672 100644
--- a/frappe/utils/jinja_globals.py
+++ b/frappe/utils/jinja_globals.py
@@ -10,10 +10,10 @@ def resolve_class(classes):
return classes
if isinstance(classes, (list, tuple)):
- return " ".join([resolve_class(c) for c in classes]).strip()
+ return " ".join(resolve_class(c) for c in classes).strip()
if isinstance(classes, dict):
- return " ".join([classname for classname in classes if classes[classname]]).strip()
+ return " ".join(classname for classname in classes if classes[classname]).strip()
return classes
diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py
index 92459abc46..fcf483bea6 100644
--- a/frappe/utils/pdf.py
+++ b/frappe/utils/pdf.py
@@ -42,6 +42,7 @@ def get_pdf(html, options=None, output=None):
except OSError as e:
if any([error in str(e) for error in PDF_CONTENT_ERRORS]):
if not filedata:
+ print(html, options)
frappe.throw(_("PDF generation failed because of broken image links"))
# allow pdfs with missing images if file got created
diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py
index 6cf2ecc304..40a393a2cc 100644
--- a/frappe/utils/print_format.py
+++ b/frappe/utils/print_format.py
@@ -7,7 +7,7 @@ from PyPDF2 import PdfFileWriter
no_cache = 1
-base_template_path = "templates/www/printview.html"
+base_template_path = "www/printview.html"
standard_format = "templates/print_formats/standard.html"
@frappe.whitelist()
diff --git a/frappe/utils/response.py b/frappe/utils/response.py
index 84f30f22fd..ca04f6def4 100644
--- a/frappe/utils/response.py
+++ b/frappe/utils/response.py
@@ -147,8 +147,8 @@ def json_handler(obj):
def as_page():
"""print web page"""
- from frappe.website.render import render
- return render(frappe.response['route'], http_status_code=frappe.response.get("http_status_code"))
+ from frappe.website.serve import get_response
+ return get_response(frappe.response['route'], http_status_code=frappe.response.get("http_status_code"))
def redirect():
return werkzeug.utils.redirect(frappe.response.location)
@@ -215,7 +215,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/context.py b/frappe/website/context.py
deleted file mode 100644
index 8278edb958..0000000000
--- a/frappe/website/context.py
+++ /dev/null
@@ -1,296 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-import frappe, os, json
-
-from frappe.website.doctype.website_settings.website_settings import get_website_settings
-from frappe.website.router import get_page_context
-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
- import frappe.www.list
-
- 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`'],
- order_by='idx asc')
-
- if not context.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)
-
- context.sidebar_items = sidebar_items
-
- info = get_fullname_and_avatar(frappe.session.user)
- context["fullname"] = info.fullname
- context["user_image"] = info.avatar
- context["user"] = info.name
-
-
-def add_metatags(context):
- tags = frappe._dict(context.get("metatags") or {})
-
- if "og:type" not in tags:
- tags["og:type"] = "article"
-
- if "title" not in tags and context.title:
- tags["title"] = context.title
-
- title = tags.get("name") or tags.get("title")
- if title:
- tags["og:title"] = tags["twitter:title"] = title
- tags["twitter:card"] = "summary"
-
- if "description" not in tags 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
diff --git a/frappe/website/doctype/about_us_settings/about_us_settings.py b/frappe/website/doctype/about_us_settings/about_us_settings.py
index 938a8de1b9..1d45adeb42 100644
--- a/frappe/website/doctype/about_us_settings/about_us_settings.py
+++ b/frappe/website/doctype/about_us_settings/about_us_settings.py
@@ -10,7 +10,7 @@ from frappe.model.document import Document
class AboutUsSettings(Document):
def on_update(self):
- from frappe.website.render import clear_cache
+ from frappe.website.utils import clear_cache
clear_cache("about")
def get_args():
diff --git a/frappe/website/doctype/blog_category/blog_category.py b/frappe/website/doctype/blog_category/blog_category.py
index 91b213744f..d7eb92ca7a 100644
--- a/frappe/website/doctype/blog_category/blog_category.py
+++ b/frappe/website/doctype/blog_category/blog_category.py
@@ -2,7 +2,7 @@
# MIT License. See license.txt
from frappe.website.website_generator import WebsiteGenerator
-from frappe.website.render import clear_cache
+from frappe.website.utils import clear_cache
class BlogCategory(WebsiteGenerator):
def autoname(self):
diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json
index 909cecf867..c2491ee5a4 100644
--- a/frappe/website/doctype/blog_post/blog_post.json
+++ b/frappe/website/doctype/blog_post/blog_post.json
@@ -18,6 +18,7 @@
"featured",
"hide_cta",
"disable_comments",
+ "disable_feedback",
"section_break_5",
"blog_intro",
"content_type",
@@ -191,6 +192,13 @@
"fieldtype": "Data",
"label": "Meta Title",
"length": 60
+ },
+ {
+ "default": "0",
+ "description": "Feedback on this blog post will be disabled if checked.",
+ "fieldname": "disable_feedback",
+ "fieldtype": "Check",
+ "label": "Disable Feedback"
}
],
"has_web_view": 1,
@@ -200,7 +208,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 5,
- "modified": "2020-12-23 14:28:36.311389",
+ "modified": "2021-06-14 13:50:02.109719",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Post",
diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py
index b39b7c7d04..965fc8e3e0 100644
--- a/frappe/website/doctype/blog_post/blog_post.py
+++ b/frappe/website/doctype/blog_post/blog_post.py
@@ -4,18 +4,13 @@
import frappe
from frappe import _
from frappe.website.website_generator import WebsiteGenerator
-from frappe.website.render import clear_cache
+from frappe.website.utils import clear_cache
from frappe.utils import today, cint, global_date_format, get_fullname, strip_html_tags, markdown, sanitize_html
from math import ceil
from frappe.website.utils import (find_first_image, get_html_content_based_on_type,
get_comment_list)
class BlogPost(WebsiteGenerator):
- website = frappe._dict(
- route = 'blog',
- order_by = "published_on desc"
- )
-
@frappe.whitelist()
def make_route(self):
if not self.route:
@@ -102,12 +97,14 @@ class BlogPost(WebsiteGenerator):
context.metatags["image"] = self.meta_image or image or None
self.load_comments(context)
+ self.load_feedback(context)
context.category = frappe.db.get_value("Blog Category",
context.doc.blog_category, ["title", "route"], as_dict=1)
context.parents = [{"name": _("Home"), "route":"/"},
{"name": "Blog", "route": "/blog"},
{"label": context.category.title, "route":context.category.route}]
+ context.guest_allowed = True
def fetch_cta(self):
if frappe.db.get_single_value("Blog Settings", "show_cta_in_blog", cache=True):
@@ -149,6 +146,17 @@ class BlogPost(WebsiteGenerator):
else:
context.comment_text = _('{0} comments').format(len(context.comment_list))
+ def load_feedback(self, context):
+ feedback = frappe.get_all('Feedback',
+ fields=['email', 'feedback', 'rating'],
+ filters=dict(
+ reference_doctype=self.doctype,
+ reference_name=self.name,
+ email=frappe.session.user
+ )
+ )
+ context.user_feedback = feedback[0] if feedback else ''
+
def set_read_time(self):
content = self.content or self.content_html or ''
if self.content_type == "Markdown":
diff --git a/frappe/website/doctype/blog_post/templates/blog_post.html b/frappe/website/doctype/blog_post/templates/blog_post.html
index dad8b97164..4678622062 100644
--- a/frappe/website/doctype/blog_post/templates/blog_post.html
+++ b/frappe/website/doctype/blog_post/templates/blog_post.html
@@ -65,6 +65,11 @@
{% include 'templates/includes/comments/comments.html' %}
{% endif %}
+ {% if not disable_feedback %}
+
+ {% include 'templates/includes/feedback/feedback.html' %}
+
+ {% endif %}
", content)
+ self.assertIn("background-color: var(--bg-color);", content)
+
+ def test_breadcrumbs(self):
+ content = get_response_content('/_test/_test_folder/_test_page')
+ self.assertIn('Test TOC', content)
+ self.assertIn(' Test Page', content)
+
+ content = get_response_content('/_test/_test_folder/index')
+ self.assertIn(' Test', content)
+ self.assertIn('Test TOC', content)
+
+ def test_downloadable_file(self):
+ pass
diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py
index cea14d3bbe..05f5cac546 100644
--- a/frappe/website/doctype/web_page/web_page.py
+++ b/frappe/website/doctype/web_page/web_page.py
@@ -3,20 +3,17 @@
import re
-import requests
-import requests.exceptions
from jinja2.exceptions import TemplateSyntaxError
import frappe
from frappe import _
-from frappe.utils import get_datetime, now, strip_html, quoted
+from frappe.utils import get_datetime, now, quoted, strip_html
from frappe.utils.jinja import render_template
-from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
-from frappe.website.router import resolve_route
-from frappe.website.utils import (extract_title, find_first_image, get_comment_list,
- get_html_content_based_on_type)
-from frappe.website.website_generator import WebsiteGenerator
from frappe.utils.safe_exec import safe_exec
+from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
+from frappe.website.utils import (extract_title, find_first_image,
+ get_comment_list, get_html_content_based_on_type)
+from frappe.website.website_generator import WebsiteGenerator
class WebPage(WebsiteGenerator):
@@ -53,6 +50,7 @@ class WebPage(WebsiteGenerator):
if self.enable_comments:
context.comment_list = get_comment_list(self.doctype, self.name)
+ context.guest_allowed = True
context.update({
"style": self.css or "",
@@ -183,32 +181,6 @@ def check_publish_status():
frappe.db.set_value("Web Page", page.name, "published", 1)
-
-def check_broken_links():
- cnt = 0
- for p in frappe.db.sql("select name, main_section from `tabWeb Page`", as_dict=True):
- for link in re.findall('href=["\']([^"\']*)["\']', p.main_section):
- if link.startswith("http"):
- try:
- res = requests.get(link)
- except requests.exceptions.SSLError:
- res = frappe._dict({"status_code": "SSL Error"})
- except requests.exceptions.ConnectionError:
- res = frappe._dict({"status_code": "Connection Error"})
-
- if res.status_code!=200:
- print("[{0}] {1}: {2}".format(res.status_code, p.name, link))
- cnt += 1
- else:
- link = link[1:] # remove leading /
- link = link.split("#")[0]
-
- if not resolve_route(link):
- print(p.name + ":" + link)
- cnt += 1
-
- print("{0} links broken".format(cnt))
-
def get_web_blocks_html(blocks):
'''Converts a list of blocks into Raw HTML and extracts out their scripts for deduplication'''
diff --git a/frappe/website/doctype/web_template/test_web_template.py b/frappe/website/doctype/web_template/test_web_template.py
index 45e35c4626..2f2dbdc40a 100644
--- a/frappe/website/doctype/web_template/test_web_template.py
+++ b/frappe/website/doctype/web_template/test_web_template.py
@@ -5,8 +5,7 @@ import frappe
import unittest
from bs4 import BeautifulSoup
from frappe.utils import set_request
-from frappe.website.render import render
-
+from frappe.website.serve import get_response
class TestWebTemplate(unittest.TestCase):
def test_render_web_template_with_values(self):
@@ -34,7 +33,7 @@ class TestWebTemplate(unittest.TestCase):
self.create_web_page()
set_request(method="GET", path="test-web-template")
- response = render()
+ response = get_response()
self.assertEqual(response.status_code, 200)
@@ -56,7 +55,7 @@ class TestWebTemplate(unittest.TestCase):
frappe.conf.developer_mode = 1
set_request(method="GET", path="test-web-template")
- response = render()
+ response = get_response()
self.assertEqual(response.status_code, 200)
html = frappe.safe_decode(response.get_data())
diff --git a/frappe/website/doctype/web_template/web_template.py b/frappe/website/doctype/web_template/web_template.py
index 891a0c3679..6905680523 100644
--- a/frappe/website/doctype/web_template/web_template.py
+++ b/frappe/website/doctype/web_template/web_template.py
@@ -7,7 +7,7 @@ from shutil import rmtree
import frappe
from frappe.model.document import Document
-from frappe.website.render import clear_cache
+from frappe.website.utils import clear_cache
from frappe import _
from frappe.modules.export_file import (
write_document_file,
diff --git a/frappe/website/doctype/website_route_meta/test_website_route_meta.py b/frappe/website/doctype/website_route_meta/test_website_route_meta.py
index 1f927abafc..c55dcce1ca 100644
--- a/frappe/website/doctype/website_route_meta/test_website_route_meta.py
+++ b/frappe/website/doctype/website_route_meta/test_website_route_meta.py
@@ -4,7 +4,7 @@
import frappe
import unittest
from frappe.utils import set_request
-from frappe.website.render import render
+from frappe.website.serve import get_response
test_dependencies = ['Blog Post']
class TestWebsiteRouteMeta(unittest.TestCase):
@@ -29,7 +29,7 @@ class TestWebsiteRouteMeta(unittest.TestCase):
# set request on this route
set_request(path=blog.route)
- response = render()
+ response = get_response()
self.assertTrue(response.status_code, 200)
diff --git a/frappe/website/doctype/website_script/website_script.py b/frappe/website/doctype/website_script/website_script.py
index 111beeaf2a..6ec10291cd 100644
--- a/frappe/website/doctype/website_script/website_script.py
+++ b/frappe/website/doctype/website_script/website_script.py
@@ -13,5 +13,5 @@ class WebsiteScript(Document):
"""clear cache"""
frappe.clear_cache(user = 'Guest')
- from frappe.website.render import clear_cache
+ from frappe.website.utils import clear_cache
clear_cache()
\ No newline at end of file
diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py
index 03a1aecc5f..1ccd106c38 100644
--- a/frappe/website/doctype/website_settings/website_settings.py
+++ b/frappe/website/doctype/website_settings/website_settings.py
@@ -1,14 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
+from urllib.parse import quote
import frappe
from frappe import _
-from frappe.utils import get_request_site_address, encode
-from frappe.model.document import Document
-from urllib.parse import quote
-from frappe.website.router import resolve_route
-from frappe.website.doctype.website_theme.website_theme import add_website_theme
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
+from frappe.model.document import Document
+from frappe.utils import encode, get_request_site_address
+from frappe.website.doctype.website_theme.website_theme import add_website_theme
INDEXING_SCOPES = "https://www.googleapis.com/auth/indexing"
@@ -22,7 +21,8 @@ class WebsiteSettings(Document):
def validate_home_page(self):
if frappe.flags.in_install:
return
- if self.home_page and not resolve_route(self.home_page):
+ from frappe.website.path_resolver import PathResolver
+ if self.home_page and not PathResolver(self.home_page).is_valid_path():
frappe.msgprint(_("Invalid Home Page") + " (Standard pages - index, login, products, blog, about, contact)")
self.home_page = ''
@@ -68,7 +68,7 @@ class WebsiteSettings(Document):
# clear web cache (for menus!)
frappe.clear_cache(user = 'Guest')
- from frappe.website.render import clear_cache
+ from frappe.website.utils import clear_cache
clear_cache()
# clears role based home pages
diff --git a/frappe/website/doctype/website_slideshow/website_slideshow.py b/frappe/website/doctype/website_slideshow/website_slideshow.py
index d31adbf986..8566475b33 100644
--- a/frappe/website/doctype/website_slideshow/website_slideshow.py
+++ b/frappe/website/doctype/website_slideshow/website_slideshow.py
@@ -14,7 +14,7 @@ class WebsiteSlideshow(Document):
def on_update(self):
# a slide show can be in use and any change in it should get reflected
- from frappe.website.render import clear_cache
+ from frappe.website.utils import clear_cache
clear_cache()
def validate_images(self):
@@ -22,7 +22,7 @@ class WebsiteSlideshow(Document):
files = map(lambda row: row.image, self.slideshow_items)
if files:
result = frappe.get_all("File", filters={ "file_url":("in", list(files)) }, fields="is_private")
- if any([file.is_private for file in result]):
+ if any(file.is_private for file in result):
frappe.throw(_("All Images attached to Website Slideshow should be public"))
def get_slideshow(doc):
diff --git a/frappe/website/page/__init__.py b/frappe/website/page/__init__.py
deleted file mode 100644
index 8b13789179..0000000000
--- a/frappe/website/page/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/frappe/website/page_renderers/base_renderer.py b/frappe/website/page_renderers/base_renderer.py
new file mode 100644
index 0000000000..14d4448e6c
--- /dev/null
+++ b/frappe/website/page_renderers/base_renderer.py
@@ -0,0 +1,25 @@
+import frappe
+from frappe.website.utils import build_response
+
+class BaseRenderer(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('/ ')
+ self.basepath = ''
+ self.basename = ''
+ self.name = ''
+ self.route = ''
+ self.file_dir = None
+
+ def can_render(self):
+ raise NotImplementedError
+
+ def render(self):
+ raise NotImplementedError
+
+ def build_response(self, data, http_status_code=None, headers=None):
+ return build_response(self.path, data, http_status_code or self.http_status_code, headers or self.headers)
+
diff --git a/frappe/website/page_renderers/base_template_page.py b/frappe/website/page_renderers/base_template_page.py
new file mode 100644
index 0000000000..7802e6e7f6
--- /dev/null
+++ b/frappe/website/page_renderers/base_template_page.py
@@ -0,0 +1,70 @@
+import frappe
+from frappe.website.doctype.website_settings.website_settings import get_website_settings
+from frappe.website.page_renderers.base_renderer import BaseRenderer
+from frappe.website.website_components.metatags import MetaTags
+
+
+class BaseTemplatePage(BaseRenderer):
+ def __init__(self, path, http_status_code=None):
+ super().__init__(path=path, http_status_code=http_status_code)
+ self.template_path = ''
+
+ def init_context(self):
+ self.context = frappe._dict()
+ self.context.update(get_website_settings())
+ self.context.update(frappe.local.conf.get("website_context") or {})
+
+ def add_csrf_token(self, html):
+ if frappe.local.session:
+ csrf_token = frappe.local.session.data.csrf_token
+ return html.replace("",
+ f'')
+
+ return html
+
+ def post_process_context(self):
+ 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()
+ # context sends us a new template path
+ self.template_path = self.context.template or self.template_path
+ self.context._context_dict = self.context
+ self.set_missing_values()
+
+ 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 set_missing_values(self):
+ # 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.canonical = frappe.utils.get_url(frappe.utils.escape_html(self.path))
+
+ if "url_prefix" not in self.context:
+ self.context.url_prefix = ""
+
+ 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 if hasattr(frappe, 'local') else self.path
+
+ 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)
diff --git a/frappe/website/page_renderers/document_page.py b/frappe/website/page_renderers/document_page.py
new file mode 100644
index 0000000000..f1741c681f
--- /dev/null
+++ b/frappe/website/page_renderers/document_page.py
@@ -0,0 +1,87 @@
+import frappe
+from frappe.model.document import get_controller
+from frappe.website.page_renderers.base_template_page import BaseTemplatePage
+from frappe.website.utils 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 can_render(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
+ 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
+ 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.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_page_info())
+
+ self.template_path = self.context.template or self.template_path
+
+ if not self.template_path:
+ self.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 prop not 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.name)
+ condition_field = controller.website.condition_field
+
+ return condition_field
diff --git a/frappe/website/page_renderers/error_page.py b/frappe/website/page_renderers/error_page.py
new file mode 100644
index 0000000000..3501c77765
--- /dev/null
+++ b/frappe/website/page_renderers/error_page.py
@@ -0,0 +1,10 @@
+from frappe.website.page_renderers.template_page import TemplatePage
+
+class ErrorPage(TemplatePage):
+ 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 can_render(self):
+ return True
diff --git a/frappe/website/page_renderers/list_page.py b/frappe/website/page_renderers/list_page.py
new file mode 100644
index 0000000000..61c781ea14
--- /dev/null
+++ b/frappe/website/page_renderers/list_page.py
@@ -0,0 +1,11 @@
+import frappe
+from frappe.website.page_renderers.template_page import TemplatePage
+
+class ListPage(TemplatePage):
+ def can_render(self):
+ 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_renderers/not_found_page.py b/frappe/website/page_renderers/not_found_page.py
new file mode 100644
index 0000000000..af510fecfc
--- /dev/null
+++ b/frappe/website/page_renderers/not_found_page.py
@@ -0,0 +1,34 @@
+import os
+from urllib.parse import urlparse
+
+import frappe
+from frappe.website.page_renderers.template_page import TemplatePage
+from frappe.website.utils import can_cache
+
+HOMEPAGE_PATHS = ('/', '/index', 'index')
+
+class NotFoundPage(TemplatePage):
+ def __init__(self, path, http_status_code):
+ self.request_path = path
+ self.request_url = frappe.local.request.url if hasattr(frappe.local, 'request') else ''
+ path = '404'
+ http_status_code = 404
+ super().__init__(path=path, http_status_code=http_status_code)
+
+ def can_render(self):
+ return True
+
+ def render(self):
+ if self.can_cache_404():
+ frappe.cache().hset('website_404', self.request_url, True)
+ return super().render()
+
+ def can_cache_404(self):
+ # do not cache 404 for custom homepages
+ return can_cache() and self.request_url and not self.is_custom_home_page()
+
+ def is_custom_home_page(self):
+ url_parts = urlparse(self.request_url)
+ request_url = os.path.splitext(url_parts.path)[0]
+ request_path = os.path.splitext(self.request_path)[0]
+ return request_url in HOMEPAGE_PATHS and request_path not in HOMEPAGE_PATHS
diff --git a/frappe/website/page_renderers/not_permitted_page.py b/frappe/website/page_renderers/not_permitted_page.py
new file mode 100644
index 0000000000..e69299f5c5
--- /dev/null
+++ b/frappe/website/page_renderers/not_permitted_page.py
@@ -0,0 +1,24 @@
+import frappe
+from frappe import _
+from frappe.website.page_renderers.template_page import TemplatePage
+from frappe.utils import cstr
+
+class NotPermittedPage(TemplatePage):
+ 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 can_render(self):
+ return True
+
+ def render(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 super().render()
diff --git a/frappe/website/page_renderers/print_page.py b/frappe/website/page_renderers/print_page.py
new file mode 100644
index 0000000000..05d4026e2b
--- /dev/null
+++ b/frappe/website/page_renderers/print_page.py
@@ -0,0 +1,23 @@
+import frappe
+from frappe.website.page_renderers.template_page import TemplatePage
+
+class PrintPage(TemplatePage):
+ '''
+ default path returns a printable object (based on permission)
+ /Quotation/Q-0001
+ '''
+ def can_render(self):
+ parts = self.path.split('/', 1)
+ if len(parts)==2:
+ 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_renderers/redirect_page.py b/frappe/website/page_renderers/redirect_page.py
new file mode 100644
index 0000000000..2049c375e8
--- /dev/null
+++ b/frappe/website/page_renderers/redirect_page.py
@@ -0,0 +1,16 @@
+import frappe
+from frappe.website.utils 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 can_render(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"
+ })
diff --git a/frappe/website/page_renderers/static_page.py b/frappe/website/page_renderers/static_page.py
new file mode 100644
index 0000000000..632e9b4302
--- /dev/null
+++ b/frappe/website/page_renderers/static_page.py
@@ -0,0 +1,41 @@
+import mimetypes
+import os
+
+from werkzeug.wrappers import Response
+from werkzeug.wsgi import wrap_file
+
+import frappe
+from frappe.website.page_renderers.base_renderer import BaseRenderer
+
+UNSUPPORTED_STATIC_PAGE_TYPES = ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json')
+
+class StaticPage(BaseRenderer):
+ def __init__(self, path, http_status_code=None):
+ super().__init__(path=path, http_status_code=http_status_code)
+ self.set_file_path()
+
+ 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.isfile(file_path):
+ self.file_path = file_path
+
+ def can_render(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')
+ 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_renderers/template_page.py b/frappe/website/page_renderers/template_page.py
new file mode 100644
index 0000000000..5e6e57e33a
--- /dev/null
+++ b/frappe/website/page_renderers/template_page.py
@@ -0,0 +1,281 @@
+import io
+import os
+import click
+
+import frappe
+from frappe.website.router import get_page_info
+from frappe.website.page_renderers.base_template_page import BaseTemplatePage
+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, cache_html, get_sidebar_items, build_response)
+
+WEBPAGE_PY_MODULE_PROPERTIES = ("base_template_path", "template", "no_cache", "sitemap", "condition_field")
+
+COMMENT_PROPERTY_KEY_VALUE_MAP = {
+ "no-breadcrumbs": ("no_breadcrumbs", 1),
+ "show-sidebar": ("show_sidebar", 1),
+ "add-breadcrumbs": ("add_breadcrumbs", 1),
+ "no-header": ("no_header", 1),
+ "add-next-prev-links": ("add_next_prev_links", 1),
+ "no-cache": ("no_cache", 1),
+ "no-sitemap": ("sitemap", 0),
+ "sitemap": ("sitemap", 1)
+}
+
+class TemplatePage(BaseTemplatePage):
+ def __init__(self, path, http_status_code=None):
+ super().__init__(path=path, http_status_code=http_status_code)
+ self.set_template_path()
+
+ def set_template_path(self):
+ '''
+ Searches for file matching the path in the /www
+ and /templates/pages folders and sets path if match is found
+ '''
+ 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 file_path in self.get_index_path_options(search_path):
+ if os.path.isfile(file_path):
+ self.app = app
+ self.app_path = app_path
+ self.file_dir = dirname
+ self.basename = os.path.splitext(file_path)[0]
+ self.template_path = os.path.relpath(file_path, self.app_path)
+ self.basepath = os.path.dirname(file_path)
+ self.filename = os.path.basename(file_path)
+ self.name = os.path.splitext(self.filename)[0]
+ return
+
+ def can_render(self):
+ return hasattr(self, 'template_path') and bool(self.template_path)
+
+ @staticmethod
+ def get_index_path_options(search_path):
+ return (frappe.as_unicode(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)
+
+ @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
+ 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 post_process_context(self):
+ self.set_user_info()
+ self.add_sidebar_and_breadcrumbs()
+ super(TemplatePage, self).post_process_context()
+
+ def add_sidebar_and_breadcrumbs(self):
+ if self.basepath:
+ self.context.sidebar_items = get_sidebar_items(self.context.website_sidebar, self.basepath)
+
+ if self.context.add_breadcrumbs and not self.context.parents:
+ parent_path = os.path.dirname(self.path)
+ if self.path.endswith('index'):
+ # in case of index page move one directory up for parent path
+ parent_path = os.path.dirname(parent_path)
+
+ for parent_file_path in self.get_index_path_options(parent_path):
+ parent_file_path = os.path.join(self.app_path, self.file_dir, parent_file_path)
+ if os.path.isfile(parent_file_path):
+ parent_page_context = get_page_info(parent_file_path, self.app, self.file_dir)
+ if parent_page_context:
+ self.context.parents = [dict(route=os.path.dirname(self.path), title=parent_page_context.title)]
+ break
+
+ 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.
+ '''
+ 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(os.path.dirname(template_basepath), os.path.basename(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]
+
+ def setup_template(self):
+ '''Setup template source, frontmatter and markdown conversion'''
+ self.source = self.get_raw_template()
+ self.extract_frontmatter()
+ self.convert_from_markdown()
+
+ 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 WEBPAGE_PY_MODULE_PROPERTIES:
+ if hasattr(self.pymodule, prop):
+ self.context[prop] = getattr(self.pymodule, prop)
+
+ def set_page_properties(self):
+ self.context.base_template = self.context.base_template \
+ or get_base_template(self.path) \
+ or 'templates/web.html'
+ self.context.basepath = self.basepath
+ self.context.basename = self.basename
+ self.context.name = self.name
+ self.context.path = self.path
+ self.context.route = self.path
+ self.context.template = self.template_path
+
+ 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)
+
+ self.set_properties_via_comments()
+
+ def set_properties_via_comments(self):
+ for comment, (context_key, value) in COMMENT_PROPERTY_KEY_VALUE_MAP.items():
+ comment_tag = f""
+ if comment_tag in self.source:
+ self.context[context_key] = value
+ click.echo(f'\n⚠️ DEPRECATION WARNING: {comment_tag} will be deprecated on 2021-12-31.')
+ click.echo(f'Please remove it from {self.template_path} in {self.app}')
+
+ 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 Exception:
+ 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.basename + '.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.basename + '.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):
+ if not self.template_path.endswith(('.md', '.html')):
+ return
+
+ 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.context.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):
+ super().set_missing_values()
+ # 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')
diff --git a/frappe/website/page_renderers/web_form.py b/frappe/website/page_renderers/web_form.py
new file mode 100644
index 0000000000..786aeef3d1
--- /dev/null
+++ b/frappe/website/page_renderers/web_form.py
@@ -0,0 +1,10 @@
+from frappe.website.page_renderers.document_page import DocumentPage
+import frappe
+
+class WebFormPage(DocumentPage):
+ def can_render(self):
+ webform_name = frappe.db.exists("Web Form", {'route': self.path}, cache=True)
+ if webform_name:
+ self.doctype = 'Web Form'
+ self.docname = webform_name
+ return bool(webform_name)
diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py
new file mode 100644
index 0000000000..bedd9f19ae
--- /dev/null
+++ b/frappe/website/path_resolver.py
@@ -0,0 +1,155 @@
+import re
+import click
+
+from werkzeug.routing import Rule
+
+import frappe
+from frappe.website.page_renderers.document_page import DocumentPage
+from frappe.website.page_renderers.list_page import ListPage
+from frappe.website.page_renderers.not_found_page import NotFoundPage
+from frappe.website.page_renderers.print_page import PrintPage
+from frappe.website.page_renderers.redirect_page import RedirectPage
+from frappe.website.page_renderers.static_page import StaticPage
+from frappe.website.page_renderers.template_page import TemplatePage
+from frappe.website.page_renderers.web_form import WebFormPage
+from frappe.website.router import evaluate_dynamic_routes
+from frappe.website.utils import can_cache, get_home_page
+
+
+class PathResolver():
+ def __init__(self, path):
+ self.path = path.strip('/ ')
+
+ def resolve(self):
+ '''Returns endpoint and a renderer instance that can render the endpoint'''
+ request = frappe._dict()
+ if hasattr(frappe.local, 'request'):
+ request = frappe.local.request or request
+
+ # check if the request url is in 404 list
+ if request.url and can_cache() and frappe.cache().hget('website_404', request.url):
+ return self.path, NotFoundPage(self.path)
+
+ try:
+ resolve_redirect(self.path, request.query_string)
+ except frappe.Redirect:
+ return frappe.flags.redirect_location, RedirectPage(self.path)
+
+ endpoint = resolve_path(self.path)
+ custom_renderers = self.get_custom_page_renderers()
+ renderers = custom_renderers + [StaticPage, WebFormPage, TemplatePage, ListPage, DocumentPage, PrintPage, NotFoundPage]
+
+ for renderer in renderers:
+ renderer_instance = renderer(endpoint, 200)
+ if renderer_instance.can_render():
+ return endpoint, renderer_instance
+
+ return endpoint, NotFoundPage(endpoint)
+
+ def is_valid_path(self):
+ _endpoint, renderer_instance = self.resolve()
+ return not isinstance(renderer_instance, NotFoundPage)
+
+ @staticmethod
+ def get_custom_page_renderers():
+ custom_renderers = []
+ for renderer_path in frappe.get_hooks('page_renderer') or []:
+ try:
+ renderer = frappe.get_attr(renderer_path)
+ if not hasattr(renderer, 'can_render'):
+ click.echo(f'{renderer.__name__} does not have can_render method')
+ continue
+ if not hasattr(renderer, 'render'):
+ click.echo(f'{renderer.__name__} does not have render method')
+ continue
+
+ custom_renderers.append(renderer)
+
+ except Exception:
+ click.echo(f'Failed to load page renderer. Import path: {renderer_path}')
+
+ return custom_renderers
+
+
+
+def resolve_redirect(path, query_string=None):
+ '''
+ Resolve redirects from hooks
+
+ Example:
+
+ website_redirect = [
+ # absolute location
+ {"source": "/from", "target": "https://mysite/from"},
+
+ # relative location
+ {"source": "/from", "target": "/main"},
+
+ # use regex
+ {"source": r"/from/(.*)", "target": r"/main/\1"}
+ # use r as a string prefix if you use regex groups or want to escape any string literal
+ ]
+ '''
+ redirects = frappe.get_hooks('website_redirects')
+ redirects += frappe.db.get_all('Website Route Redirect', ['source', 'target'])
+
+ if not redirects: return
+
+ redirect_to = frappe.cache().hget('website_redirects', path)
+
+ if redirect_to:
+ frappe.flags.redirect_location = redirect_to
+ raise frappe.Redirect
+
+ for rule in redirects:
+ pattern = rule['source'].strip('/ ') + '$'
+ 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_to_match, redirect_to)
+ raise frappe.Redirect
+
+
+def resolve_path(path):
+ if not path:
+ path = "index"
+
+ if path.endswith('.html'):
+ path = path[:-5]
+
+ if path == "index":
+ path = get_home_page()
+
+ frappe.local.path = path
+
+ if path != "index":
+ path = resolve_from_map(path)
+
+ return path
+
+def resolve_from_map(path):
+ '''transform dynamic route to a static one from hooks and route defined in doctype'''
+ rules = [Rule(r["from_route"], endpoint=r["to_route"], defaults=r.get("defaults"))
+ for r in get_website_rules()]
+
+ return evaluate_dynamic_routes(rules, path) or path
+
+def get_website_rules():
+ '''Get website route rules from hooks and DocType route'''
+ def _get():
+ rules = frappe.get_hooks("website_route_rules")
+ for d in frappe.get_all('DocType', 'name, route', dict(has_web_view=1)):
+ if d.route:
+ rules.append(dict(from_route = '/' + d.route.strip('/'), to_route=d.name))
+
+ return rules
+
+ if frappe.local.dev_server:
+ # dont cache in development
+ return _get()
+
+ return frappe.cache().get_value('website_route_rules', _get)
diff --git a/frappe/website/purifycss.py b/frappe/website/purifycss.py
deleted file mode 100644
index bac68b881b..0000000000
--- a/frappe/website/purifycss.py
+++ /dev/null
@@ -1,42 +0,0 @@
-'''
-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)
diff --git a/frappe/website/redirect.py b/frappe/website/redirect.py
deleted file mode 100644
index 3194895d95..0000000000
--- a/frappe/website/redirect.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import re, frappe
-
-def resolve_redirect(path):
- '''
- Resolve redirects from hooks
-
- Example:
-
- website_redirect = [
- # absolute location
- {"source": "/from", "target": "https://mysite/from"},
-
- # relative location
- {"source": "/from", "target": "/main"},
-
- # use regex
- {"source": r"/from/(.*)", "target": r"/main/\1"}
- # use r as a string prefix if you use regex groups or want to escape any string literal
- ]
- '''
- redirects = frappe.get_hooks('website_redirects')
- redirects += frappe.db.get_all('Website Route Redirect', ['source', 'target'])
-
- if not redirects: return
-
- redirect_to = frappe.cache().hget('website_redirects', path)
-
- if redirect_to:
- frappe.flags.redirect_location = redirect_to
- raise frappe.Redirect
-
- for rule in redirects:
- pattern = rule['source'].strip('/ ') + '$'
- if re.match(pattern, path):
- redirect_to = re.sub(pattern, rule['target'], path)
- frappe.flags.redirect_location = redirect_to
- frappe.cache().hset('website_redirects', path, redirect_to)
- raise frappe.Redirect
-
diff --git a/frappe/website/render.py b/frappe/website/render.py
deleted file mode 100644
index 2b4a5e2dab..0000000000
--- a/frappe/website/render.py
+++ /dev/null
@@ -1,369 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-import frappe
-from frappe import _
-import frappe.sessions
-from frappe.utils import cstr
-import os, mimetypes, json
-import re
-
-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
-
-class PageNotFoundError(Exception): pass
-
-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
-
-def build_response(path, data, http_status_code, headers=None):
- # build response
- response = Response()
- response.data = set_content_type(response, data, path)
- response.status_code = http_status_code
- response.headers["X-Page-Name"] = path.encode("ascii", errors="xmlcharrefreplace")
- response.headers["X-From-Cache"] = frappe.local.response.from_cache or False
-
- add_preload_headers(response)
- if headers:
- for key, val in headers.items():
- response.headers[key] = val.encode("ascii", errors="xmlcharrefreplace")
-
- return response
-
-
-def add_preload_headers(response):
- from bs4 import BeautifulSoup
-
- try:
- preload = []
- soup = BeautifulSoup(response.data, "lxml")
- for elem in soup.find_all('script', src=re.compile(".*")):
- preload.append(("script", elem.get("src")))
-
- for elem in soup.find_all('link', rel="stylesheet"):
- preload.append(("style", elem.get("href")))
-
- links = []
- for _type, link in preload:
- links.append("<{}>; rel=preload; as={}".format(link, _type))
-
- if links:
- response.headers["Link"] = ",".join(links)
- except Exception:
- import traceback
- 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
-
- 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
-
- 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"
-
- if path.endswith('.html'):
- path = path[:-5]
-
- if path == "index":
- path = get_home_page()
-
- frappe.local.path = path
-
- if path != "index":
- path = resolve_from_map(path)
-
- return path
-
-def resolve_from_map(path):
- '''transform dynamic route to a static one from hooks and route defined in doctype'''
- rules = [Rule(r["from_route"], endpoint=r["to_route"], defaults=r.get("defaults"))
- for r in get_website_rules()]
-
- return evaluate_dynamic_routes(rules, path) or path
-
-def get_website_rules():
- '''Get website route rules from hooks and DocType route'''
- def _get():
- rules = frappe.get_hooks("website_route_rules")
- for d in frappe.get_all('DocType', 'name, route', dict(has_web_view=1)):
- if d.route:
- rules.append(dict(from_route = '/' + d.route.strip('/'), to_route=d.name))
-
- return rules
-
- if frappe.local.dev_server:
- # dont cache in development
- return _get()
-
- return frappe.cache().get_value('website_route_rules', _get)
-
-def set_content_type(response, data, path):
- if isinstance(data, dict):
- response.mimetype = 'application/json'
- response.charset = 'utf-8'
- data = json.dumps(data)
- return data
-
- response.mimetype = 'text/html'
- response.charset = 'utf-8'
-
- if "." in path:
- content_type, encoding = mimetypes.guess_type(path)
- if content_type:
- response.mimetype = content_type
- if encoding:
- response.charset = encoding
-
- 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)
-
-def render_403(e, pathname):
- frappe.local.message = cstr(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
-
diff --git a/frappe/website/router.py b/frappe/website/router.py
index aa74d140c1..a9e2f68fe5 100644
--- a/frappe/website/router.py
+++ b/frappe/website/router.py
@@ -6,131 +6,9 @@ import os
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 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.
- The `www` folder is also a part of generator **Web Page**.
-
- The only exceptions are `/about` and `/contact` these will be searched in Web Pages
- first before checking the standard pages."""
-
- if path not in ("about", "contact"):
- context = get_page_info_from_template(path)
- if context:
- return context
- return get_page_context_from_doctype(path)
- else:
- context = get_page_context_from_doctype(path)
- if context:
- return context
- return get_page_info_from_template(path)
-
-def get_page_context(path):
- page_context = None
- if can_cache():
- page_context_cache = frappe.cache().hget("page_context", path) or {}
- page_context = page_context_cache.get(frappe.local.lang, None)
-
- if not page_context:
- page_context = make_page_context(path)
- if can_cache(page_context.no_cache):
- page_context_cache[frappe.local.lang] = page_context
- frappe.cache().hset("page_context", path, page_context_cache)
-
- return page_context
-
-def make_page_context(path):
- context = resolve_route(path)
- if not context:
- raise frappe.PageDoesNotExistError
-
- context.doctype = context.ref_doctype
-
- if context.page_title:
- context.title = context.page_title
-
- context.pathname = frappe.local.path
-
- return context
-
-def get_page_info_from_template(path):
- '''Return page_info from path'''
- for app in frappe.get_installed_apps(frappe_last=True):
- app_path = frappe.get_app_path(app)
-
- folders = get_start_folders()
-
- for start in folders:
- search_path = os.path.join(app_path, start, path)
- options = (search_path, search_path + '.html', search_path + '.md',
- search_path + '/index.html', search_path + '/index.md')
- for o in options:
- option = frappe.as_unicode(o)
- if os.path.exists(option) and not os.path.isdir(option):
- return get_page_info(option, app, start, app_path=app_path)
-
- return None
-
-def get_page_context_from_doctype(path):
- page_info = get_page_info_from_doctypes(path)
- if not page_info:
- page_info = get_page_info_from_web_page_with_dynamic_routes(path)
-
- if page_info:
- return frappe.get_doc(page_info.get("doctype"),
- page_info.get("name")).get_page_info()
-
-def clear_sitemap():
- delete_page_cache("*")
-
-def get_all_page_context_from_doctypes():
- '''
- Get all doctype generated routes (for sitemap.xml)
- '''
- routes = frappe.cache().get_value("website_generator_routes")
- if not routes:
- routes = get_page_info_from_doctypes()
- frappe.cache().set_value("website_generator_routes", routes)
-
- return routes
-
-def get_page_info_from_doctypes(path=None):
- '''
- Find a document with matching `route` from all doctypes with `has_web_view`=1
- '''
- routes = {}
- for doctype in get_doctypes_with_web_view():
- filters = {}
- controller = get_controller(doctype)
- meta = frappe.get_meta(doctype)
-
- condition_field = (meta.is_published_field or
- # custom doctypes dont have controllers and no website attribute
- (controller.website.condition_field if not meta.custom else None))
-
- if condition_field:
- filters[condition_field] = 1
-
- if path:
- filters['route'] = path
-
- try:
- for r in frappe.get_all(doctype, fields = ['name', 'route', 'modified'],
- filters = filters, limit = 1):
-
- routes[r.route] = {"doctype": doctype, "name": r.name, "modified": r.modified}
-
- # just want one path, return it!
- if path:
- return routes[r.route]
- except Exception as e:
- if not frappe.db.is_missing_column(e): raise e
-
- return routes
-
def get_page_info_from_web_page_with_dynamic_routes(path):
'''
Query Web Page with dynamic_route = 1 and evaluate if any of the routes match
@@ -229,7 +107,7 @@ def get_page_info(path, app, start, basepath=None, app_path=None, fname=None):
if basepath is None:
basepath = os.path.dirname(path)
- page_name, extn = fname.rsplit(".", 1)
+ page_name, extn = os.path.splitext(fname)
# add website route
page_info = frappe._dict()
@@ -265,14 +143,12 @@ def get_page_info(path, app, start, basepath=None, app_path=None, fname=None):
# get the source
setup_source(page_info)
- # extract properties from HTML comments
- load_properties_from_source(page_info)
+ if not page_info.title:
+ page_info.title = extract_title(page_info.source, page_info.route)
# extract properties from controller attributes
load_properties_from_controller(page_info)
- page_info.build_version = frappe.utils.get_build_version()
-
return page_info
def get_frontmatter(string):
@@ -375,47 +251,6 @@ def setup_index(page_info):
with open(index_txt_path, 'r') as f:
page_info.index = f.read().splitlines()
-def load_properties_from_source(page_info):
- '''Load properties like no_cache, title from source html'''
-
- if not page_info.title:
- page_info.title = extract_title(page_info.source, page_info.route)
-
- base_template = extract_comment_tag(page_info.source, 'base_template')
- if base_template:
- page_info.base_template = base_template
-
- if (page_info.base_template
- and "{%- extends" not in page_info.source
- and "{% extends" not in page_info.source
- and "
+ Test Page
+" not in page_info.source):
- page_info.source = '''{{% extends "{0}" %}}
- {{% block page_content %}}{1}{{% endblock %}}'''.format(page_info.base_template, page_info.source)
-
- if "" in page_info.source:
- page_info.no_breadcrumbs = 1
-
- if "" in page_info.source:
- page_info.show_sidebar = 1
-
- if "" in page_info.source:
- page_info.add_breadcrumbs = 1
-
- if "" in page_info.source:
- page_info.no_header = 1
-
- if "" in page_info.source:
- page_info.add_next_prev_links = 1
-
- if "" in page_info.source:
- page_info.no_cache = 1
-
- if "" in page_info.source:
- page_info.sitemap = 0
-
- if "" in page_info.source:
- page_info.sitemap = 1
-
def load_properties_from_controller(page_info):
if not page_info.controller: return
@@ -432,8 +267,10 @@ def get_doctypes_with_web_view():
def _get():
installed_apps = frappe.get_installed_apps()
doctypes = frappe.get_hooks("website_generators")
- doctypes += [d.name for d in frappe.get_all('DocType', 'name, module',
- dict(has_web_view=1)) if frappe.local.module_app[frappe.scrub(d.module)] in installed_apps]
+ doctypes_with_web_view = frappe.get_all('DocType', fields=['name', 'module'],
+ filters=dict(has_web_view=1))
+ module_app_map = frappe.local.module_app
+ doctypes += [d.name for d in doctypes_with_web_view if module_app_map[frappe.scrub(d.module)] in installed_apps]
return doctypes
return frappe.cache().get_value('doctypes_with_web_view', _get)
diff --git a/frappe/website/serve.py b/frappe/website/serve.py
new file mode 100644
index 0000000000..fe7fc77064
--- /dev/null
+++ b/frappe/website/serve.py
@@ -0,0 +1,29 @@
+import frappe
+from frappe.website.page_renderers.error_page import ErrorPage
+from frappe.website.page_renderers.not_permitted_page import NotPermittedPage
+from frappe.website.page_renderers.redirect_page import RedirectPage
+from frappe.website.path_resolver import PathResolver
+
+
+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
+
+ try:
+ path_resolver = PathResolver(path)
+ endpoint, renderer_instance = path_resolver.resolve()
+ response = renderer_instance.render()
+ except frappe.Redirect:
+ return RedirectPage(endpoint or path, http_status_code).render()
+ except frappe.PermissionError as e:
+ response = NotPermittedPage(endpoint, http_status_code, exception=e).render()
+ except Exception as e:
+ response = ErrorPage(exception=e).render()
+
+ return response
+
+def get_response_content(path=None, http_status_code=200):
+ response = get_response(path, http_status_code)
+ return str(response.data, 'utf-8')
diff --git a/frappe/website/utils.py b/frappe/website/utils.py
index aa98595e2d..0f5f182ea2 100644
--- a/frappe/website/utils.py
+++ b/frappe/website/utils.py
@@ -1,9 +1,18 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-import re
+import json
+import mimetypes
import os
-import frappe
+import re
+from functools import wraps
+import yaml
+from six import iteritems
+from werkzeug.wrappers import Response
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
from frappe.utils import md_to_html
@@ -128,14 +137,8 @@ def get_home_page_via_hooks():
return home_page
-def is_signup_enabled():
- if getattr(frappe.local, "is_signup_enabled", None) is None:
- frappe.local.is_signup_enabled = True
- if frappe.utils.cint(frappe.db.get_value("Website Settings",
- "Website Settings", "disable_signup")):
- frappe.local.is_signup_enabled = False
-
- return frappe.local.is_signup_enabled
+def is_signup_disabled():
+ return frappe.db.get_single_value('Website Settings', 'disable_signup', True)
def cleanup_page_name(title):
"""make page name from title"""
@@ -145,91 +148,15 @@ def cleanup_page_name(title):
name = title.lower()
name = re.sub(r'[~!@#$%^&*+()<>,."\'\?]', '', name)
name = re.sub('[:/]', '-', name)
-
name = '-'.join(name.split())
-
# replace repeating hyphens
name = re.sub(r"(-)\1+", r"\1", name)
-
return name[:140]
-def get_shade(color, percent):
- color, color_format = detect_color_format(color)
- r, g, b, a = color
-
- avg = (float(int(r) + int(g) + int(b)) / 3)
- # switch dark and light shades
- if avg > 128:
- percent = -percent
-
- # stronger diff for darker shades
- if percent < 25 and avg < 64:
- percent = percent * 2
-
- new_color = []
- for channel_value in (r, g, b):
- new_color.append(get_shade_for_channel(channel_value, percent))
-
- r, g, b = new_color
-
- return format_color(r, g, b, a, color_format)
-
-
-def detect_color_format(color):
- if color.startswith("rgba"):
- color_format = "rgba"
- color = [c.strip() for c in color[5:-1].split(",")]
-
- elif color.startswith("rgb"):
- color_format = "rgb"
- color = [c.strip() for c in color[4:-1].split(",")] + [1]
-
- else:
- # assume hex
- color_format = "hex"
-
- if color.startswith("#"):
- color = color[1:]
-
- if len(color) == 3:
- # hex in short form like #fff
- color = "{0}{0}{1}{1}{2}{2}".format(*tuple(color))
-
- color = [int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16), 1]
-
- return color, color_format
-
-
-def get_shade_for_channel(channel_value, percent):
- v = int(channel_value) + int(int('ff', 16) * (float(percent)/100))
- if v < 0:
- v=0
- if v > 255:
- v=255
-
- return v
-
-
-def format_color(r, g, b, a, color_format):
- if color_format == "rgba":
- return "rgba({0}, {1}, {2}, {3})".format(r, g, b, a)
-
- elif color_format == "rgb":
- return "rgb({0}, {1}, {2})".format(r, g, b)
-
- else:
- # assume hex
- return "#{0}{1}{2}".format(convert_to_hex(r), convert_to_hex(g), convert_to_hex(b))
-
-
-def convert_to_hex(channel_value):
- h = hex(channel_value)[2:]
-
- if len(h) < 2:
- h = "0" + h
-
- return h
+def get_shade(color, percent=None):
+ frappe.msgprint(_('get_shade method has been deprecated.'))
+ return color
def abs_url(path):
"""Deconstructs and Reconstructs a URL into an absolute URL or a URL relative from root '/'"""
@@ -359,25 +286,6 @@ def extract_comment_tag(source, tag):
return None
-def add_missing_headers():
- '''Walk and add missing headers in docs (to be called from bench execute)'''
- path = frappe.get_app_path('erpnext', 'docs')
- for basepath, folders, files in os.walk(path):
- for fname in files:
- if fname.endswith('.md'):
- with open(os.path.join(basepath, fname), 'r') as f:
- content = frappe.as_unicode(f.read())
-
- if not content.startswith('# ') and not '
+{next}
+{% endblock %}
diff --git a/frappe/www/_test/_test_folder/_test_page.js b/frappe/www/_test/_test_folder/_test_page.js
new file mode 100644
index 0000000000..6e0c1f3a87
--- /dev/null
+++ b/frappe/www/_test/_test_folder/_test_page.js
@@ -0,0 +1 @@
+console.log('test data');
\ No newline at end of file
diff --git a/frappe/www/_test/_test_folder/_test_page.py b/frappe/www/_test/_test_folder/_test_page.py
new file mode 100644
index 0000000000..1813a06bac
--- /dev/null
+++ b/frappe/www/_test/_test_folder/_test_page.py
@@ -0,0 +1,3 @@
+def get_context(context):
+ context.base_template_path = 'frappe/templates/test/_test_base.html'
+ context.add_breadcrumbs = 1
diff --git a/frappe/www/_test/_test_folder/_test_toc.md b/frappe/www/_test/_test_folder/_test_toc.md
new file mode 100644
index 0000000000..02cc3c82be
--- /dev/null
+++ b/frappe/www/_test/_test_folder/_test_toc.md
@@ -0,0 +1,19 @@
+---
+title: Test TOC
+add_breadcrumbs: 1
+show_sidebar: 0
+
+metatags:
+ description: Test Description.
+ keywords: Frappe Framework.
+---
+
+# Level 1
+
+## Level 1.1
+
+## Level 1.2
+
+## Level 1.3
+
+### Level 1.3.1
diff --git a/frappe/www/_test/_test_folder/index.md b/frappe/www/_test/_test_folder/index.md
new file mode 100644
index 0000000000..1a5a9e7f81
--- /dev/null
+++ b/frappe/www/_test/_test_folder/index.md
@@ -0,0 +1,9 @@
+---
+title: Test TOC
+add_breadcrumbs: 1
+show_sidebar: 1
+---
+
+# Index
+
+{index}
\ No newline at end of file
diff --git a/frappe/www/_test/_test_folder/new.csv/__init__.py b/frappe/www/_test/_test_folder/new.csv/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/www/_test/_test_folder/new.csv/index.html b/frappe/www/_test/_test_folder/new.csv/index.html
new file mode 100644
index 0000000000..7a1bb69558
--- /dev/null
+++ b/frappe/www/_test/_test_folder/new.csv/index.html
@@ -0,0 +1,12 @@
+
+
+