Merge branch 'develop' into autocomplete-control

This commit is contained in:
Saqib Ansari 2022-02-14 12:29:23 +05:30
parent 436543d212
commit 6b671af1ec
19 changed files with 178 additions and 86 deletions

View file

@ -17,6 +17,7 @@
"hide_days",
"hide_seconds",
"reqd",
"is_virtual",
"search_index",
"column_break_18",
"options",
@ -534,13 +535,19 @@
"fieldname": "show_dashboard",
"fieldtype": "Check",
"label": "Show Dashboard"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Virtual"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-04 11:56:19.812863",
"modified": "2022-02-14 11:56:19.812863",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -1078,6 +1078,9 @@ def validate_fields(meta):
field.fetch_from = field.fetch_from.strip('\n').strip()
def validate_data_field_type(docfield):
if docfield.get("is_virtual"):
return
if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label))
@ -1323,10 +1326,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
else:
raise
def check_fieldname_conflicts(doctype, fieldname):
def check_fieldname_conflicts(docfield):
"""Checks if fieldname conflicts with methods or properties"""
doc = frappe.get_doc({"doctype": doctype})
doc = frappe.get_doc({"doctype": docfield.dt})
available_objects = [x for x in dir(doc) if isinstance(x, str)]
property_list = [
x for x in available_objects if isinstance(getattr(type(doc), x, None), property)
@ -1334,9 +1336,10 @@ def check_fieldname_conflicts(doctype, fieldname):
method_list = [
x for x in available_objects if x not in property_list and callable(getattr(doc, x))
]
msg = _("Fieldname {0} conflicting with meta object").format(docfield.fieldname)
if fieldname in method_list + property_list:
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
if docfield.fieldname in method_list + property_list:
frappe.msgprint(msg, raise_exception=not docfield.is_virtual)
def clear_linked_doctype_cache():
frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled')

View file

@ -1,7 +1,7 @@
{
"actions": [],
"allow_import": 1,
"creation": "2022-01-25 14:14:01.217507",
"creation": "2013-01-10 16:34:01",
"description": "Adds a custom field to a DocType",
"doctype": "DocType",
"document_type": "Setup",
@ -34,6 +34,7 @@
"non_negative",
"reqd",
"unique",
"is_virtual",
"read_only",
"ignore_user_permissions",
"hidden",
@ -240,6 +241,12 @@
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
},
{
"default": "0",
"fieldname": "read_only",
@ -424,7 +431,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-01-29 15:42:21.885999",
"modified": "2022-02-14 15:42:21.885999",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",

View file

@ -54,7 +54,7 @@ class CustomField(Document):
old_fieldtype = self.db_get('fieldtype')
is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype)
if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
if not self.is_virtual and is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype))
if not self.fieldname:
@ -65,7 +65,7 @@ class CustomField(Document):
if not self.flags.ignore_validate:
from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts
check_fieldname_conflicts(self.dt, self.fieldname)
check_fieldname_conflicts(self)
def on_update(self):
if not frappe.flags.in_setup_wizard:

View file

@ -418,6 +418,9 @@ class CustomizeForm(Document):
return property_value
def validate_fieldtype_change(self, df, old_value, new_value):
if df.is_virtual:
return
allowed = self.allow_fieldtype_change(old_value, new_value)
if allowed:
old_value_length = cint(frappe.db.type_map.get(old_value)[1])
@ -430,7 +433,8 @@ class CustomizeForm(Document):
self.validate_fieldtype_length()
else:
self.flags.update_db = True
if not allowed:
else:
frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx))
def validate_fieldtype_length(self):
@ -558,7 +562,8 @@ docfield_properties = {
'allow_in_quick_entry': 'Check',
'hide_border': 'Check',
'hide_days': 'Check',
'hide_seconds': 'Check'
'hide_seconds': 'Check',
'is_virtual': 'Check',
}
doctype_link_properties = {

View file

@ -14,6 +14,7 @@
"non_negative",
"reqd",
"unique",
"is_virtual",
"in_list_view",
"in_standard_filter",
"in_global_search",
@ -115,6 +116,12 @@
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
},
{
"default": "0",
"fieldname": "in_list_view",
@ -436,7 +443,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-29 15:43:05.540546",
"modified": "2022-01-27 21:45:22.349776",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
@ -18,53 +18,19 @@ class PropertySetter(Document):
def validate(self):
self.validate_fieldtype_change()
if self.is_new():
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name)
# clear cache
frappe.clear_cache(doctype = self.doc_type)
def validate_fieldtype_change(self):
if self.field_name in not_allowed_fieldtype_change and \
self.property == 'fieldtype':
frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name))
def get_property_list(self, dt):
return frappe.db.get_all('DocField',
fields=['fieldname', 'label', 'fieldtype'],
filters={
'parent': dt,
'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
'fieldname': ['!=', '']
},
order_by='label asc',
as_dict=1
)
def get_setup_data(self):
return {
'doctypes': frappe.get_all("DocType", pluck="name"),
'dt_properties': self.get_property_list('DocType'),
'df_properties': self.get_property_list('DocField')
}
def get_field_ids(self):
return frappe.db.get_values(
"DocField",
filters={"parent": self.doc_type},
fieldname=["name", "fieldtype", "label", "fieldname"],
as_dict=True,
)
def get_defaults(self):
if not self.field_name:
return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0]
else:
return frappe.db.get_values(
"DocField",
filters={"fieldname": self.field_name, "parent": self.doc_type},
fieldname="*",
)[0]
if (
self.property == 'fieldtype'
and self.field_name in not_allowed_fieldtype_change
):
frappe.throw(
_("Field type cannot be changed for {0}").format(self.field_name)
)
def on_update(self):
if frappe.flags.in_patch:
@ -74,6 +40,7 @@ class PropertySetter(Document):
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
validate_fields_for_doctype(self.doc_type)
def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False,
validate_fields_for_doctype=True):
# WARNING: Ignores Permissions
@ -91,6 +58,7 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for
property_setter.insert()
return property_setter
def delete_property_setter(doc_type, property, field_name=None, row_name=None):
"""delete other property setters on this, if this is new"""
filters = dict(doc_type=doc_type, property=property)
@ -100,4 +68,3 @@ def delete_property_setter(doc_type, property, field_name=None, row_name=None):
filters["row_name"] = row_name
frappe.db.delete('Property Setter', filters)

