diff --git a/frappe/__init__.py b/frappe/__init__.py index 91a0dfc812..cf15c12812 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -346,6 +346,10 @@ def set_value(doctype, docname, fieldname, value): def get_doclist(doctype, name=None): return bean(doctype, name).doclist +def get_doc(arg1, arg2=None): + import frappe.model.document + return frappe.model.document.get_doc(arg1, arg2) + def get_doctype(doctype, processed=False): import frappe.model.doctype return frappe.model.doctype.get(doctype, processed) diff --git a/frappe/core/doctype/event/event.py b/frappe/core/doctype/event/event.py index aa4312e686..9e99b08f0a 100644 --- a/frappe/core/doctype/event/event.py +++ b/frappe/core/doctype/event/event.py @@ -6,9 +6,15 @@ import frappe from frappe.utils import getdate, cint, add_months, date_diff, add_days, nowdate from frappe.core.doctype.user.user import STANDARD_USERS +from frappe.model.document import Document weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] +class Event(Document): + def validate(self): + if self.starts_on and self.ends_on and self.starts_on > self.ends_on: + frappe.msgprint(frappe._("Event End must be after Start"), raise_exception=True) + class DocType: def __init__(self, d, dl): self.doc, self.doclist = d, dl diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py new file mode 100644 index 0000000000..65c012bcef --- /dev/null +++ b/frappe/model/base_document.py @@ -0,0 +1,242 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _, msgprint +from frappe.utils import cint, flt, cstr, now +from frappe.model import default_fields +from frappe.model.db_schema import type_map +from frappe.model.naming import set_new_name + +class BaseDocument(object): + 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!= "_valid_columns" and key in self.get_valid_columns(): + return None + + raise AttributeError(key) + + 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, filters=None, limit=None, default=None): + if key: + 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, 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): + if not self.get(key): + self.__dict__[key] = [] + self.get(key).append(self._init_child(value, key)) + else: + raise ValueError + + def extend(self, key, value): + if isinstance(value, list): + for v in value: + self.append(v) + else: + raise ValueError + + 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.get_table_field_doctype(key) + if not value.get("doctype"): + raise AttributeError, key + 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) or []) + 1 + + return value + + @property + def doc(self): + return self + + @property + def meta(self): + if not self.get("_meta"): + self._meta = frappe.get_meta(self.doctype) + return self._meta + + def get_valid_dict(self): + d = {} + for fieldname in self.valid_columns: + d[fieldname] = self.get(fieldname) + return d + + @property + def valid_columns(self): + return self.get_valid_columns() + + def get_valid_columns(self): + if not hasattr(self, "_valid_columns"): + doctype = self.__dict__.get("doctype") + 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._valid_columns + + def get_table_field_doctype(self, fieldname): + return self.meta.get("fields", {"fieldname":fieldname})[0].options + + def db_insert(self): + set_new_name(self) + d = self.get_valid_dict() + columns = d.keys() + 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)) + ), 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("fields"): + if df.fieldtype in ("Int", "Check"): + self.set(df.fieldname, cint(self.get(df.fieldname))) + elif df.fieldtype in ("Float", "Currency"): + self.set(df.fieldname, flt(self.get(df.fieldname))) + + 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("fields", {"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 + + def _validate_constants(self): + if frappe.flags.in_import: + 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: + if self.get(fieldname) != values.get(fieldname): + frappe.throw("{0}: {1}".format(_("Value cannot be changed for"), + _(meta.get("fields", {"fieldname":fieldname})[0].label)), + frappe.CannotChangeConstantError) + +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 diff --git a/frappe/model/code.py b/frappe/model/code.py index c0263dbef9..9972cbfc62 100644 --- a/frappe/model/code.py +++ b/frappe/model/code.py @@ -17,7 +17,7 @@ methods in following modules are imported for backward compatibility """ import frappe -from frappe.modules import get_doctype_module +from frappe.modules import get_doctype_module, load_doctype_module, get_module_name import frappe.model.doc def get_obj(dt = None, dn = None, doc=None, doclist=None, with_children = 0): @@ -43,18 +43,6 @@ def get_server_obj(doc, doclist = [], basedoctype = ''): module = get_doctype_module(doc.doctype) return load_doctype_module(doc.doctype, module).DocType(doc, doclist) -def load_doctype_module(doctype, module=None, prefix=""): - if not module: - module = get_doctype_module(doctype) - return frappe.get_module(get_module_name(doctype, module, prefix)) - -def get_module_name(doctype, module, prefix=""): - from frappe.modules import scrub - return '{app}.{module}.doctype.{doctype}.{prefix}{doctype}'.format(\ - app = scrub(frappe.local.module_app[scrub(module)]), - module = scrub(module), doctype = scrub(doctype), prefix=prefix) - - def run_server_obj(server_obj, method_name, arg=None): """ Executes a method (`method_name`) from the given object (`server_obj`) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index c612c16742..96a13b97e8 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -14,7 +14,6 @@ class DatabaseQuery(object): def __init__(self, doctype): self.doctype = doctype self.tables = [] - self.meta = [] self.conditions = [] self.ignore_permissions = False self.fields = ["name"] @@ -53,7 +52,6 @@ class DatabaseQuery(object): def prepare_args(self): self.parse_args() self.extract_tables() - self.load_metadata() self.remove_user_tags() self.build_conditions() @@ -111,20 +109,13 @@ class DatabaseQuery(object): if not table_name[0]=='`': table_name = '`' + table_name + '`' if not table_name in self.tables: - self.tables.append(table_name) + self.append_table(table_name) - def load_metadata(self): - """load all doctypes and roles""" - self.meta = {} - - for t in self.tables: - if t.startswith('`'): - doctype = t[4:-1] - if self.meta.get(doctype): - continue - if (not self.ignore_permissions) and (not frappe.has_permission(doctype)): - raise frappe.PermissionError, doctype - self.meta[doctype] = frappe.model.doctype.get(doctype) + def append_table(self, table_name): + self.tables.append(table_name) + doctype = table_name[4:-1] + if (not self.ignore_permissions) and (not frappe.has_permission(doctype)): + raise frappe.PermissionError, doctype def remove_user_tags(self): """remove column _user_tags if not in table""" @@ -170,10 +161,7 @@ class DatabaseQuery(object): tname = ('`tab' + f[0] + '`') if not tname in self.tables: - self.tables.append(tname) - - if not tname in self.meta: - self.load_metadata() + self.append_table(tname) # prepare in condition if f[2] in ['in', 'not in']: @@ -184,7 +172,7 @@ class DatabaseQuery(object): f[3] = "(" + ', '.join(opts) + ")" self.conditions.append('ifnull(' + tname + '.' + f[1] + ", '') " + f[2] + " " + f[3]) else: - df = self.meta[f[0]].get({"doctype": "DocField", "fieldname": f[1]}) + df = frappe.get_meta(f[0]).get("fields", {"fieldname": f[1]}) if f[2] == "like" or (isinstance(f[3], basestring) and (not df or df[0].fieldtype not in ["Float", "Int", "Currency", "Percent"])): @@ -216,10 +204,9 @@ class DatabaseQuery(object): self.or_conditions = [] if not self.tables: self.extract_tables() - if not self.meta: self.load_metadata() # explict permissions - restricted_by_user = frappe.permissions.get_user_perms(self.meta[self.doctype]).restricted + restricted_by_user = frappe.permissions.get_user_perms(frappe.get_meta(self.doctype)).restricted # get restrictions restrictions = frappe.defaults.get_restrictions() @@ -238,7 +225,7 @@ class DatabaseQuery(object): return self.match_filters def add_restrictions(self, restrictions): - fields_to_check = self.meta[self.doctype].get_restricted_fields(restrictions.keys()) + fields_to_check = frappe.get_meta(self.doctype).get_restricted_fields(restrictions.keys()) if self.doctype in restrictions: fields_to_check.append(frappe._dict({"fieldname":"name", "options":self.doctype})) diff --git a/frappe/model/db_schema.py b/frappe/model/db_schema.py index f77a9c053c..fa6240dca1 100644 --- a/frappe/model/db_schema.py +++ b/frappe/model/db_schema.py @@ -23,7 +23,7 @@ type_map = { ,'Code': ('text', '') ,'Text Editor': ('text', '') ,'Date': ('date', '') - ,'Datetime': ('datetime', '') + ,'Datetime': ('datetime', '6') ,'Time': ('time', '') ,'Text': ('text', '') ,'Data': ('varchar', '255') @@ -78,8 +78,8 @@ class DbTable: # create table frappe.db.sql("""create table `%s` ( name varchar(255) not null primary key, - creation datetime, - modified datetime, + creation datetime(6), + modified datetime(6), modified_by varchar(40), owner varchar(60), docstatus int(1) default '0', diff --git a/frappe/model/document.py b/frappe/model/document.py index df2ff10eee..b3169da79d 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -5,196 +5,27 @@ from __future__ import unicode_literals import frappe from frappe import _, msgprint from frappe.utils import cint, flt, cstr, now -from frappe.model import default_fields -from frappe.model.db_schema import type_map -from frappe.model.naming import set_new_name +from frappe.modules import load_doctype_module +from frappe.model.base_document import BaseDocument -# save / update # once_only validation -# permissions # methods -# timestamps and docstatus -class BaseDocument(object): - 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!= "_valid_columns" and key in self.get_valid_columns(): - return None - - raise AttributeError(key) - - 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_doc(arg1, arg2=None): + if isinstance(arg1, basestring): + doctype = arg1 + else: + doctype = arg1.get("doctype") - def get(self, key=None, filters=None, limit=None, default=None): - if key: - 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, 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): - if not self.get(key): - self.__dict__[key] = [] - self.get(key).append(self._init_child(value, key)) - else: - raise ValueError - - def extend(self, key, value): - if isinstance(value, list): - for v in value: - self.append(v) - else: - raise ValueError + module = load_doctype_module(doctype) + classname = doctype.replace(" ", "") + if hasattr(module, classname): + _class = getattr(module, classname) + if issubclass(_class, Document): + return getattr(module, classname)(arg1, arg2) - 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.get_table_field_doctype(key) - if not value.get("doctype"): - raise AttributeError, key - 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) or []) + 1 - - return value + return Document(arg1, arg2) - @property - def meta(self): - if not self.get("_meta"): - self._meta = frappe.get_meta(self.doctype) - return self._meta - - def get_valid_dict(self): - d = {} - for fieldname in self.valid_columns: - d[fieldname] = self.get(fieldname) - return d - - @property - def valid_columns(self): - return self.get_valid_columns() - - def get_valid_columns(self): - if not hasattr(self, "_valid_columns"): - doctype = self.__dict__.get("doctype") - 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._valid_columns - - def get_table_field_doctype(self, fieldname): - return self.meta.get("fields", {"fieldname":fieldname})[0].options - - def db_insert(self): - set_new_name(self) - d = self.get_valid_dict() - columns = d.keys() - 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)) - ), 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("fields"): - if df.fieldtype in ("Int", "Check"): - self.set(df.fieldname, cint(self.get(df.fieldname))) - elif df.fieldtype in ("Float", "Currency"): - self.set(df.fieldname, flt(self.get(df.fieldname))) - - 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("fields", {"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 @@ -224,7 +55,7 @@ class Document(BaseDocument): def load_from_db(self): if not getattr(self, "_metaclass", False) and self.meta.issingle: self.update(frappe.db.get_singles_dict(self.doctype)) - self.fix_numeric_types() + self._fix_numeric_types() else: d = frappe.db.get_value(self.doctype, self.name, "*", as_dict=1) @@ -241,13 +72,24 @@ class Document(BaseDocument): def get_table_fields(self): return self.meta.get('fields', {"fieldtype":"Table"}) + + def has_permission(self, permtype): + if getattr(self, "_ignore_permissions", False): + return True + return frappe.has_permission(self.doctype, permtype, self) def insert(self): # check links # check permissions + self.set("__islocal", True) + if not self.has_permission("create"): + raise frappe.PermissionError self._set_defaults() self._set_docstatus_user_and_timestamp() + self._check_if_latest() + self.run_method("before_insert") + self.run_before_save_methods() self._validate() # run validate, on update etc. @@ -262,13 +104,20 @@ class Document(BaseDocument): for d in self.get_all_children(): d.parent = self.name d.db_insert() + self.run_method("after_insert") + self.run_post_save_methods() def save(self): if self.get("__islocal") or not self.get("name"): self.insert() return + if not self.has_permission("write"): + raise frappe.PermissionError + self._set_docstatus_user_and_timestamp() + self._check_if_latest() + self.run_before_save_methods() self._validate() # parent @@ -278,10 +127,14 @@ class Document(BaseDocument): self.db_update() # children + ignore_children_type = self.get("_ignore_children_type", []) for d in self.get_all_children(): - d.parent = self.name - d.db_update() - + if d.doctype not in _ignore_children_type: + d.parent = self.name + d.db_update() + + self.run_post_save_methods() + def update_single(self, d): frappe.db.sql("""delete from tabSingles where doctype=%s""", self.doctype) for field, value in d.iteritems(): @@ -308,6 +161,14 @@ class Document(BaseDocument): d.owner = self.owner if not d.creation: d.creation = self.creation + + def _validate(self): + self._validate_mandatory() + self._validate_links() + self._validate_constants() + for d in self.get_all_children(): + d._validate_constants() + self._extract_images_from_text_editor() def _set_defaults(self): if frappe.flags.in_import: @@ -323,16 +184,10 @@ class Document(BaseDocument): if isinstance(value, list): for d in value: d.set_missing_values(new_doc) - - def _validate(self): - self.check_if_latest() - self.validate_mandatory() - self.validate_links() - - # check restrictions - - def check_if_latest(self): + + def _check_if_latest(self): conflict = False + self._action = "save" if not self.get('__islocal'): if self.meta.issingle: modified = frappe.db.get_value(self.doctype, self.name, "modified") @@ -366,6 +221,8 @@ class Document(BaseDocument): self._action = "save" elif self.docstatus==1: self._action = "submit" + if not self.has_permission("submit"): + raise frappe.PermissionError else: raise frappe.DocstatusTransitionError @@ -373,8 +230,12 @@ class Document(BaseDocument): if self.docstatus==1: self._action = "update_after_submit" self.validate_update_after_submit() + if not self.has_permission("submit"): + raise frappe.PermissionError elif self.docstatus==2: self._action = "cancel" + if not self.has_permission("cancel"): + raise frappe.PermissionError else: raise frappe.DocstatusTransitionError @@ -385,13 +246,13 @@ class Document(BaseDocument): # check only allowed values are updated pass - def validate_mandatory(self): + def _validate_mandatory(self): if self.get("ignore_mandatory"): return - missing = self.get_missing_mandatory_fields() + missing = self._get_missing_mandatory_fields() for d in self.get_all_children(): - missing.extend(d.get_missing_mandatory_fields()) + missing.extend(d._get_missing_mandatory_fields()) if not missing: return @@ -401,7 +262,7 @@ class Document(BaseDocument): raise frappe.MandatoryError(", ".join((each[0] for each in missing))) - def validate_links(self): + def _validate_links(self): if self.get("ignore_links"): return @@ -423,40 +284,51 @@ class Document(BaseDocument): if isinstance(value, list): ret.extend(value) return ret + + def _extract_images_from_text_editor(self): + from frappe.utils.file_manager import extract_images_from_html + if self.doctype != "DocType": + for df in self.meta.get("fields", {"fieldtype":"Text Editor"}): + extract_images_from_html(self, df.fieldname) - def trigger(self, func, *args, **kwargs): - return + def run_method(self, method, *args, **kwargs): + """run standard triggers, plus those in frappe""" + def add_to_response(out, new_response): + if isinstance(new_response, dict): + out.update(new_response) + + if hasattr(self, method): + add_to_response(frappe.local.response, + getattr(self, method)(*args, **kwargs)) + + args = [self, method] + list(args or []) + + for handler in frappe.get_hooks("bean_event:" + self.doctype + ":" + method) \ + + frappe.get_hooks("bean_event:*:" + method): + add_to_response(frappe.local.response, + frappe.call(frappe.get_attr(handler), *args, **kwargs)) -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) }""" + return frappe.local.response - 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 + def run_before_save_methods(self): + if self._action=="save": + self.run_method("validate") + self.run_method("before_save") + elif self._action=="submit": + self.run_method("validate") + self.run_method("before_submit") + elif self._action=="cancel": + self.run_method("before_cancel") + elif self._action=="update_after_submit": + self.run_method("before_update_after_submit") - if add: - out.append(d) - if limit and (len(out)-1)==limit: - break - - return out \ No newline at end of file + def run_post_save_methods(self): + if self._action=="save": + self.run_method("on_update") + elif self._action=="submit": + self.run_method("on_update") + self.run_method("on_submit") + elif self._action=="cancel": + self.run_method("on_cancel") + elif self._action=="update_after_submit": + self.run_method("on_update_after_submit") diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 00c5b6eb09..cf694e750d 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -115,6 +115,20 @@ class Meta(Document): idx += 1 self.set("fields", newlist) + + def get_restricted_fields(self, restricted_types): + restricted_fields = self.get("fields", { + "fieldtype":"Link", + "parent": self.name, + "ignore_restrictions":("!=", 1), + "options":("in", restricted_types) + }) + if self.name in restricted_types: + restricted_fields.append(frappe._dict({ + "label":"Name", "fieldname":"name", "options": self.name + })) + return restricted_fields + ####### diff --git a/frappe/modules/__init__.py b/frappe/modules/__init__.py index edc71b717c..f9f706133c 100644 --- a/frappe/modules/__init__.py +++ b/frappe/modules/__init__.py @@ -44,3 +44,14 @@ def export_doc(doctype, name, module=None): def get_doctype_module(doctype): return frappe.db.get_value('DocType', doctype, 'module') or "core" + +def load_doctype_module(doctype, module=None, prefix=""): + if not module: + module = get_doctype_module(doctype) + return frappe.get_module(get_module_name(doctype, module, prefix)) + +def get_module_name(doctype, module, prefix=""): + from frappe.modules import scrub + return '{app}.{module}.doctype.{doctype}.{prefix}{doctype}'.format(\ + app = scrub(frappe.local.module_app[scrub(module)]), + module = scrub(module), doctype = scrub(doctype), prefix=prefix) diff --git a/frappe/patches.txt b/frappe/patches.txt index 655217e402..f87f01ebfa 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -22,3 +22,4 @@ frappe.patches.4_0.private_backups frappe.patches.4_0.set_module_in_report frappe.patches.4_0.remove_old_parent frappe.patches.4_0.rename_profile_to_user +frappe.patches.4_0.update_datetime diff --git a/frappe/patches/4_0/update_datetime.py b/frappe/patches/4_0/update_datetime.py new file mode 100644 index 0000000000..c87b2bee46 --- /dev/null +++ b/frappe/patches/4_0/update_datetime.py @@ -0,0 +1,9 @@ +import frappe + +def execute(): + for table in frappe.db.sql_list("show tables"): + for field in frappe.db.sql("desc `%s`" % table): + if field[1]=="datetime": + frappe.db.sql("alter table `%s` change `%s` `%s` datetime(6)" % \ + (table, field[0], field[0])) + \ No newline at end of file diff --git a/frappe/permissions.py b/frappe/permissions.py index a8a2241f16..e99e46a2ba 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -19,12 +19,12 @@ def has_permission(doctype, ptype="read", refdoc=None, verbose=True): if frappe.is_table(doctype): return True - meta = frappe.get_doctype(doctype) + meta = frappe.get_meta(doctype) - if ptype=="submit" and not cint(meta[0].is_submittable): + if ptype=="submit" and not cint(meta.is_submittable): return False - if ptype=="import" and not cint(meta[0].allow_import): + if ptype=="import" and not cint(meta.allow_import): return False if frappe.session.user=="Administrator": @@ -36,24 +36,26 @@ def has_permission(doctype, ptype="read", refdoc=None, verbose=True): if refdoc: if isinstance(refdoc, basestring): - refdoc = frappe.doc(meta[0].name, refdoc) + refdoc = frappe.doc(meta.name, refdoc) if not has_unrestricted_access(meta, refdoc, verbose=verbose): return False - if not has_additional_permission(refdoc): + if not has_controller_permissions(refdoc): return False return True def get_user_perms(meta, user=None): - cache_key = (meta[0].name, user) + if not user: + user = frappe.session.user + cache_key = (meta.name, user) if not frappe.local.user_perms.get(cache_key): perms = frappe._dict() user_roles = frappe.get_roles(user) - for p in meta.get({"doctype": "DocPerm"}): - if cint(p.permlevel)==0 and (p.role=="All" or p.role in user_roles): + for p in meta.permissions: + if cint(p.permlevel)==0 and (p.role in user_roles): for ptype in rights: if ptype == "restricted": perms[ptype] = perms.get(ptype, 1) and cint(p.get(ptype)) @@ -100,8 +102,8 @@ def has_unrestricted_access(meta, refdoc, verbose=True): # check all restrictions before returning return False if has_restricted_data else True -def has_additional_permission(doc): - if doc.fields.get("__islocal"): +def has_controller_permissions(doc): + if doc.get("__islocal"): bean = frappe.bean([doc]) else: bean = frappe.bean(doc.doctype, doc.name) diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 5496290e06..965501bcfe 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -3,11 +3,9 @@ import frappe, unittest, time -from frappe.model.document import Document - class TestDocument(unittest.TestCase): def test_load(self): - d = Document("DocType", "User") + d = frappe.get_doc("DocType", "User") self.assertEquals(d.doctype, "DocType") self.assertEquals(d.name, "User") self.assertEquals(d.allow_rename, 1) @@ -16,13 +14,13 @@ class TestDocument(unittest.TestCase): self.assertTrue(filter(lambda d: d.fieldname=="email", d.fields)) def test_load_single(self): - d = Document("Website Settings", "Website Settings") + d = frappe.get_doc("Website Settings", "Website Settings") self.assertEquals(d.name, "Website Settings") self.assertEquals(d.doctype, "Website Settings") self.assertTrue(d.disable_signup in (0, 1)) def test_insert(self): - d = Document({ + d = frappe.get_doc({ "doctype":"Event", "subject":"_Test Event 1", "starts_on": "2014-01-01", @@ -38,7 +36,7 @@ class TestDocument(unittest.TestCase): return d def test_insert_with_child(self): - d = Document({ + d = frappe.get_doc({ "doctype":"Event", "subject":"_Test Event 2", "starts_on": "2014-01-01", @@ -54,7 +52,7 @@ class TestDocument(unittest.TestCase): self.assertEquals(frappe.db.get_value("Event", d.name, "subject"), "_Test Event 2") - d1 = Document("Event", d.name) + d1 = frappe.get_doc("Event", d.name) self.assertTrue(d1.event_individuals[0].person, "Administrator") def test_update(self): @@ -65,7 +63,7 @@ class TestDocument(unittest.TestCase): self.assertEquals(frappe.db.get_value(d.doctype, d.name, "subject"), "subject changed") def test_mandatory(self): - d = Document({ + d = frappe.get_doc({ "doctype": "User", "email": "test_mandatory@example.com", }) @@ -77,20 +75,29 @@ class TestDocument(unittest.TestCase): def test_confict_validation(self): d1 = self.test_insert() - d2 = Document(d1.doctype, d1.name) - time.sleep(1) + d2 = frappe.get_doc(d1.doctype, d1.name) d1.save() self.assertRaises(frappe.TimestampMismatchError, d2.save) def test_confict_validation_single(self): - d1 = Document("Website Settings", "Website Settings") - d2 = Document("Website Settings", "Website Settings") - time.sleep(1) + d1 = frappe.get_doc("Website Settings", "Website Settings") + d2 = frappe.get_doc("Website Settings", "Website Settings") d1.save() self.assertRaises(frappe.TimestampMismatchError, d2.save) + + def test_permission(self): + frappe.set_user("Guest") + d = self.assertRaises(frappe.PermissionError, self.test_insert) + frappe.set_user("Administrator") + + def test_permission_single(self): + frappe.set_user("Guest") + d = frappe.get_doc("Website Settings", "Website Settigns") + self.assertRaises(frappe.PermissionError, d.save) + frappe.set_user("Administrator") def test_link_validation(self): - d = Document({ + d = frappe.get_doc({ "doctype": "User", "email": "test_link_validation@example.com", "first_name": "Link Validation", @@ -106,4 +113,12 @@ class TestDocument(unittest.TestCase): "role": "System Manager" }) d.insert() - self.assertEquals(frappe.db.get_value("User", d.name), d.name) \ No newline at end of file + self.assertEquals(frappe.db.get_value("User", d.name), d.name) + + def test_validate(self): + d = self.test_insert() + d.starts_on = "2014-01-01" + d.ends_on = "2013-01-01" + self.assertRaises(frappe.ValidationError, d.validate) + self.assertRaises(frappe.ValidationError, d.save) + diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 7711a84372..d6b3500122 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -177,7 +177,7 @@ def now(): return getdate(frappe.local.current_date).strftime("%Y-%m-%d") + " " + \ now_datetime().strftime('%H:%M:%S') else: - return now_datetime().strftime('%Y-%m-%d %H:%M:%S') + return now_datetime().strftime('%Y-%m-%d %H:%M:%S.%f') def nowdate(): """return current date as yyyy-mm-dd""" @@ -217,7 +217,7 @@ def get_datetime(datetime_str): if isinstance(datetime_str, datetime): return datetime_str.replace(microsecond=0, tzinfo=None) - return datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S') + return datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S.%f') def get_datetime_str(datetime_obj): if isinstance(datetime_obj, basestring):