diff --git a/frappe/__init__.py b/frappe/__init__.py index 20c0fcf205..91a0dfc812 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -111,6 +111,7 @@ def init(site, sites_path=None): local.test_objects = {} local.jenv = None local.jloader =None + local.meta = {} setup_module_map() @@ -349,9 +350,9 @@ 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 get_meta(doctype, cached=True): + import frappe.model.meta + return frappe.model.meta.get_meta(doctype, cached=cached) def delete_doc(doctype=None, name=None, doclist = None, force=0, ignore_doctypes=None, for_reload=False, ignore_permissions=False): diff --git a/frappe/cli.py b/frappe/cli.py index 5e383d2cce..d191de2397 100755 --- a/frappe/cli.py +++ b/frappe/cli.py @@ -660,11 +660,27 @@ def smtp_debug_server(): os.execv(python, [python, '-m', "smtpd", "-n", "-c", "DebuggingServer", "localhost:25"]) @cmd -def run_tests(app=None, module=None, doctype=None, verbose=False): +def run_tests(app=None, module=None, doctype=None, verbose=False, profile=False): import frappe.test_runner - ret = frappe.test_runner.main(app and app[0], module and module[0], doctype and doctype[0], verbose) - if len(ret.failures) > 0 or len(ret.errors) > 0: - exit(1) + + def _run(): + ret = frappe.test_runner.main(app and app[0], module and module[0], doctype and doctype[0], verbose) + if len(ret.failures) > 0 or len(ret.errors) > 0: + exit(1) + + if profile: + import cProfile, pstats, StringIO + pr = cProfile.Profile() + pr.enable() + _run() + pr.disable() + s = StringIO.StringIO() + sortby = 'cumulative' + ps = pstats.Stats(pr, stream=s).sort_stats(sortby) + ps.print_stats() + print s.getvalue() + else: + _run() @cmd def serve(port=8000, profile=False, sites_path='.', site=None): diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 8cb982a164..7dd9487f05 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -7,6 +7,7 @@ import frappe no_value_fields = ['Section Break', 'Column Break', 'HTML', 'Table', 'Button', 'Image'] default_fields = ['doctype','name','owner','creation','modified','modified_by','parent','parentfield','parenttype','idx','docstatus'] +integer_docfield_properties = ["reqd", "search_index", "in_list_view", "permlevel", "hidden", "read_only", "ignore_restrictions", "allow_on_submit", "report_hide", "in_filter", "no_copy", "print_hide"] def insert(doclist): if not isinstance(doclist, list): diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index 70c80971be..4e909bd56e 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -29,8 +29,10 @@ def get_new_doc(doctype, parent_doc = None, parentfield = None): if parentfield: doc.parentfield = parentfield + defaults = frappe.defaults.get_defaults() + for d in meta.get({"doctype":"DocField", "parent": doctype}): - default = frappe.defaults.get_user_default(d.fieldname) + default = defaults.get(d.fieldname) if (d.fieldtype=="Link") and d.ignore_restrictions != 1 and (d.options in restrictions)\ and len(restrictions[d.options])==1: diff --git a/frappe/model/doctype.py b/frappe/model/doctype.py index d6a592220a..5b651f6744 100644 --- a/frappe/model/doctype.py +++ b/frappe/model/doctype.py @@ -27,16 +27,6 @@ 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: diff --git a/frappe/model/document.py b/frappe/model/document.py index 482c07a00d..69e0f77cad 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -9,65 +9,81 @@ from frappe.model import default_fields from frappe.model.db_schema import type_map from frappe.model.naming import set_new_name -# validation - links, mandatory +# save / update # once_only validation # permissions # methods # timestamps and docstatus -# defaults (insert) class BaseDocument(object): - def __init__(self, d): - self.update(d) + def __init__(self, d, valid_columns=None): + self.update(d, valid_columns=valid_columns) def __getattr__(self, key): if self.__dict__.has_key(key): return self.__dict__[key] - if key in self.get_table_columns(): + if key!= "_valid_columns" and key in self.get_valid_columns(): return None raise AttributeError(key) - def update(self, d): + def update(self, d, valid_columns=None): + if valid_columns: + self.__dict__["_valid_columns"] = valid_columns if "doctype" in d: self.set("doctype", d.get("doctype")) for key, value in d.iteritems(): self.set(key, value) - def get(self, key=None, default=None): + def get(self, key=None, filters=None, limit=None, default=None): if key: - return self.__dict__.get(key, default) + if filters: + return _filter(self.__dict__.get(key), filters, limit=limit) + else: + return self.__dict__.get(key, default) else: return self.__dict__ - def set(self, key, value): + def set(self, key, value, valid_columns=None): + if isinstance(value, list): + tmp = [] + for v in value: + tmp.append(self._init_child(v, key, valid_columns)) + value = tmp + + self.__dict__[key] = value + + def append(self, key, value): 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): + else: + raise ValueError + + def extend(self, key, value): + if isinstance(value, list): for v in value: - self.set(key, v) + self.append(v) else: - self.__dict__[key] = value + raise ValueError - def _init_child(self, value, key): + def _init_child(self, value, key, valid_columns=None): if not self.doctype: return value if not isinstance(value, BaseDocument): if not value.get("doctype"): - value["doctype"] = self.meta.get({"fieldname":key})[0].options + value["doctype"] = self.get_table_field_doctype(key) if not value.get("doctype"): raise AttributeError, key - value = BaseDocument(value) + value = BaseDocument(value, valid_columns=valid_columns) value.parent = self.name value.parenttype = self.doctype value.parentfield = key if not value.idx: - value.idx = len(self.get(key)) + value.idx = len(self.get(key) or []) + 1 return value @@ -79,24 +95,31 @@ class BaseDocument(object): def get_valid_dict(self): d = {} - for fieldname in self.table_columns: + for fieldname in self.valid_columns: d[fieldname] = self.get(fieldname) return d + + def get_for_save(self): + d = self.get_valid_dict() + @property - def table_columns(self): - return self.get_table_columns() + def valid_columns(self): + return self.get_valid_columns() - def get_table_columns(self): - if not hasattr(self, "_table_columns"): + def get_valid_columns(self): + if not hasattr(self, "_valid_columns"): doctype = self.__dict__.get("doctype") - self._table_columns = default_fields[1:] + \ - [df.fieldname for df in frappe.get_meta(doctype).get_docfields() + self._valid_columns = default_fields[1:] + \ + [df.fieldname for df in frappe.get_meta(doctype).get("fields") if df.fieldtype in type_map] - return self._table_columns + return self._valid_columns + + def get_table_field_doctype(self, fieldname): + return self.meta.get("fields", {"fieldname":fieldname})[0].options - def insert_row(self): + def db_insert(self): set_new_name(self) d = self.get_valid_dict() columns = d.keys() @@ -106,9 +129,19 @@ class BaseDocument(object): columns = ", ".join(["`"+c+"`" for c in columns]), values = ", ".join(["%s"] * len(columns)) ), d.values()) + self.set("__islocal", False) + + def db_update(self): + d = self.get_valid_dict() + columns = d.keys() + frappe.db.sql("""update `tab{doctype}` + set {values} where name=%s""".format( + doctype = self.doctype, + values = ", ".join(["`"+c+"`=%s" for c in columns]) + ), d.values() + [d.get("name")]) def fix_numeric_types(self): - for df in self.meta.get_docfields(): + for df in self.meta.get("fields"): if df.fieldtype in ("Int", "Check"): self.set(df.fieldname, cint(self.get(df.fieldname))) elif df.fieldtype in ("Float", "Currency"): @@ -137,7 +170,7 @@ class BaseDocument(object): missing = [] - for df in self.meta.get({"doctype": "DocField", "reqd": 1}): + for df in self.meta.get("fields", {"reqd": 1}): if self.get(df.fieldname) in (None, []): missing.append((df.fieldname, get_msg(df))) @@ -193,17 +226,25 @@ class Document(BaseDocument): raise frappe.DataError("Document({0}, {1})".format(arg1, arg2)) def load_from_db(self): - if self.meta[0].issingle: + if not getattr(self, "_metaclass", False) and self.meta.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, + self.update(d, valid_columns = d.keys()) + + for df in self.get_table_fields(): + children = frappe.db.get_values(df.options, {"parent": self.name, "parenttype": self.doctype, "parentfield": df.fieldname}, "*", as_dict=True) - self.update(d) + if children: + self.set(df.fieldname, children, children[0].keys()) + else: + self.set(df.fieldname, []) + + def get_table_fields(self): + return self.meta.get('fields', {"fieldtype":"Table"}) def insert(self): # check links @@ -215,16 +256,34 @@ class Document(BaseDocument): # run validate, on update etc. # parent - if self.meta[0].issingle: + if self.meta.issingle: self.update_single(self.get_valid_dict()) else: - self.insert_row() + self.db_insert() # children for d in self.get_all_children(): d.parent = self.name - d.insert_row() + d.db_insert() + + def save(self): + if self.get("__islocal") or not self.get("name"): + self.insert() + return + + self._validate() + + # parent + if self.meta.issingle: + self.update_single(self.get_valid_dict()) + else: + self.db_update() + # children + for d in self.get_all_children(): + d.parent = self.name + d.db_update() + def update_single(self, d): frappe.db.sql("""delete from tabSingles where doctype=%s""", d.get("doctype")) for field, value in d.iteritems(): @@ -241,7 +300,7 @@ class Document(BaseDocument): self.set_missing_values(new_doc) # children - for df in self.meta.get({"fieldtype":"Table"}): + for df in self.meta.get("fields", {"fieldtype":"Table"}): new_doc = frappe.new_doc(df.options).fields value = self.get(df.fieldname) if isinstance(value, list): @@ -249,12 +308,60 @@ class Document(BaseDocument): d.set_missing_values(new_doc) def _validate(self): - self.trigger("validate") + #self.check_if_latest() self.validate_mandatory() self.validate_links() # check restrictions + def check_if_latest(self, method="save"): + conflict = False + if not self.get('__islocal'): + if self.meta.issingle: + modified = frappe.db.get_value(self.doctype, self.name, "modified") + if cstr(modified) and cstr(modified) != cstr(self.modified): + conflict = True + else: + tmp = frappe.db.sql("""select modified, docstatus from `tab%s` + where name=%s for update""" + % (self.doctype, '%s'), self.name, as_dict=True) + + if not tmp: + frappe.msgprint("""This record does not exist. Please refresh.""", raise_exception=1) + + modified = cstr(tmp[0].modified) + if modified and modified != cstr(self.modified): + conflict = True + + self.check_docstatus_transition(tmp[0].docstatus, method) + + if conflict: + frappe.msgprint(_("Error: Document has been modified after you have opened it") \ + + (" (%s, %s). " % (modified, self.modified)) \ + + _("Please refresh to get the latest document."), raise_exception=TimestampMismatchError) + + def check_docstatus_transition(self, db_docstatus): + valid = { + "save": [0,0], + "submit": [0,1], + "cancel": [1,2], + "update_after_submit": [1,1] + } + + labels = { + 0: _("Draft"), + 1: _("Submitted"), + 2: _("Cancelled") + } + + if not hasattr(self, "to_docstatus"): + self.to_docstatus = 0 + + if [db_docstatus, self.to_docstatus] != valid[method]: + frappe.msgprint(_("Cannot change from") + ": " + labels[db_docstatus] + " > " + \ + labels[self.to_docstatus], raise_exception=DocstatusTransitionError) + + def validate_mandatory(self): if self.get("ignore_mandatory"): return @@ -288,11 +395,45 @@ class Document(BaseDocument): def get_all_children(self): ret = [] - for df in self.meta.get({"fieldtype": "Table"}): + for df in self.meta.get("fields", {"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 + return + +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 = [] + + for d in data: + add = True + for f in filters: + fval = filters[f] + + if fval is True: + fval = ["not None", fval] + elif fval is False: + fval = ["None", fval] + elif not isinstance(fval, (tuple, list)): + if isinstance(fval, basestring) and fval.startswith("^"): + fval = ["^", fval[1:]] + else: + fval = ["=", fval] + + if not frappe.compare(d.get(f), fval[0], fval[1]): + add = False + break + + if add: + out.append(d) + if limit and (len(out)-1)==limit: + break + + return out \ No newline at end of file diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 51be1c2c2d..00c5b6eb09 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -6,7 +6,118 @@ from __future__ import unicode_literals import frappe from frappe.utils import cstr, cint +from frappe.model import integer_docfield_properties +from frappe.model.document import Document + +###### + +def get_meta(doctype, cached=True): + if cached: + if doctype not in frappe.local.meta: + frappe.local.meta[doctype] = frappe.cache().get_value("meta:" + doctype, lambda: Meta(doctype)) + return frappe.local.meta.get(doctype) + else: + return Meta(doctype) + +class Meta(Document): + _metaclass = True + def __init__(self, doctype): + super(Meta, self).__init__("DocType", doctype) + + def get_link_fields(self): + tmp = self.get("fields", {"fieldtype":"Link"}) + tmp.extend(self.get("fields", {"fieldtype":"Select", "options": "^link:"})) + return tmp + + def get_table_fields(self): + return [ + frappe._dict({"fieldname": "fields", "options": "DocField"}), + frappe._dict({"fieldname": "permissions", "options": "DocPerm"}) + ] + + def get_valid_columns(self): + if not hasattr(self, "_valid_columns"): + doctype = self.__dict__.get("doctype") + self._valid_columns = frappe.db.get_table_columns(doctype) + + return self._valid_columns + + def get_table_field_doctype(self, fieldname): + return { "fields": "DocField", "permissions": "DocPerm"}.get(fieldname) + + def process(self): + self.add_custom_fields() + self.apply_property_setters() + self.sort_fields() + + def add_custom_fields(self): + try: + self.extend("fields", frappe.db.sql("""SELECT * FROM `tabCustom Field` + WHERE dt = %s AND docstatus < 2""", (doctype,), as_dict=1)) + except Exception, e: + if e.args[0]==1146: + return + else: + raise + def apply_property_setters(self): + for ps in frappe.db.sql("""select * from `tabProperty Setter` where + doc_type=%s""", (doctype,), as_dict=1): + if ps['doctype_or_field']=='DocType': + if ps.get('property_type', None) in ('Int', 'Check'): + ps['value'] = cint(ps['value']) + + self.set(ps["property"], ps["value"]) + else: + docfield = self.get("fields", {"fieldname":ps["fieldname"]}, limit=1)[0] + + if not docfield: continue + if ps["property"] in integer_docfield_properties: + ps['value'] = cint(ps['value']) + + docfield.set(ps["property"], ps["value"]) + + def sort_fields(self): + """sort on basis of previous_field""" + newlist = [] + pending = self.get("fields") + + if self.get("_idx"): + for fieldname in json.loads(self.get("_idx")): + d = self.get("fields", {"fieldname": fieldname}, limit=1) + if d: + newlist.append(d[0]) + pending.remove(d[0]) + else: + maxloops = 20 + while (pending and maxloops>0): + maxloops -= 1 + for d in pending[:]: + if d.previous_field: + # field already added + for n in newlist: + if n.fieldname==d.previous_field: + newlist.insert(newlist.index(n)+1, d) + pending.remove(d) + break + else: + newlist.append(d) + pending.remove(d) + + # recurring at end + if pending: + newlist += pending + + # renum + idx = 1 + for d in newlist: + d.idx = idx + idx += 1 + + self.set("fields", newlist) + +####### + def is_single(doctype): try: return frappe.db.get_value("DocType", doctype, "issingle") diff --git a/frappe/model/naming.py b/frappe/model/naming.py index e90e812473..1540c80deb 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -48,7 +48,7 @@ def set_new_name(doc): doc.name = doc.get('__newname') # default name for table - elif doc.meta[0].istable: + elif doc.meta.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 8e5eada0ea..ed5a678e29 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -35,6 +35,7 @@ class TestDocument(unittest.TestCase): # test if default values are added self.assertEquals(d.send_reminder, 1) + return d def test_insert_with_child(self): d = Document({ @@ -55,7 +56,14 @@ class TestDocument(unittest.TestCase): d1 = Document("Event", d.name) self.assertTrue(d1.event_individuals[0].person, "Administrator") - + + def test_update(self): + d = self.test_insert() + d.subject = "subject changed" + d.save() + + self.assertEquals(frappe.db.get_value(d.doctype, d.name, "subject"), "subject changed") + def test_mandatory(self): d = Document({ "doctype": "User",