seitime-frappe/frappe/model/base_document.py
mergify[bot] 53a2539c5a
feat(minor): Implement __getitem__ in Base Document (#14855) (#14866)
* feat(minor): Implement __getitem__ in Base Document

This addition allows for syntactic sugars like writing doc["name"] to
access document name. This addition is solely for the sake of code
simplicity. Since dictionaries in Python follow this standard, we can
write code that works for both Vanilla Dicts, Frappe Dicts & all
Document objects

* fix: Use getattr over get in getitem

For the sake of consistency and some level of sanity in the universe.

Co-authored-by: Ankush Menat <ankush@frappe.io>

* fix: Raise KeyError instead of AttributeError

why: shit breaks down. Specifically stuff in Jinja templates, attributes
that are conditional. Either this is acceptable or subscripting objects
cannot and probably should not be possible along with our pre-condiitons

* chore: Use hasattr instead of get default

Couldn't decide between a style commit or a chore

Co-authored-by: Ankush Menat <ankush@frappe.io>
(cherry picked from commit 583a7ef23923d362c85beeabe57703c2d5b12a65)

Co-authored-by: gavin <gavin18d@gmail.com>
2021-11-02 13:24:36 +05:30

1028 lines
31 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
import datetime
from frappe import _
from frappe.model import default_fields, 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.html_utils import unescape_html
max_positive_value = {
'smallint': 2 ** 15,
'int': 2 ** 31,
'bigint': 2 ** 63
}
DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link')
def get_controller(doctype):
"""Returns the **class** object of the given DocType.
For `custom` type, returns `frappe.model.document.Document`.
:param doctype: DocType name as string."""
def _get_controller():
from frappe.model.document import Document
from frappe.utils.nestedset import NestedSet
module_name, custom = frappe.db.get_value(
"DocType", doctype, ("module", "custom"), cache=True
) or ["Core", False]
if custom:
if frappe.db.field_exists("DocType", "is_tree"):
is_tree = frappe.db.get_value("DocType", doctype, "is_tree", cache=True)
else:
is_tree = False
_class = NestedSet if is_tree else Document
else:
class_overrides = frappe.get_hooks('override_doctype_class')
if class_overrides and class_overrides.get(doctype):
import_path = class_overrides[doctype][-1]
module_path, classname = import_path.rsplit('.', 1)
module = frappe.get_module(module_path)
if not hasattr(module, classname):
raise ImportError('{0}: {1} does not exist in module {2}'.format(doctype, classname, module_path))
else:
module = load_doctype_module(doctype, module_name)
classname = doctype.replace(" ", "").replace("-", "")
if hasattr(module, classname):
_class = getattr(module, classname)
if issubclass(_class, BaseDocument):
_class = getattr(module, classname)
else:
raise ImportError(doctype)
else:
raise ImportError(doctype)
return _class
if frappe.local.dev_server:
return _get_controller()
site_controllers = frappe.controllers.setdefault(frappe.local.site, {})
if doctype not in site_controllers:
site_controllers[doctype] = _get_controller()
return site_controllers[doctype]
class BaseDocument(object):
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
def __init__(self, d):
self.update(d)
self.dont_update_if_missing = []
if hasattr(self, "__setup__"):
self.__setup__()
def __getitem__(self, key):
return self.get(key) if hasattr(self, key) else frappe.throw(msg=key, exc=KeyError)
@property
def meta(self):
if not getattr(self, "_meta", None):
self._meta = frappe.get_meta(self.doctype)
return self._meta
def __getstate__(self):
self._meta = None
return self.__dict__
def update(self, d):
""" Update multiple fields of a doctype using a dictionary of key-value pairs.
Example:
doc.update({
"user": "admin",
"balance": 42000
})
"""
if "doctype" in d:
self.set("doctype", d.get("doctype"))
# first set default field values of base document
for key in default_fields:
if key in d:
self.set(key, d.get(key))
for key, value in d.items():
self.set(key, value)
return self
def update_if_missing(self, d):
if isinstance(d, BaseDocument):
d = d.get_valid_dict()
if "doctype" in d:
self.set("doctype", d.get("doctype"))
for key, value in d.items():
# dont_update_if_missing is a list of fieldnames, for which, you don't want to set default value
if (self.get(key) is None) and (value is not None) and (key not in self.dont_update_if_missing):
self.set(key, value)
def get_db_value(self, key):
return frappe.db.get_value(self.doctype, self.name, key)
def get(self, key=None, filters=None, limit=None, default=None):
if key:
if isinstance(key, dict):
return _filter(self.get_all_children(), key, limit=limit)
if filters:
if isinstance(filters, dict):
value = _filter(self.__dict__.get(key, []), filters, limit=limit)
else:
default = filters
filters = None
value = self.__dict__.get(key, default)
else:
value = self.__dict__.get(key, default)
if value is None and key not in self.ignore_in_getter \
and key in (d.fieldname for d in self.meta.get_table_fields()):
self.set(key, [])
value = self.__dict__.get(key)
return value
else:
return self.__dict__
def getone(self, key, filters=None):
return self.get(key, filters=filters, limit=1)[0]
def set(self, key, value, as_value=False):
if isinstance(value, list) and not as_value:
self.__dict__[key] = []
self.extend(key, value)
else:
self.__dict__[key] = value
def delete_key(self, key):
if key in self.__dict__:
del self.__dict__[key]
def append(self, key, value=None):
""" Append an item to a child table.
Example:
doc.append("childtable", {
"child_table_field": "value",
"child_table_int_field": 0,
...
})
"""
if value==None:
value={}
if isinstance(value, (dict, BaseDocument)):
if not self.__dict__.get(key):
self.__dict__[key] = []
value = self._init_child(value, key)
self.__dict__[key].append(value)
# reference parent document
value.parent_doc = self
return value
else:
# metaclasses may have arbitrary lists
# which we can ignore
if (getattr(self, '_metaclass', None)
or self.__class__.__name__ in ('Meta', 'FormMeta', 'DocField')):
return value
raise ValueError(
'Document for field "{0}" attached to child table of "{1}" must be a dict or BaseDocument, not {2} ({3})'.format(key,
self.name, str(type(value))[1:-1], value)
)
def extend(self, key, value):
if isinstance(value, list):
for v in value:
self.append(key, v)
else:
raise ValueError
def remove(self, doc):
self.get(doc.parentfield).remove(doc)
def _init_child(self, value, key):
if not self.doctype:
return value
if not isinstance(value, BaseDocument):
if "doctype" not in value or value['doctype'] is None:
value["doctype"] = self.get_table_field_doctype(key)
if not value["doctype"]:
raise AttributeError(key)
value = get_controller(value["doctype"])(value)
value.init_valid_columns()
value.parent = self.name
value.parenttype = self.doctype
value.parentfield = key
if value.docstatus is None:
value.docstatus = 0
if not getattr(value, "idx", None):
value.idx = len(self.get(key) or []) + 1
if not getattr(value, "name", None):
value.__dict__['__islocal'] = 1
return value
def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls = False):
d = frappe._dict()
for fieldname in self.meta.get_valid_columns():
d[fieldname] = self.get(fieldname)
# if no need for sanitization and value is None, continue
if not sanitize and d[fieldname] is None:
continue
df = self.meta.get_field(fieldname)
if df:
if df.fieldtype=="Check":
d[fieldname] = 1 if cint(d[fieldname]) else 0
elif df.fieldtype=="Int" and not isinstance(d[fieldname], int):
d[fieldname] = cint(d[fieldname])
elif df.fieldtype in ("Currency", "Float", "Percent") and not isinstance(d[fieldname], float):
d[fieldname] = flt(d[fieldname])
elif df.fieldtype in ("Datetime", "Date", "Time") and d[fieldname]=="":
d[fieldname] = None
elif df.get("unique") and cstr(d[fieldname]).strip()=="":
# unique empty field should be set to None
d[fieldname] = None
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
frappe.throw(_('Value for {0} cannot be a list').format(_(df.label)))
if convert_dates_to_str and isinstance(d[fieldname], (
datetime.datetime,
datetime.date,
datetime.time,
datetime.timedelta
)):
d[fieldname] = str(d[fieldname])
if d[fieldname] == None and ignore_nulls:
del d[fieldname]
return d
def init_valid_columns(self):
for key in default_fields:
if key not in self.__dict__:
self.__dict__[key] = None
if key in ("idx", "docstatus") and self.__dict__[key] is None:
self.__dict__[key] = 0
for key in self.get_valid_columns():
if key not in self.__dict__:
self.__dict__[key] = None
def get_valid_columns(self):
if self.doctype not in frappe.local.valid_columns:
if self.doctype in DOCTYPES_FOR_DOCTYPE:
from frappe.model.meta import get_table_columns
valid = get_table_columns(self.doctype)
else:
valid = self.meta.get_valid_columns()
frappe.local.valid_columns[self.doctype] = valid
return frappe.local.valid_columns[self.doctype]
def is_new(self):
return self.get("__islocal")
def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=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] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls, no_default_fields=no_default_fields) for d in children]
if no_nulls:
for k in list(doc):
if doc[k] is None:
del doc[k]
if no_default_fields:
for k in list(doc):
if k in default_fields:
del doc[k]
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers", "__unsaved"):
if self.get(key):
doc[key] = self.get(key)
return doc
def as_json(self):
return frappe.as_json(self.as_dict())
def get_table_field_doctype(self, fieldname):
try:
return self.meta.get_field(fieldname).options
except AttributeError:
if self.doctype == 'DocType':
return dict(links='DocType Link', actions='DocType Action').get(fieldname)
raise
def get_parentfield_of_doctype(self, doctype):
fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options==doctype]
return fieldname[0] if fieldname else None
def db_insert(self):
"""INSERT the document (with valid columns) in the database."""
if not self.name:
# name will be set by document class in most cases
set_new_name(self)
if not self.creation:
self.creation = self.modified = now()
self.created_by = self.modified_by = frappe.session.user
# if doctype is "DocType", don't insert null values as we don't know who is valid yet
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
columns = list(d)
try:
frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns})
VALUES ({values})""".format(
doctype = self.doctype,
columns = ", ".join("`"+c+"`" for c in columns),
values = ", ".join(["%s"] * len(columns))
), list(d.values()))
except Exception as e:
if frappe.db.is_primary_key_violation(e):
if self.meta.autoname=="hash":
# hash collision? try again
frappe.flags.retry_count = (frappe.flags.retry_count or 0) + 1
if frappe.flags.retry_count > 5 and not frappe.flags.in_test:
raise
self.name = None
self.db_insert()
return
frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
elif frappe.db.is_unique_key_violation(e):
# unique constraint
self.show_unique_validation_message(e)
else:
raise
self.set("__islocal", False)
def db_update(self):
if self.get("__islocal") or not self.name:
self.db_insert()
return
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
# don't update name, as case might've been changed
name = d['name']
del d['name']
columns = list(d)
try:
frappe.db.sql("""UPDATE `tab{doctype}`
SET {values} WHERE `name`=%s""".format(
doctype = self.doctype,
values = ", ".join("`"+c+"`=%s" for c in columns)
), list(d.values()) + [name])
except Exception as e:
if frappe.db.is_unique_key_violation(e):
self.show_unique_validation_message(e)
else:
raise
def db_update_all(self):
"""Raw update parent + children
DOES NOT VALIDATE AND CALL TRIGGERS"""
self.db_update()
for df in self.meta.get_table_fields():
for doc in self.get(df.fieldname):
doc.db_update()
def show_unique_validation_message(self, e):
if frappe.db.db_type != 'postgres':
fieldname = str(e).split("'")[-2]
label = None
# MariaDB gives key_name in error. Extracting fieldname from key name
try:
fieldname = self.get_field_name_by_key_name(fieldname)
except IndexError:
pass
label = self.get_label_from_fieldname(fieldname)
frappe.msgprint(_("{0} must be unique").format(label or fieldname))
# this is used to preserve traceback
raise frappe.UniqueValidationError(self.doctype, self.name, e)
def get_field_name_by_key_name(self, key_name):
"""MariaDB stores a mapping between `key_name` and `column_name`.
This function returns the `column_name` associated with the `key_name` passed
Args:
key_name (str): The name of the database index.
Raises:
IndexError: If the key is not found in the table.
Returns:
str: The column name associated with the key.
"""
return frappe.db.sql(f"""
SHOW
INDEX
FROM
`tab{self.doctype}`
WHERE
key_name=%s
AND
Non_unique=0
""", key_name, as_dict=True)[0].get("Column_name")
def get_label_from_fieldname(self, fieldname):
"""Returns the associated label for fieldname
Args:
fieldname (str): The fieldname in the DocType to use to pull the label.
Returns:
str: The label associated with the fieldname, if found, otherwise `None`.
"""
df = self.meta.get_field(fieldname)
if df:
return df.label
def update_modified(self):
"""Update modified timestamp"""
self.set("modified", now())
frappe.db.set_value(self.doctype, self.name, 'modified', self.modified, update_modified=False)
def _fix_numeric_types(self):
for df in self.meta.get("fields"):
if df.fieldtype == "Check":
self.set(df.fieldname, cint(self.get(df.fieldname)))
elif self.get(df.fieldname) is not None:
if df.fieldtype == "Int":
self.set(df.fieldname, cint(self.get(df.fieldname)))
elif df.fieldtype in ("Float", "Currency", "Percent"):
self.set(df.fieldname, flt(self.get(df.fieldname)))
if self.docstatus is not None:
self.docstatus = cint(self.docstatus)
def _get_missing_mandatory_fields(self):
"""Get mandatory fields that do not have any values"""
def get_msg(df):
if df.fieldtype in table_fields:
return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label))
elif self.parentfield:
return "{}: {} {} #{}: {}: {}".format(_("Error"), frappe.bold(_(self.doctype)),
_("Row"), self.idx, _("Value missing for"), _(df.label))
else:
return _("Error: Value missing for {0}: {1}").format(_(df.parent), _(df.label))
missing = []
for df in self.meta.get("fields", {"reqd": ('=', 1)}):
if self.get(df.fieldname) in (None, []) or not strip_html(cstr(self.get(df.fieldname))).strip():
missing.append((df.fieldname, get_msg(df)))
# check for missing parent and parenttype
if self.meta.istable:
for fieldname in ("parent", "parenttype"):
if not self.get(fieldname):
missing.append((fieldname, get_msg(frappe._dict(label=fieldname))))
return missing
def get_invalid_links(self, is_submittable=False):
"""Returns list of invalid links and also updates fetch values if not set"""
def get_msg(df, docname):
if self.parentfield:
return "{} #{}: {}: {}".format(_("Row"), self.idx, _(df.label), docname)
else:
return "{}: {}".format(_(df.label), docname)
invalid_links = []
cancelled_links = []
for df in (self.meta.get_link_fields()
+ self.meta.get("fields", {"fieldtype": ('=', "Dynamic Link")})):
docname = self.get(df.fieldname)
if docname:
if df.fieldtype=="Link":
doctype = df.options
if not doctype:
frappe.throw(_("Options not set for link field {0}").format(df.fieldname))
else:
doctype = self.get(df.options)
if not doctype:
frappe.throw(_("{0} must be set first").format(self.meta.get_label(df.options)))
# MySQL is case insensitive. Preserve case of the original docname in the Link Field.
# get a map of values ot fetch along with this link query
# that are mapped as link_fieldname.source_fieldname in Options of
# Readonly or Data or Text type fields
fields_to_fetch = [
_df for _df in self.meta.get_fields_to_fetch(df.fieldname)
if
not _df.get('fetch_if_empty')
or (_df.get('fetch_if_empty') and not self.get(_df.fieldname))
]
if not frappe.get_meta(doctype).get('is_virtual'):
if not fields_to_fetch:
# cache a single value type
values = frappe._dict(name=frappe.db.get_value(doctype, docname,
'name', cache=True))
else:
values_to_fetch = ['name'] + [_df.fetch_from.split('.')[-1]
for _df in fields_to_fetch]
# don't cache if fetching other values too
values = frappe.db.get_value(doctype, docname,
values_to_fetch, as_dict=True)
if frappe.get_meta(doctype).issingle:
values.name = doctype
if frappe.get_meta(doctype).get('is_virtual'):
values = frappe.get_doc(doctype, docname)
if values:
setattr(self, df.fieldname, values.name)
for _df in fields_to_fetch:
if self.is_new() or self.docstatus != 1 or _df.allow_on_submit:
self.set_fetch_from_value(doctype, _df, values)
notify_link_count(doctype, docname)
if not values.name:
invalid_links.append((df.fieldname, docname, get_msg(df, docname)))
elif (df.fieldname != "amended_from"
and (is_submittable or self.meta.is_submittable) and frappe.get_meta(doctype).is_submittable
and cint(frappe.db.get_value(doctype, docname, "docstatus"))==2):
cancelled_links.append((df.fieldname, docname, get_msg(df, docname)))
return invalid_links, cancelled_links
def set_fetch_from_value(self, doctype, df, values):
fetch_from_fieldname = df.fetch_from.split('.')[-1]
value = values[fetch_from_fieldname]
if df.fieldtype in ['Small Text', 'Text', 'Data']:
if fetch_from_fieldname in default_fields:
from frappe.model.meta import get_default_df
fetch_from_df = get_default_df(fetch_from_fieldname)
else:
fetch_from_df = frappe.get_meta(doctype).get_field(fetch_from_fieldname)
if not fetch_from_df:
frappe.throw(
_('Please check the value of "Fetch From" set for field {0}').format(frappe.bold(df.label)),
title = _('Wrong Fetch From value')
)
fetch_from_ft = fetch_from_df.get('fieldtype')
if fetch_from_ft == 'Text Editor' and value:
value = unescape_html(strip_html(value))
setattr(self, df.fieldname, value)
def _validate_selects(self):
if frappe.flags.in_import:
return
for df in self.meta.get_select_fields():
if df.fieldname=="naming_series" or not (self.get(df.fieldname) and df.options):
continue
options = (df.options or "").split("\n")
# if only empty options
if not filter(None, options):
continue
# strip and set
self.set(df.fieldname, cstr(self.get(df.fieldname)).strip())
value = self.get(df.fieldname)
if value not in options and not (frappe.flags.in_test and value.startswith("_T-")):
# show an elaborate message
prefix = _("Row #{0}:").format(self.idx) if self.get("parentfield") else ""
label = _(self.meta.get_label(df.fieldname))
comma_options = '", "'.join(_(each) for each in options)
frappe.throw(_('{0} {1} cannot be "{2}". It should be one of "{3}"').format(prefix, label,
value, comma_options))
def _validate_data_fields(self):
from frappe.core.doctype.user.user import STANDARD_USERS
# data_field options defined in frappe.model.data_field_options
for data_field in self.meta.get_data_fields():
data = self.get(data_field.fieldname)
data_field_options = data_field.get("options")
old_fieldtype = data_field.get("oldfieldtype")
if old_fieldtype and old_fieldtype != "Data":
continue
if data_field_options == "Email":
if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS):
continue
for email_address in frappe.utils.split_emails(data):
frappe.utils.validate_email_address(email_address, throw=True)
if data_field_options == "Name":
frappe.utils.validate_name(data, throw=True)
if data_field_options == "Phone":
frappe.utils.validate_phone_number(data, throw=True)
if data_field_options == "URL":
if not data:
continue
frappe.utils.validate_url(data, throw=True)
def _validate_constants(self):
if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants:
return
constants = [d.fieldname for d in self.meta.get("fields", {"set_only_once": ('=',1)})]
if constants:
values = frappe.db.get_value(self.doctype, self.name, constants, as_dict=True)
for fieldname in constants:
df = self.meta.get_field(fieldname)
# This conversion to string only when fieldtype is Date
if df.fieldtype == 'Date' or df.fieldtype == 'Datetime':
value = str(values.get(fieldname))
else:
value = values.get(fieldname)
if self.get(fieldname) != value:
frappe.throw(_("Value cannot be changed for {0}").format(self.meta.get_label(fieldname)),
frappe.CannotChangeConstantError)
def _validate_length(self):
if frappe.flags.in_install:
return
if self.meta.issingle:
# single doctype value type is mediumtext
return
type_map = frappe.db.type_map
for fieldname, value in self.get_valid_dict().items():
df = self.meta.get_field(fieldname)
if not df or df.fieldtype == 'Check':
# skip standard fields and Check fields
continue
column_type = type_map[df.fieldtype][0] or None
if column_type == 'varchar':
default_column_max_length = type_map[df.fieldtype][1] or None
max_length = cint(df.get("length")) or cint(default_column_max_length)
if len(cstr(value)) > max_length:
self.throw_length_exceeded_error(df, max_length, value)
elif column_type in ('int', 'bigint', 'smallint'):
max_length = max_positive_value[column_type]
if abs(cint(value)) > max_length:
self.throw_length_exceeded_error(df, max_length, value)
def _validate_code_fields(self):
for field in self.meta.get_code_fields():
code_string = self.get(field.fieldname)
language = field.get("options")
if language == "Python":
frappe.utils.validate_python_code(code_string, fieldname=field.label, is_expression=False)
elif language == "PythonExpression":
frappe.utils.validate_python_code(code_string, fieldname=field.label)
def throw_length_exceeded_error(self, df, max_length, value):
if self.parentfield and self.idx:
reference = _("{0}, Row {1}").format(_(self.doctype), self.idx)
else:
reference = "{0} {1}".format(_(self.doctype), self.name)
frappe.throw(_("{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}")\
.format(reference, _(df.label), max_length, value), frappe.CharacterLengthExceededError, title=_('Value too big'))
def _validate_update_after_submit(self):
# get the full doc with children
db_values = frappe.get_doc(self.doctype, self.name).as_dict()
for key in self.as_dict():
df = self.meta.get_field(key)
db_value = db_values.get(key)
if df and not df.allow_on_submit and (self.get(key) or db_value):
if df.fieldtype in table_fields:
# just check if the table size has changed
# individual fields will be checked in the loop for children
self_value = len(self.get(key))
db_value = len(db_value)
else:
self_value = self.get_value(key)
if self_value != db_value:
frappe.throw(_("Not allowed to change {0} after submission").format(df.label),
frappe.UpdateAfterSubmitError)
def _sanitize_content(self):
"""Sanitize HTML and Email in field values. Used to prevent XSS.
- Ignore if 'Ignore XSS Filter' is checked or fieldtype is 'Code'
"""
from bs4 import BeautifulSoup
if frappe.flags.in_install:
return
for fieldname, value in self.get_valid_dict().items():
if not value or not isinstance(value, str):
continue
value = frappe.as_unicode(value)
if (u"<" not in value and u">" not in value):
# doesn't look like html so no need
continue
elif "<!-- markdown -->" in value and not bool(BeautifulSoup(value, "html.parser").find()):
# should be handled separately via the markdown converter function
continue
df = self.meta.get_field(fieldname)
sanitized_value = value
if df and (df.get("ignore_xss_filter")
or (df.get("fieldtype") in ("Data", "Small Text", "Text") and df.get("options")=="Email")
or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode", "Code")
# cancelled and submit but not update after submit should be ignored
or self.docstatus==2
or (self.docstatus==1 and not df.get("allow_on_submit"))):
continue
else:
sanitized_value = sanitize_html(value, linkify=df and df.fieldtype=='Text Editor')
self.set(fieldname, sanitized_value)
def _save_passwords(self):
"""Save password field values in __Auth table"""
from frappe.utils.password import set_encrypted_password, remove_encrypted_password
if self.flags.ignore_save_passwords is True:
return
for df in self.meta.get('fields', {'fieldtype': ('=', 'Password')}):
if self.flags.ignore_save_passwords and df.fieldname in self.flags.ignore_save_passwords: continue
new_password = self.get(df.fieldname)
if not new_password:
remove_encrypted_password(self.doctype, self.name, df.fieldname)
if new_password and not self.is_dummy_password(new_password):
# is not a dummy password like '*****'
set_encrypted_password(self.doctype, self.name, new_password, df.fieldname)
# set dummy password like '*****'
self.set(df.fieldname, '*'*len(new_password))
def get_password(self, fieldname='password', raise_exception=True):
from frappe.utils.password import get_decrypted_password
if self.get(fieldname) and not self.is_dummy_password(self.get(fieldname)):
return self.get(fieldname)
return get_decrypted_password(self.doctype, self.name, fieldname, raise_exception=raise_exception)
def is_dummy_password(self, pwd):
return ''.join(set(pwd))=='*'
def precision(self, fieldname, parentfield=None):
"""Returns float precision for a particular field (or get global default).
:param fieldname: Fieldname for which precision is required.
:param parentfield: If fieldname is in child table."""
from frappe.model.meta import get_field_precision
if parentfield and not isinstance(parentfield, str):
parentfield = parentfield.parentfield
cache_key = parentfield or "main"
if not hasattr(self, "_precision"):
self._precision = frappe._dict()
if cache_key not in self._precision:
self._precision[cache_key] = frappe._dict()
if fieldname not in self._precision[cache_key]:
self._precision[cache_key][fieldname] = None
doctype = self.meta.get_field(parentfield).options if parentfield else self.doctype
df = frappe.get_meta(doctype).get_field(fieldname)
if df.fieldtype in ("Currency", "Float", "Percent"):
self._precision[cache_key][fieldname] = get_field_precision(df, self)
return self._precision[cache_key][fieldname]
def get_formatted(self, fieldname, doc=None, currency=None, absolute_value=False, translated=False, format=None):
from frappe.utils.formatters import format_value
df = self.meta.get_field(fieldname)
if not df and fieldname in default_fields:
from frappe.model.meta import get_default_df
df = get_default_df(fieldname)
if not currency and df:
currency = self.get(df.get("options"))
if not frappe.db.exists('Currency', currency, cache=True):
currency = None
val = self.get(fieldname)
if translated:
val = _(val)
if not doc:
doc = getattr(self, "parent_doc", None) or self
if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)):
val = abs(self.get(fieldname))
return format_value(val, df=df, doc=doc, currency=currency, format=format)
def is_print_hide(self, fieldname, df=None, for_print=True):
"""Returns true if fieldname is to be hidden for print.
Print Hide can be set via the Print Format Builder or in the controller as a list
of hidden fields. Example
class MyDoc(Document):
def __setup__(self):
self.print_hide = ["field1", "field2"]
:param fieldname: Fieldname to be checked if hidden.
"""
meta_df = self.meta.get_field(fieldname)
if meta_df and meta_df.get("__print_hide"):
return True
print_hide = 0
if self.get(fieldname)==0 and not self.meta.istable:
print_hide = ( df and df.print_hide_if_no_value ) or ( meta_df and meta_df.print_hide_if_no_value )
if not print_hide:
if df and df.print_hide is not None:
print_hide = df.print_hide
elif meta_df:
print_hide = meta_df.print_hide
return print_hide
def in_format_data(self, fieldname):
"""Returns True if shown via Print Format::`format_data` property.
Called from within standard print format."""
doc = getattr(self, "parent_doc", self)
if hasattr(doc, "format_data_map"):
return fieldname in doc.format_data_map
else:
return True
def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields):
"""If the user does not have permissions at permlevel > 0, then reset the values to original / default"""
to_reset = []
for df in high_permlevel_fields:
if df.permlevel not in has_access_to and df.fieldtype not in display_fieldtypes:
to_reset.append(df)
if to_reset:
if self.is_new():
# if new, set default value
ref_doc = frappe.new_doc(self.doctype)
else:
# get values from old doc
if self.get('parent_doc'):
parent_doc = self.parent_doc.get_latest()
ref_doc = [d for d in parent_doc.get(self.parentfield) if d.name == self.name][0]
else:
ref_doc = self.get_latest()
for df in to_reset:
self.set(df.fieldname, ref_doc.get(df.fieldname))
def get_value(self, fieldname):
df = self.meta.get_field(fieldname)
val = self.get(fieldname)
return self.cast(val, df)
def cast(self, value, df):
return cast_fieldtype(df.fieldtype, value, show_warning=False)
def _extract_images_from_text_editor(self):
from frappe.core.doctype.file.file import extract_images_from_doc
if self.doctype != "DocType":
for df in self.meta.get("fields", {"fieldtype": ('=', "Text Editor")}):
extract_images_from_doc(self, df.fieldname)
def _filter(data, filters, limit=None):
"""pass filters as:
{"key": "val", "key": ["!=", "val"],
"key": ["in", "val"], "key": ["not in", "val"], "key": "^val",
"key" : True (exists), "key": False (does not exist) }"""
out, _filters = [], {}
if not data:
return out
# setup filters as tuples
if filters:
for f in filters:
fval = filters[f]
if not isinstance(fval, (tuple, list)):
if fval is True:
fval = ("not None", fval)
elif fval is False:
fval = ("None", fval)
elif isinstance(fval, str) and fval.startswith("^"):
fval = ("^", fval[1:])
else:
fval = ("=", fval)
_filters[f] = fval
for d in data:
add = True
for f, fval in _filters.items():
if not frappe.compare(getattr(d, f, None), fval[0], fval[1]):
add = False
break
if add:
out.append(d)
if limit and (len(out)-1)==limit:
break
return out