View file

@ -67,7 +67,7 @@ class DBTable:
"""
get columns from docfields and custom fields
"""
fields = self.meta.get_fieldnames_with_value(True)
fields = self.meta.get_fieldnames_with_value(with_field_meta=True)
# optional fields like _comments
if not self.meta.get('istable'):
@ -85,6 +85,9 @@ class DBTable:
})
for field in fields:
if field.get("is_virtual"):
continue
self.columns[field.get('fieldname')] = DbColumn(
self,
field.get('fieldname'),

View file

@ -1,16 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import datetime
import frappe
import datetime
from frappe import _
from frappe.model import default_fields, table_fields, child_table_fields
from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields
from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count
from frappe.modules import load_doctype_module
from frappe.model import display_fieldtypes
from frappe.utils import (cint, flt, now, cstr, strip_html,
sanitize_html, sanitize_email, cast_fieldtype)
from frappe.utils import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html
from frappe.utils.html_utils import unescape_html
from frappe.model.docstatus import DocStatus
@ -254,7 +252,22 @@ class BaseDocument(object):
continue
df = self.meta.get_field(fieldname)
if df:
if df and df.get("is_virtual"):
from frappe.utils.safe_exec import get_safe_globals
if d[fieldname] is None:
if df.get("options"):
d[fieldname] = frappe.safe_eval(
code=df.get("options"),
eval_globals=get_safe_globals(),
eval_locals={"doc": self},
)
else:
_val = getattr(self, fieldname, None)
if _val and not callable(_val):
d[fieldname] = _val
elif df:
if df.fieldtype=="Check":
d[fieldname] = 1 if cint(d[fieldname]) else 0
@ -328,6 +341,7 @@ class BaseDocument(object):
def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False):
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str)
doc["doctype"] = self.doctype
for df in self.meta.get_table_fields():
children = self.get(df.fieldname) or []
doc[df.fieldname] = [

View file

@ -444,9 +444,16 @@ class Meta(Document):
self.permissions = [Document(d) for d in custom_perms]
def get_fieldnames_with_value(self, with_field_meta=False):
return [df if with_field_meta else df.fieldname \
for df in self.fields if df.fieldtype not in no_value_fields]
def is_value_field(docfield):
return not (
docfield.get("is_virtual")
or docfield.fieldtype in no_value_fields
)
if with_field_meta:
return [df for df in self.fields if is_value_field(df)]
return [df.fieldname for df in self.fields if is_value_field(df)]
def get_fields_to_check_permissions(self, user_permission_doctypes):
fields = self.get("fields", {

View file

@ -39,6 +39,9 @@ frappe.ui.form.Control = class BaseControl {
if (this.df.get_status) {
return this.df.get_status(this);
}
if (this.df.is_virtual) {
return "Read";
}
if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) {
// like in case of a dialog box
@ -52,7 +55,7 @@ frappe.ui.form.Control = class BaseControl {
if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console
return "None";
} else if (cint(this.df.read_only)) {
} else if (cint(this.df.read_only || this.df.is_virtual)) {
// eslint-disable-next-line
if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console
return "Read";

View file

@ -55,7 +55,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
// prepare a list of mandatory, bold and allow in quick entry fields
this.mandatory = fields.filter(df => {
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only);
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only && !df.is_virtual);
});
}

View file

@ -192,9 +192,18 @@ frappe.ui.form.ScriptManager = class ScriptManager {
}
function setup_add_fetch(df) {
if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Attach Image',
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1)
&& df.fetch_from && df.fetch_from.indexOf(".")!=-1) {
let is_read_only_field = (
['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Text Editor', 'Attach Image',
'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype)
|| df.read_only == 1
|| df.is_virtual == 1
)
if (
is_read_only_field
&& df.fetch_from
&& df.fetch_from.indexOf(".") != -1
) {
var parts = df.fetch_from.split(".");
me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent);
}

View file

@ -1672,7 +1672,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
frappe.model.is_value_type(field_doc) &&
field_doc.fieldtype !== "Read Only" &&
!field_doc.hidden &&
!field_doc.read_only
!field_doc.read_only &&
!field_doc.is_virtual
);
};

