diff --git a/frappe/__init__.py b/frappe/__init__.py index 2062004296..20c0fcf205 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -348,6 +348,10 @@ def get_doclist(doctype, name=None): def get_doctype(doctype, processed=False): import frappe.model.doctype return frappe.model.doctype.get(doctype, processed) + +def get_meta(doctype, processed=False): + import frappe.model.doctype + return frappe.model.doctype.get_meta(doctype, processed) def delete_doc(doctype=None, name=None, doclist = None, force=0, ignore_doctypes=None, for_reload=False, ignore_permissions=False): diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 0bbc5a5825..84e1e4d2d9 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -34,3 +34,4 @@ class MandatoryError(ValidationError): pass class InvalidSignatureError(ValidationError): pass class RateLimitExceededError(ValidationError): pass class CannotChangeConstantError(ValidationError): pass +class LinkValidationError(ValidationError): pass \ No newline at end of file diff --git a/frappe/model/doctype.py b/frappe/model/doctype.py index 0d02cba40b..d6a592220a 100644 --- a/frappe/model/doctype.py +++ b/frappe/model/doctype.py @@ -27,6 +27,16 @@ docfield_types = frappe.local('doctype_docfield_types') # doctype_cache = {} # docfield_types = None +def get_meta(doctype, processed=False, cached=True): + meta = [] + for d in get(doctype=doctype, processed=processed, cached=cached): + if d.doctype=="DocType" and d.name==doctype: + meta.append(d) + elif d.parent and d.parent==doctype: + meta.append(d) + + return DocTypeDocList(meta) + def get(doctype, processed=False, cached=True): """return doclist""" if cached: @@ -333,12 +343,12 @@ def get_property(dt, prop, fieldname=None): return doctypelist[0].fields.get(prop) def get_link_fields(doctype): - """get docfields of links and selects with "link:" """ + """get docfields of links and selects with "link:" + for main doctype and child doctypes""" doctypelist = get(doctype) - - return doctypelist.get({"fieldtype":"Link"}).extend(doctypelist.get({"fieldtype":"Select", + return doctypelist.get({"fieldtype":"Link"}).extend(doctypelist.get({"fieldtype":"Select", "options": "^link:"})) - + def add_validators(doctype, doclist): for validator in frappe.db.sql("""select name from `tabDocType Validator` where for_doctype=%s""", (doctype,), as_dict=1): @@ -416,4 +426,8 @@ class DocTypeDocList(frappe.model.doclist.DocList): def get_permissions(self, user=None): user_roles = frappe.get_roles(user) return [p for p in self.get({"doctype": "DocPerm"}) - if cint(p.permlevel)==0 and (p.role=="All" or p.role in user_roles)] \ No newline at end of file + if cint(p.permlevel)==0 and (p.role=="All" or p.role in user_roles)] + + def get_link_fields(self): + return self.get({"fieldtype":"Link", "parent": self[0].name})\ + .extend(self.get({"fieldtype":"Select", "options": "^link:", "parent": self[0].name})) diff --git a/frappe/model/document.py b/frappe/model/document.py index 5b3269374b..482c07a00d 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from frappe import _, msgprint from frappe.utils import cint, flt from frappe.model import default_fields from frappe.model.db_schema import type_map @@ -22,9 +23,11 @@ class BaseDocument(object): def __getattr__(self, key): if self.__dict__.has_key(key): return self.__dict__[key] - if key != "_table_columns" and key in self.get_table_columns(): + + if key in self.get_table_columns(): return None - raise AttributeError, key + + raise AttributeError(key) def update(self, d): if "doctype" in d: @@ -39,19 +42,16 @@ class BaseDocument(object): return self.__dict__ def set(self, key, value): - if isinstance(value, list): + if isinstance(value, dict): + # appending + if not self.get(key): + self.__dict__[key] = [] + self.get(key).append(self._init_child(value, key)) + elif isinstance(value, list): for v in value: self.set(key, v) - return else: - if isinstance(value, dict): - # appending - if not self.get(key): - self.__dict__[key] = [] - self.get(key).append(self._init_child(value, key)) - return - - self.__dict__[key] = value + self.__dict__[key] = value def _init_child(self, value, key): if not self.doctype: @@ -71,11 +71,10 @@ class BaseDocument(object): return value - @property def meta(self): if not self.get("_meta"): - self._meta = frappe.get_doctype(self.doctype) + self._meta = frappe.get_meta(self.doctype) return self._meta def get_valid_dict(self): @@ -92,7 +91,7 @@ class BaseDocument(object): if not hasattr(self, "_table_columns"): doctype = self.__dict__.get("doctype") self._table_columns = default_fields[1:] + \ - [df.fieldname for df in frappe.get_doctype(doctype).get_docfields() + [df.fieldname for df in frappe.get_meta(doctype).get_docfields() if df.fieldtype in type_map] return self._table_columns @@ -118,42 +117,103 @@ class BaseDocument(object): if self.docstatus is not None: self.docstatus = cint(self.docstatus) + def set_missing_values(self, d): + for key, value in d.iteritems(): + if self.get(key) is None: + self.set(key, value) + + def get_missing_mandatory_fields(self): + """Get mandatory fields that do not have any values""" + def get_msg(df): + if df.fieldtype == "Table": + return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label)) + + elif self.parentfield: + return "{}: {} #{}: {}: {}".format(_("Error"), _("Row"), self.idx, + _("Value missing for"), _(df.label)) + else: + return "{}: {}: {}".format(_("Error"), _("Value missing for"), _(df.label)) + + missing = [] + + for df in self.meta.get({"doctype": "DocField", "reqd": 1}): + if self.get(df.fieldname) in (None, []): + missing.append((df.fieldname, get_msg(df))) + + return missing + + def get_invalid_links(self): + def get_msg(df, docname): + if self.parentfield: + return "{} #{}: {}: {}".format(_("Row"), self.idx, _(df.label), docname) + else: + return "{}: {}".format(_(df.label), docname) + + invalid_links = [] + for df in self.meta.get_link_fields(): + doctype = df.options + + if not doctype: + frappe.throw("Options not set for link field: {}".format(df.fieldname)) + + elif doctype.lower().startswith("link:"): + doctype = doctype[5:] + + docname = self.get(df.fieldname) + if docname and not frappe.db.get_value(doctype, docname): + invalid_links.append((df.fieldname, docname, get_msg(df, docname))) + + return invalid_links + class Document(BaseDocument): def __init__(self, arg1, arg2=None): self.doctype = self.name = None - if isinstance(arg1, basestring) and not arg2: - # single - self.doctype = self.name = arg1 - if arg1 and isinstance(arg1, basestring) and arg2: - self.doctype = arg1 - if isinstance(arg2, dict): - # filter - self.name = frappe.db.get_value(arg1, arg2, "name") - if self.name is None: - raise frappe.DoesNotExistError + if arg1 and isinstance(arg1, basestring): + if not arg2: + # single + self.doctype = self.name = arg1 else: - self.name = arg2 + self.doctype = arg1 + if isinstance(arg2, dict): + # filter + self.name = frappe.db.get_value(arg1, arg2, "name") + if self.name is None: + raise frappe.DoesNotExistError + else: + self.name = arg2 self.load_from_db() + elif isinstance(arg1, dict): super(Document, self).__init__(arg1) + + else: + # incorrect arguments. let's not proceed. + raise frappe.DataError("Document({0}, {1})".format(arg1, arg2)) def load_from_db(self): if self.meta[0].issingle: self.update(frappe.db.get_singles_dict(self.doctype)) self.fix_numeric_types() + else: d = frappe.db.get_value(self.doctype, self.name, "*", as_dict=1) for df in self.meta.get({"doctype":"DocField", "fieldtype":"Table"}): d[df.fieldname] = frappe.db.get_values(df.options, - {"parent": self.name}, "*", as_dict=True) + {"parent": self.name, "parenttype": self.doctype, "parentfield": df.fieldname}, + "*", as_dict=True) self.update(d) def insert(self): # check links # check permissions + self.set_defaults() + self._validate() + + # run validate, on update etc. + # parent if self.meta[0].issingle: self.update_single(self.get_valid_dict()) @@ -161,13 +221,9 @@ class Document(BaseDocument): self.insert_row() # children - for df in self.meta.get({"fieldtype":"Table"}): - value = self.get(df.fieldname) - if isinstance(value, list): - for d in value: - d.parent = self.name - print d.__dict__ - d.insert_row() + for d in self.get_all_children(): + d.parent = self.name + d.insert_row() def update_single(self, d): frappe.db.sql("""delete from tabSingles where doctype=%s""", d.get("doctype")) @@ -176,5 +232,67 @@ class Document(BaseDocument): frappe.db.sql("""insert into tabSingles(doctype, field, value) values (%s, %s, %s)""", (d.get("doctype", field, value))) - - + + def set_defaults(self): + if frappe.flags.in_import: + return + + new_doc = frappe.new_doc(self.doctype).fields + self.set_missing_values(new_doc) + + # children + for df in self.meta.get({"fieldtype":"Table"}): + new_doc = frappe.new_doc(df.options).fields + value = self.get(df.fieldname) + if isinstance(value, list): + for d in value: + d.set_missing_values(new_doc) + + def _validate(self): + self.trigger("validate") + self.validate_mandatory() + self.validate_links() + + # check restrictions + + def validate_mandatory(self): + if self.get("ignore_mandatory"): + return + + missing = self.get_missing_mandatory_fields() + for d in self.get_all_children(): + missing.extend(d.get_missing_mandatory_fields()) + + if not missing: + return + + for fieldname, msg in missing: + msgprint(msg) + + raise frappe.MandatoryError(", ".join((each[0] for each in missing))) + + def validate_links(self): + if self.get("ignore_links"): + return + + invalid_links = self.get_invalid_links() + for d in self.get_all_children(): + invalid_links.extend(d.get_invalid_links()) + + if not invalid_links: + return + + msg = ", ".join((each[2] for each in invalid_links)) + frappe.throw("{}: {}".format(_("Could not find the following documents"), msg), + frappe.LinkValidationError) + + def get_all_children(self): + ret = [] + for df in self.meta.get({"fieldtype": "Table"}): + value = self.get(df.fieldname) + if isinstance(value, list): + ret.extend(value) + return ret + + def trigger(self, func, *args, **kwargs): + return \ No newline at end of file diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 1668c49948..51be1c2c2d 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -28,16 +28,8 @@ def get_link_fields(doctype): """ import frappe.model.doctype doclist = frappe.model.doctype.get(doctype) - return [ - (d.fields.get('fieldname'), d.fields.get('options'), d.fields.get('label')) - for d in doclist - if d.fields.get('doctype') == 'DocField' and d.fields.get('parent') == doctype - and d.fields.get('fieldname')!='owner' - and (d.fields.get('fieldtype') == 'Link' or - ( d.fields.get('fieldtype') == 'Select' - and (d.fields.get('options') or '').startswith('link:')) - ) - ] + return [(d.fields.get('fieldname'), d.fields.get('options'), d.fields.get('label')) + for d in doclist.get_link_fields() if d.fields.get('fieldname')!='owner'] def get_table_fields(doctype): child_tables = [[d[0], d[1]] for d in frappe.db.sql("""select options, fieldname diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 7d7f6a88be..e90e812473 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -44,11 +44,11 @@ def set_new_name(doc): doc.name = make_autoname(autoname, doc.doctype) # given - elif doc.fields.get('__newname',''): - doc.name = doc.fields['__newname'] + elif doc.get('__newname', None): + doc.name = doc.get('__newname') # default name for table - elif doc.meta.istable: + elif doc.meta[0].istable: doc.name = make_autoname('#########', doc.doctype) # unable to determine a name, use global series diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 2176637ec3..8e5eada0ea 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -32,6 +32,9 @@ class TestDocument(unittest.TestCase): self.assertTrue(d.name.startswith("EV")) self.assertEquals(frappe.db.get_value("Event", d.name, "subject"), "_Test Event 1") + + # test if default values are added + self.assertEquals(d.send_reminder, 1) def test_insert_with_child(self): d = Document({ @@ -49,6 +52,36 @@ class TestDocument(unittest.TestCase): self.assertTrue(d.name.startswith("EV")) self.assertEquals(frappe.db.get_value("Event", d.name, "subject"), "_Test Event 2") - + d1 = Document("Event", d.name) self.assertTrue(d1.event_individuals[0].person, "Administrator") + + def test_mandatory(self): + d = Document({ + "doctype": "User", + "email": "test_mandatory@example.com", + }) + self.assertRaises(frappe.MandatoryError, d.insert) + + d.set("first_name", "Test Mandatory") + d.insert() + self.assertEquals(frappe.db.get_value("User", d.name), d.name) + + def test_link_validation(self): + d = Document({ + "doctype": "User", + "email": "test_link_validation@example.com", + "first_name": "Link Validation", + "user_roles": [ + { + "role": "ABC" + } + ] + }) + self.assertRaises(frappe.LinkValidationError, d.insert) + d.user_roles = [] + d.set("user_roles", { + "role": "System Manager" + }) + d.insert() + self.assertEquals(frappe.db.get_value("User", d.name), d.name) \ No newline at end of file