* perf: defer translation.py imports This indirectly imports babel which isn't really required most of the time. * perf: defer gzip import * perf: move validate_and_sanitize_search_inputs This causes all sorts of indirect imports and increases memory usage * perf: defer requests module imports * perf: defer system settings import * perf: defer LOG_DOCTYPES import Causes many indirect imports * perf: defer update_site_config * perf: defer notifications import * perf: remove unused import * perf: defer safe exec import * test: memory usage overhead
314 lines
8.7 KiB
Python
314 lines
8.7 KiB
Python
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: MIT. See LICENSE
|
|
import io
|
|
import os
|
|
|
|
import frappe
|
|
from frappe import _
|
|
from frappe.build import scrub_html_template
|
|
from frappe.model.meta import Meta
|
|
from frappe.model.utils import render_include
|
|
from frappe.modules import get_module_path, load_doctype_module, scrub
|
|
from frappe.utils import get_html_format
|
|
from frappe.utils.data import get_link_to_form
|
|
|
|
ASSET_KEYS = (
|
|
"__js",
|
|
"__css",
|
|
"__list_js",
|
|
"__calendar_js",
|
|
"__map_js",
|
|
"__linked_with",
|
|
"__messages",
|
|
"__print_formats",
|
|
"__workflow_docs",
|
|
"__form_grid_templates",
|
|
"__listview_template",
|
|
"__tree_js",
|
|
"__dashboard",
|
|
"__kanban_column_fields",
|
|
"__templates",
|
|
"__custom_js",
|
|
"__custom_list_js",
|
|
)
|
|
|
|
|
|
def get_meta(doctype, cached=True) -> "FormMeta":
|
|
# don't cache for developer mode as js files, templates may be edited
|
|
cached = cached and not frappe.conf.developer_mode
|
|
if cached:
|
|
meta = frappe.cache.hget("doctype_form_meta", doctype)
|
|
if not meta:
|
|
# Cache miss - explicitly get meta from DB to avoid
|
|
meta = FormMeta(doctype, cached=False)
|
|
frappe.cache.hset("doctype_form_meta", doctype, meta)
|
|
else:
|
|
meta = FormMeta(doctype)
|
|
|
|
if frappe.local.lang != "en":
|
|
meta.set_translations(frappe.local.lang)
|
|
|
|
return meta
|
|
|
|
|
|
class FormMeta(Meta):
|
|
def __init__(self, doctype, *, cached=True):
|
|
self.__dict__.update(frappe.get_meta(doctype, cached=cached).__dict__)
|
|
self.load_assets()
|
|
|
|
def load_assets(self):
|
|
if self.get("__assets_loaded", False):
|
|
return
|
|
|
|
self.add_search_fields()
|
|
self.add_linked_document_type()
|
|
|
|
if not self.istable:
|
|
self.add_code()
|
|
self.add_custom_script()
|
|
self.load_print_formats()
|
|
self.load_workflows()
|
|
self.load_templates()
|
|
self.load_dashboard()
|
|
self.load_kanban_meta()
|
|
|
|
self.set("__assets_loaded", True)
|
|
|
|
def as_dict(self, no_nulls=False):
|
|
d = super().as_dict(no_nulls=no_nulls)
|
|
|
|
for k in ASSET_KEYS:
|
|
d[k] = self.get(k)
|
|
|
|
# d['fields'] = d.get('fields', [])
|
|
|
|
for i, df in enumerate(d.get("fields") or []):
|
|
for k in ("search_fields", "is_custom_field", "linked_document_type"):
|
|
df[k] = self.get("fields")[i].get(k)
|
|
|
|
return d
|
|
|
|
def add_code(self):
|
|
if self.custom:
|
|
return
|
|
|
|
path = os.path.join(get_module_path(self.module), "doctype", scrub(self.name))
|
|
|
|
def _get_path(fname):
|
|
return os.path.join(path, scrub(fname))
|
|
|
|
system_country = frappe.get_system_settings("country")
|
|
|
|
self._add_code(_get_path(self.name + ".js"), "__js")
|
|
if system_country:
|
|
self._add_code(_get_path(os.path.join("regional", system_country + ".js")), "__js")
|
|
|
|
self._add_code(_get_path(self.name + ".css"), "__css")
|
|
self._add_code(_get_path(self.name + "_list.js"), "__list_js")
|
|
if system_country:
|
|
self._add_code(_get_path(os.path.join("regional", system_country + "_list.js")), "__list_js")
|
|
|
|
self._add_code(_get_path(self.name + "_calendar.js"), "__calendar_js")
|
|
self._add_code(_get_path(self.name + "_tree.js"), "__tree_js")
|
|
|
|
listview_template = _get_path(self.name + "_list.html")
|
|
if os.path.exists(listview_template):
|
|
self.set("__listview_template", get_html_format(listview_template))
|
|
|
|
self.add_code_via_hook("doctype_js", "__js")
|
|
self.add_code_via_hook("doctype_list_js", "__list_js")
|
|
self.add_code_via_hook("doctype_tree_js", "__tree_js")
|
|
self.add_code_via_hook("doctype_calendar_js", "__calendar_js")
|
|
self.add_html_templates(path)
|
|
|
|
def _add_code(self, path, fieldname):
|
|
js = get_js(path)
|
|
if js:
|
|
comment = f"\n\n/* Adding {path} */\n\n"
|
|
sourceURL = f"\n\n//# sourceURL={scrub(self.name) + fieldname}"
|
|
self.set(fieldname, (self.get(fieldname) or "") + comment + js + sourceURL)
|
|
|
|
def add_html_templates(self, path):
|
|
if self.custom:
|
|
return
|
|
templates = dict()
|
|
for fname in os.listdir(path):
|
|
if fname.endswith(".html"):
|
|
with open(os.path.join(path, fname), encoding="utf-8") as f:
|
|
templates[fname.split(".", 1)[0]] = scrub_html_template(f.read())
|
|
|
|
self.set("__templates", templates or None)
|
|
|
|
def add_code_via_hook(self, hook, fieldname):
|
|
for path in get_code_files_via_hooks(hook, self.name):
|
|
self._add_code(path, fieldname)
|
|
|
|
def add_custom_script(self):
|
|
"""embed all require files"""
|
|
# custom script
|
|
client_scripts = (
|
|
frappe.get_all(
|
|
"Client Script",
|
|
filters={"dt": self.name, "enabled": 1},
|
|
fields=["name", "script", "view"],
|
|
order_by="creation asc",
|
|
)
|
|
or ""
|
|
)
|
|
|
|
list_script = ""
|
|
form_script = ""
|
|
for script in client_scripts:
|
|
if not script.script:
|
|
continue
|
|
|
|
if script.view == "List":
|
|
list_script += f"""
|
|
// {script.name}
|
|
{script.script}
|
|
|
|
"""
|
|
|
|
elif script.view == "Form":
|
|
form_script += f"""
|
|
// {script.name}
|
|
{script.script}
|
|
|
|
"""
|
|
|
|
file = scrub(self.name)
|
|
form_script += f"\n\n//# sourceURL={file}__custom_js"
|
|
list_script += f"\n\n//# sourceURL={file}__custom_list_js"
|
|
|
|
self.set("__custom_js", form_script)
|
|
self.set("__custom_list_js", list_script)
|
|
|
|
def add_search_fields(self):
|
|
"""add search fields found in the doctypes indicated by link fields' options"""
|
|
for df in self.get("fields", {"fieldtype": "Link", "options": ["!=", "[Select]"]}):
|
|
if df.options:
|
|
try:
|
|
search_fields = frappe.get_meta(df.options).search_fields
|
|
except frappe.DoesNotExistError:
|
|
self._show_missing_doctype_msg(df)
|
|
|
|
if search_fields:
|
|
search_fields = search_fields.split(",")
|
|
df.search_fields = [sf.strip() for sf in search_fields]
|
|
|
|
def _show_missing_doctype_msg(self, df):
|
|
# A link field is referring to non-existing doctype, this usually happens when
|
|
# customizations are removed or some custom app is removed but hasn't cleaned
|
|
# up after itself.
|
|
frappe.clear_last_message()
|
|
|
|
msg = _("Field {0} is referring to non-existing doctype {1}.").format(
|
|
frappe.bold(df.fieldname), frappe.bold(df.options)
|
|
)
|
|
|
|
if df.get("is_custom_field"):
|
|
custom_field_link = get_link_to_form("Custom Field", df.name)
|
|
msg += " " + _("Please delete the field from {0} or add the required doctype.").format(
|
|
custom_field_link
|
|
)
|
|
|
|
frappe.throw(msg, title=_("Missing DocType"))
|
|
|
|
def add_linked_document_type(self):
|
|
for df in self.get("fields", {"fieldtype": "Link"}):
|
|
if df.options:
|
|
try:
|
|
df.linked_document_type = frappe.get_meta(df.options).document_type
|
|
except frappe.DoesNotExistError:
|
|
self._show_missing_doctype_msg(df)
|
|
|
|
def load_print_formats(self):
|
|
print_formats = frappe.db.sql(
|
|
"""select * FROM `tabPrint Format`
|
|
WHERE doc_type=%s AND docstatus<2 and disabled=0""",
|
|
(self.name,),
|
|
as_dict=1,
|
|
update={"doctype": "Print Format"},
|
|
)
|
|
|
|
self.set("__print_formats", print_formats)
|
|
|
|
def load_workflows(self):
|
|
# get active workflow
|
|
workflow_name = self.get_workflow()
|
|
workflow_docs = []
|
|
|
|
if workflow_name and frappe.db.exists("Workflow", workflow_name):
|
|
workflow = frappe.get_doc("Workflow", workflow_name)
|
|
workflow_docs.append(workflow)
|
|
|
|
for d in workflow.get("states"):
|
|
workflow_docs.append(frappe.get_doc("Workflow State", d.state))
|
|
|
|
self.set("__workflow_docs", workflow_docs)
|
|
|
|
def load_templates(self):
|
|
if not self.custom:
|
|
module = load_doctype_module(self.name)
|
|
app = module.__name__.split(".", 1)[0]
|
|
templates = {}
|
|
if hasattr(module, "form_grid_templates"):
|
|
for key, path in module.form_grid_templates.items():
|
|
templates[key] = get_html_format(frappe.get_app_path(app, path))
|
|
|
|
self.set("__form_grid_templates", templates)
|
|
|
|
def set_translations(self, lang):
|
|
from frappe.translate import extract_messages_from_code, make_dict_from_messages
|
|
|
|
self.set("__messages", frappe.get_lang_dict("doctype", self.name))
|
|
|
|
# set translations for grid templates
|
|
if self.get("__form_grid_templates"):
|
|
for content in self.get("__form_grid_templates").values():
|
|
messages = extract_messages_from_code(content)
|
|
messages = make_dict_from_messages(messages)
|
|
self.get("__messages").update(messages)
|
|
|
|
def load_dashboard(self):
|
|
self.set("__dashboard", self.get_dashboard_data())
|
|
|
|
def load_kanban_meta(self):
|
|
self.load_kanban_column_fields()
|
|
|
|
def load_kanban_column_fields(self):
|
|
try:
|
|
values = frappe.get_list(
|
|
"Kanban Board", fields=["field_name"], filters={"reference_doctype": self.name}
|
|
)
|
|
|
|
fields = [x["field_name"] for x in values]
|
|
fields = list(set(fields))
|
|
self.set("__kanban_column_fields", fields)
|
|
except frappe.PermissionError:
|
|
# no access to kanban board
|
|
pass
|
|
|
|
|
|
def get_code_files_via_hooks(hook, name):
|
|
code_files = []
|
|
for app_name in frappe.get_installed_apps():
|
|
code_hook = frappe.get_hooks(hook, default={}, app_name=app_name)
|
|
if not code_hook:
|
|
continue
|
|
|
|
files = code_hook.get(name, [])
|
|
if not isinstance(files, list):
|
|
files = [files]
|
|
|
|
for file in files:
|
|
path = frappe.get_app_path(app_name, *file.strip("/").split("/"))
|
|
code_files.append(path)
|
|
|
|
return code_files
|
|
|
|
|
|
def get_js(path):
|
|
js = frappe.read_file(path)
|
|
if js:
|
|
return render_include(js)
|