View file

@ -259,8 +259,16 @@ frappe.utils.xss_sanitise = function (string, options) {
'/': '/'
};
const REGEX_SCRIPT = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14
const REGEX_ALERT = /confirm\(.*\)|alert\(.*\)|prompt\(.*\)/gi; // captures alert, confirm, prompt
options = Object.assign({}, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty.
// Rule 3 - TODO: Check event handlers?
// script and alert should be checked first or else it will be escaped
if (options.strategies.includes('js')) {
sanitised = sanitised.replace(REGEX_SCRIPT, "");
sanitised = sanitised.replace(REGEX_ALERT, "");
}
// Rule 1
if (options.strategies.includes('html')) {
for (let char in HTML_ESCAPE_MAP) {
@ -270,11 +278,6 @@ frappe.utils.xss_sanitise = function (string, options) {
}
}
// Rule 3 - TODO: Check event handlers?
if (options.strategies.includes('js')) {
sanitised = sanitised.replace(REGEX_SCRIPT, "");
}
return sanitised;
}

View file

@ -648,6 +648,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
// not a cancelled doc
&& data.docstatus !== 2
&& !df.read_only
&& !df.is_virtual
&& !df.hidden
// not a standard field i.e., owner, modified_by, etc.
&& !frappe.model.std_fields_list.includes(df.fieldname))
@ -1029,7 +1030,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
title += ` (${__(doctype)})`;
}
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only;
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only && !docfield.is_virtual;
const align = (() => {
const is_numeric = frappe.model.is_numeric_field(docfield);

View file

@ -343,7 +343,7 @@ frappe.views.TreeView = class TreeView {
this.ignore_fields = this.opts.ignore_fields || [];
var mandatory_fields = $.map(me.opts.meta.fields, function(d) {
return (d.reqd || d.bold && !d.read_only) ? d : null });
return (d.reqd || d.bold && !d.read_only && !!d.is_virtual) ? d : null });
var opts_field_names = this.fields.map(function(d) {
return d.fieldname

View file

@ -1,11 +1,20 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import os
import unittest
from contextlib import contextmanager
from datetime import timedelta
from unittest.mock import patch
import frappe
from frappe.utils import cint
from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series
from frappe.desk.doctype.note.note import Note
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
from frappe.utils import cint, now_datetime
class CustomTestNote(Note):
@property
def age(self):
return now_datetime() - self.creation
class TestDocument(unittest.TestCase):
@ -256,4 +265,50 @@ class TestDocument(unittest.TestCase):
def test_limit_for_get(self):
doc = frappe.get_doc("DocType", "DocType")
# assuming DocType has more that 3 Data fields
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
def test_virtual_fields(self):
"""Virtual fields are accessible via API and Form views, whenever .as_dict is invoked
"""
frappe.db.delete("Custom Field", {"dt": "Note", "fieldname":"age"})
def patch_note():
return patch("frappe.controllers", new={frappe.local.site: {'Note': CustomTestNote}})
@contextmanager
def customize_note(with_options=False):
options = "frappe.utils.now_datetime() - doc.creation" if with_options else ""
custom_field = frappe.get_doc({
"doctype": "Custom Field",
"dt": "Note",
"fieldname": "age",
"fieldtype": "Data",
"read_only": True,
"is_virtual": True,
"options": options,
})
try:
yield custom_field.insert(ignore_if_duplicate=True)
finally:
custom_field.delete()
with patch_note():
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, CustomTestNote)
self.assertIsInstance(doc.age, timedelta)
self.assertIsNone(doc.as_dict().get("age"))
self.assertIsNone(doc.get_valid_dict().get("age"))
with customize_note(), patch_note():
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, CustomTestNote)
self.assertIsInstance(doc.age, timedelta)
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)
with customize_note(with_options=True):
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, Note)
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)

View file

@ -60,7 +60,7 @@ frappe.ui.form.on("Web Form", {
options: field.options,
reqd: field.reqd,
default: field.default,
read_only: field.read_only,
read_only: field.read_only || field.is_virtual,
depends_on: field.depends_on,
mandatory_depends_on: field.mandatory_depends_on,
read_only_depends_on: field.read_only_depends_on,