Rushabh Mehta 11 년 전
committed by Anand Doshi
부모
커밋
1e364ba1c7
31개의 변경된 파일86개의 추가작업 그리고 2025개의 파일을 삭제
  1. +17
    -28
      frappe/__init__.py
  2. +2
    -2
      frappe/boot.py
  3. +1
    -1
      frappe/client.py
  4. +1
    -2
      frappe/core/doctype/customize_form/customize_form.py
  5. +2
    -2
      frappe/core/doctype/print_format/print_format.py
  6. +1
    -10
      frappe/core/doctype/user/test_user.py
  7. +3
    -5
      frappe/core/doctype/user/user.py
  8. +2
    -5
      frappe/core/doctype/workflow/workflow.py
  9. +1
    -1
      frappe/core/page/data_import_tool/data_import_tool.py
  10. +1
    -2
      frappe/core/page/messages/messages.py
  11. +0
    -1
      frappe/memc.py
  12. +3
    -3
      frappe/model/__init__.py
  13. +5
    -1
      frappe/model/base_document.py
  14. +0
    -521
      frappe/model/bean.py
  15. +1
    -1
      frappe/model/create_new.py
  16. +0
    -735
      frappe/model/doc.py
  17. +0
    -143
      frappe/model/doclist.py
  18. +0
    -425
      frappe/model/doctype.py
  19. +11
    -1
      frappe/model/document.py
  20. +2
    -2
      frappe/model/mapper.py
  21. +11
    -5
      frappe/model/meta.py
  22. +1
    -1
      frappe/model/naming.py
  23. +0
    -103
      frappe/model/utils.py
  24. +3
    -3
      frappe/test_runner.py
  25. +4
    -4
      frappe/translate.py
  26. +3
    -7
      frappe/utils/datautils.py
  27. +1
    -1
      frappe/website/doctype/blog_post/test_blog_post.py
  28. +1
    -1
      frappe/website/doctype/website_group/website_group.py
  29. +5
    -4
      frappe/widgets/form/utils.py
  30. +1
    -2
      frappe/widgets/reportview.py
  31. +3
    -3
      frappe/widgets/search.py

+ 17
- 28
frappe/__init__.py 파일 보기

@@ -15,7 +15,6 @@ import os, sys, importlib, inspect
import json
import semantic_version

from frappe.core.doctype.print_format.print_format import get_html as get_print_html
from .exceptions import *

local = Local()
@@ -315,8 +314,7 @@ def get_obj(dt = None, dn = None, doc=None, doclist=None, with_children = True):
return get_obj(dt, dn, doc, doclist, with_children)

def doc(doctype=None, name=None, fielddata=None):
from frappe.model.doc import Document
return Document(doctype, name, fielddata)
return Document(doctype, name, fielddata)

def new_doc(doctype, parent_doc=None, parentfield=None):
from frappe.model.create_new import get_new_doc
@@ -334,7 +332,7 @@ def bean(doctype=None, name=None, copy=None):
"""return an instance of the object, wrapped as a Bean (frappe.model.bean)"""
from frappe.model.bean import Bean
if copy:
return Bean(copy_doclist(copy))
return Bean(copy_doc(copy))
else:
return Bean(doctype, name)

@@ -348,10 +346,6 @@ def get_doclist(doctype, name=None):
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)
def get_meta(doctype, cached=True):
import frappe.model.meta
@@ -531,26 +525,21 @@ def import_doclist(path, ignore_links=False, ignore_insert=False, insert=False):
from frappe.core.page.data_import_tool import data_import_tool
data_import_tool.import_doclist(path, ignore_links=ignore_links, ignore_insert=ignore_insert, insert=insert)

def copy_doclist(in_doclist):
new_doclist = []
parent_doc = None
for i, d in enumerate(in_doclist):
is_dict = False
if isinstance(d, dict):
is_dict = True
values = _dict(d.copy())
else:
values = _dict(d.fields.copy())
newd = new_doc(values.doctype, parent_doc=(None if i==0 else parent_doc), parentfield=values.parentfield)
newd.fields.update(values)
if i==0:
parent_doc = newd
new_doclist.append(newd.fields if is_dict else newd)
return doclist(new_doclist)
def copy_doc(doc):
import copy
d = doc.as_dict()
newdoc = get_doc(copy.deepcopy(d))
newdoc.name = None
newdoc.set("__islocal", 1)
newdoc.owner = None
newdoc.creation = None
for d in newdoc.get_all_children():
d.name = None
d.parent = None
d.set("__islocal", 1)
d.owner = None
d.creation = None
return newdoc

def compare(val1, condition, val2):
import frappe.utils


+ 2
- 2
frappe/boot.py 파일 보기

@@ -126,7 +126,7 @@ def get_user(bootinfo):
"""get user info"""
bootinfo['user'] = frappe.user.load_user()
def add_home_page(bootinfo, doclist):
def add_home_page(bootinfo, docs):
"""load home page"""

if frappe.session.user=="Guest":
@@ -141,4 +141,4 @@ def add_home_page(bootinfo, doclist):
page = frappe.widgets.page.get('desktop')

bootinfo['home_page'] = page.name
doclist.append(page)
docs.append(page)

+ 1
- 1
frappe/client.py 파일 보기

@@ -55,7 +55,7 @@ def insert(doclist):
# inserting a child record
d = doclist[0]
bean = frappe.bean(d["parenttype"], d["parent"])
bean.doclist.append(d)
bean.append(d)
bean.save()
return [d]
else:


+ 1
- 2
frappe/core/doctype/customize_form/customize_form.py 파일 보기

@@ -267,8 +267,7 @@ class CustomizeForm(Document):

# If the above conditions are fulfilled,
# create a property setter doc, but dont save it yet.
from frappe.model.doc import Document
d = Document('Property Setter')
d = frappe.get_doc('Property Setter')
d.doctype_or_field = ref_d.doctype=='DocField' and 'DocField' or 'DocType'
d.doc_type = self.doc.doc_type
d.field_name = ref_d.fieldname


+ 2
- 2
frappe/core/doctype/print_format/print_format.py 파일 보기

@@ -75,12 +75,12 @@ def get_html(doc, doclist, print_format=None):

template = Environment().from_string(get_print_format_name(doc.doctype,
print_format or frappe.form_dict.format))
doctype = frappe.get_doctype(doc.doctype)
doctype = frappe.get_meta(doc.doctype)
args = {
"doc": doc,
"doclist": doclist,
"doctype": doctype,
"meta": meta,
"frappe": frappe,
"utils": frappe.utils
}


+ 1
- 10
frappe/core/doctype/user/test_user.py 파일 보기

@@ -46,16 +46,7 @@ class TestUser(unittest.TestCase):
frappe.db.set_value("Control Panel", "Control Panel", "_test", "_test_val")
self.assertEquals(frappe.db.get_value("Control Panel", None, "_test"), "_test_val")
self.assertEquals(frappe.db.get_value("Control Panel", "Control Panel", "_test"), "_test_val")
def test_doclist(self):
p_meta = frappe.get_doctype("User")
self.assertEquals(len(p_meta.get({"doctype": "DocField", "parent": "User", "fieldname": "first_name"})), 1)
self.assertEquals(len(p_meta.get({"doctype": "DocField", "parent": "User", "fieldname": "^first"})), 1)
self.assertEquals(len(p_meta.get({"fieldname": ["!=", "first_name"]})), len(p_meta) - 1)
self.assertEquals(len(p_meta.get({"fieldname": ["in", ["first_name", "last_name"]]})), 2)
self.assertEquals(len(p_meta.get({"fieldname": ["not in", ["first_name", "last_name"]]})), len(p_meta) - 2)
test_records = [
[


+ 3
- 5
frappe/core/doctype/user/user.py 파일 보기

@@ -54,9 +54,8 @@ class User(Document):
if self.doc.user_type == "System User" and not self.get_other_system_managers():
msgprint("""Adding System Manager Role as there must
be atleast one 'System Manager'.""")
self.doclist.append({
self.append("user_roles", {
"doctype": "UserRole",
"parentfield": "user_roles",
"role": "System Manager"
})
@@ -244,11 +243,10 @@ class User(Document):
def add_roles(self, *roles):
for role in roles:
if role in [d.role for d in self.doclist.get({"doctype":"UserRole"})]:
if role in [d.role for d in self.get("user_roles")]:
continue
self.bean.doclist.append({
self.append("user_roles", {
"doctype": "UserRole",
"parentfield": "user_roles",
"role": role
})


+ 2
- 5
frappe/core/doctype/workflow/workflow.py 파일 보기

@@ -18,10 +18,8 @@ class Workflow(Document):
def create_custom_field_for_workflow_state(self):
frappe.clear_cache(doctype=self.doc.document_type)
doctypeobj = frappe.get_doctype(self.doc.document_type)
if not len(doctypeobj.get({"doctype":"DocField",
"fieldname":self.doc.workflow_state_field})):
meta = frappe.get_meta(self.doc.document_type)
if not meta.get_field(self.doc.workflow_state_field):
# create custom field
frappe.bean([{
"doctype":"Custom Field",
@@ -32,7 +30,6 @@ class Workflow(Document):
"hidden": 1,
"fieldtype": "Link",
"options": "Workflow State",
#"insert_after": doctypeobj.get({"doctype":"DocField"})[-1].fieldname
}]).save()
frappe.msgprint("Created Custom Field '%s' in '%s'" % (self.doc.workflow_state_field,


+ 1
- 1
frappe/core/page/data_import_tool/data_import_tool.py 파일 보기

@@ -328,7 +328,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
doclist.append(d)
else:
break
return doclist
else:
d = frappe._dict(zip(columns, rows[start_idx][1:]))


+ 1
- 2
frappe/core/page/messages/messages.py 파일 보기

@@ -66,8 +66,7 @@ def post(arg=None):
import json
arg = json.loads(arg)

from frappe.model.doc import Document
d = Document('Comment')
d = frappe.get_doc('Comment')
d.parenttype = arg.get("parenttype")
d.comment = arg['txt']
d.comment_docname = arg['contact']


+ 0
- 1
frappe/memc.py 파일 보기

@@ -3,7 +3,6 @@
from __future__ import unicode_literals

import memcache, frappe
from frappe.model.doc import Document

class MClient(memcache.Client):
"""memcache client that will automatically prefix conf.db_name"""


+ 3
- 3
frappe/model/__init__.py 파일 보기

@@ -87,8 +87,8 @@ def delete_fields(args_dict, delete=0):
def rename_field(doctype, old_fieldname, new_fieldname):
"""This functions assumes that doctype is already synced"""
doctype_list = frappe.get_doctype(doctype)
new_field = doctype_list.get_field(new_fieldname)
meta = frappe.get_meta(doctype)
new_field = meta.get_field(new_fieldname)
if not new_field:
print "rename_field: " + (new_fieldname) + " not found in " + doctype
return
@@ -99,7 +99,7 @@ def rename_field(doctype, old_fieldname, new_fieldname):
where parentfield=%s""" % (new_field.options.split("\n")[0], "%s", "%s"),
(new_fieldname, old_fieldname))
elif new_field.fieldtype not in no_value_fields:
if doctype_list[0].issingle:
if meta.issingle:
frappe.db.sql("""update `tabSingles` set field=%s
where doctype=%s and field=%s""",
(new_fieldname, doctype, old_fieldname))


+ 5
- 1
frappe/model/base_document.py 파일 보기

@@ -69,7 +69,7 @@ class BaseDocument(object):
def extend(self, key, value):
if isinstance(value, list):
for v in value:
self.append(v)
self.append(key, v)
else:
raise ValueError
@@ -146,6 +146,10 @@ class BaseDocument(object):
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()
columns = d.keys()
frappe.db.sql("""update `tab{doctype}`


+ 0
- 521
frappe/model/bean.py 파일 보기

@@ -1,521 +0,0 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
"""
Transactions are defined as collection of classes, a Bean represents collection of Document
objects for a transaction with main and children.

Group actions like save, etc are performed on doclists
"""

import frappe
from frappe import _, msgprint
from frappe.utils import cint, cstr, flt
from frappe.model.doc import Document
import frappe.permissions

class DocstatusTransitionError(frappe.ValidationError): pass
class BeanPermissionError(frappe.ValidationError): pass
class TimestampMismatchError(frappe.ValidationError): pass

class Bean:
"""
Collection of Documents with one parent and multiple children
"""
def __init__(self, dt=None, dn=None):
self.obj = None
self.ignore_permissions = False
self.ignore_children_type = []
self.ignore_links = False
self.ignore_validate = False
self.ignore_fields = False
self.ignore_mandatory = False
self.ignore_restrictions = False
if isinstance(dt, basestring) and not dn:
dn = dt
if dt and dn:
if isinstance(dn, dict):
dn = frappe.db.get_value(dt, dn, "name")
if dn is None:
raise frappe.DoesNotExistError
self.load_from_db(dt, dn)
elif isinstance(dt, list):
self.set_doclist(dt)
elif isinstance(dt, dict):
self.set_doclist([dt])

def load_from_db(self, dt=None, dn=None):
"""
Load doclist from dt
"""

if not dt: dt = self.doc.doctype
if not dn: dn = self.doc.name

doc = Document(dt, dn)
# get all children types
tablefields = frappe.model.meta.get_table_fields(dt)

# load chilren
doclist = frappe.doclist([doc,])

self.set_doclist(doclist)
def __iter__(self):
return self.doclist.__iter__()

@property
def meta(self):
if not hasattr(self, "_meta"):
self._meta = frappe.get_doctype(self.doc.doctype)
return self._meta

def from_compressed(self, data, docname):
from frappe.model.utils import expand
self.set_doclist(expand(data))
def set_doclist(self, doclist):
for i, d in enumerate(doclist):
if isinstance(d, dict):
doclist[i] = Document(fielddata=d)
self.doclist = frappe.doclist(doclist)
self.doc = self.doclist[0]
if self.doc.meta.issingle:
self.doc.cast_floats_and_ints()
if self.obj:
self.obj.doclist = self.doclist
self.obj.doc = self.doc

def make_controller(self):
if not self.doc.doctype:
raise frappe.DataError("Bean doctype not specified")
if self.obj:
# update doclist before running any method
self.obj.doclist = self.doclist
return self.obj
self.obj = frappe.get_obj(doc=self.doc, doclist=self.doclist)
self.obj.bean = self
self.controller = self.obj
return self.obj

def get_controller(self):
return self.make_controller()

def to_dict(self):
return [d.fields for d in self.doclist]

def check_if_latest(self, method="save"):
from frappe.model.meta import is_single

conflict = False
if not cint(self.doc.fields.get('__islocal')):
if is_single(self.doc.doctype):
modified = frappe.db.get_value(self.doc.doctype, self.doc.name, "modified")
if isinstance(modified, list):
modified = modified[0]
if cstr(modified) and cstr(modified) != cstr(self.doc.modified):
conflict = True
else:
tmp = frappe.db.sql("""select modified, docstatus from `tab%s`
where name=%s for update"""
% (self.doc.doctype, '%s'), self.doc.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.doc.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.doc.modified)) \
+ _("Please refresh to get the latest document."), raise_exception=TimestampMismatchError)
def check_docstatus_transition(self, db_docstatus, method):
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 method != "runserverobj" and [db_docstatus, self.to_docstatus] != valid[method]:
frappe.msgprint(_("Cannot change from") + ": " + labels[db_docstatus] + " > " + \
labels[self.to_docstatus], raise_exception=DocstatusTransitionError)

def update_timestamps_and_docstatus(self):
from frappe.utils import now
ts = now()
user = frappe.__dict__.get('session', {}).get('user') or 'Administrator'

for d in self.doclist:
if self.doc.fields.get('__islocal'):
if not d.owner:
d.owner = user
if not d.creation:
d.creation = ts

d.modified_by = user
d.modified = ts
if d.docstatus != 2 and self.to_docstatus >= int(d.docstatus): # don't update deleted
d.docstatus = self.to_docstatus

def prepare_for_save(self, method):
self.check_if_latest(method)
self.update_timestamps_and_docstatus()
self.update_parent_info()
if self.doc.fields.get("__islocal"):
# set name before validate
self.run_method('before_set_name')
self.doc.set_new_name(self)
self.run_method('before_insert')
if method != "cancel":
self.extract_images_from_text_editor()
def update_parent_info(self):
idx_map = {}
is_local = cint(self.doc.fields.get("__islocal"))
if not frappe.flags.in_import:
parentfields = [d.fieldname for d in self.meta.get({"doctype": "DocField", "fieldtype": "Table"})]
for i, d in enumerate(self.doclist[1:]):
if d.parentfield:
if not frappe.flags.in_import:
if not d.parentfield in parentfields:
frappe.msgprint("Bad parentfield %s" % d.parentfield,
raise_exception=True)
d.parenttype = self.doc.doctype
d.parent = self.doc.name
if not d.idx:
d.idx = idx_map.setdefault(d.parentfield, 0) + 1
else:
d.idx = cint(d.idx)
if is_local:
# if parent is new, all children should be new
d.fields["__islocal"] = 1
d.name = None
idx_map[d.parentfield] = d.idx

def run_method(self, method, *args, **kwargs):
if not args:
args = []
self.make_controller()
def add_to_response(out, new_response):
if isinstance(new_response, dict):
out.update(new_response)
if hasattr(self.controller, method):
add_to_response(frappe.local.response,
frappe.call(getattr(self.controller, method), *args, **kwargs))
self.set_doclist(self.controller.doclist)

args = [self, method] + list(args)
for handler in frappe.get_hooks("bean_event:" + self.doc.doctype + ":" + method) \
+ frappe.get_hooks("bean_event:*:" + method):
add_to_response(frappe.local.response, frappe.call(frappe.get_attr(handler), *args, **kwargs))

return frappe.local.response
def get_attr(self, method):
self.make_controller()
return getattr(self.controller, method, None)

def insert(self, ignore_permissions=None):
if ignore_permissions:
self.ignore_permissions = True
self.doc.fields["__islocal"] = 1
self.set_defaults()
if frappe.flags.in_test:
if self.meta.get_field("naming_series"):
self.doc.naming_series = "_T-" + self.doc.doctype + "-"
return self.save()
def insert_or_update(self):
if self.doc.name and frappe.db.exists(self.doc.doctype, self.doc.name):
return self.save()
else:
return self.insert()
def set_defaults(self):
if frappe.flags.in_import:
return
new_docs = {}
new_doclist = []
for d in self.doclist:
if not d.doctype in new_docs:
new_docs[d.doctype] = frappe.new_doc(d.doctype)
newd = frappe.doc(new_docs[d.doctype].fields.copy())
newd.fields.update(d.fields)
new_doclist.append(newd)
self.set_doclist(new_doclist)

def has_read_perm(self):
return self.has_permission("read")
def has_permission(self, permtype):
if self.ignore_permissions:
return True
if self.doc.parent and self.doc.parenttype:
return frappe.has_permission(self.doc.parenttype, permtype,
frappe.doc(self.doc.parenttype, self.doc.parent))
else:
return frappe.has_permission(self.doc.doctype, permtype, self.doc)
def save(self, check_links=1, ignore_permissions=None):
if ignore_permissions!=None:
self.ignore_permissions = ignore_permissions
perm_to_check = "write"
if self.doc.fields.get("__islocal"):
perm_to_check = "create"
if not self.doc.owner:
self.doc.owner = frappe.session.user
self.to_docstatus = 0
self.prepare_for_save("save")
# check permissions after preparing for save, since name might be required
if self.has_permission(perm_to_check):
if not self.ignore_validate:
self.run_method('validate')
self.validate_doclist()
self.save_main()
self.save_children()
self.run_method('on_update')
if perm_to_check=="create":
self.run_method("after_insert")
else:
self.no_permission_to(_(perm_to_check.title()))
return self

def submit(self):
if self.has_permission("submit"):
self.to_docstatus = 1
self.prepare_for_save("submit")
self.run_method('validate')
self.validate_doclist()
self.save_main()
self.save_children()
self.run_method('on_update')
self.run_method('on_submit')
else:
self.no_permission_to(_("Submit"))
return self

def cancel(self):
if self.has_permission("cancel"):
self.to_docstatus = 2
self.prepare_for_save("cancel")
self.run_method('before_cancel')
self.save_main()
self.save_children()
self.run_method('on_cancel')
self.check_no_back_links_exist()
else:
self.no_permission_to(_("Cancel"))
return self

def update_after_submit(self):
if self.doc.docstatus != 1:
frappe.msgprint("Only to called after submit", raise_exception=1)
if self.has_permission("write"):
self.to_docstatus = 1
self.prepare_for_save("update_after_submit")
self.run_method('validate')
self.run_method('before_update_after_submit')
self.validate_doclist()
self.save_main()
self.save_children()
self.run_method('on_update_after_submit')
else:
self.no_permission_to(_("Update"))
return self

def save_main(self):
try:
self.doc.save(check_links = False, ignore_fields = self.ignore_fields)
except NameError, e:
frappe.msgprint('%s "%s" already exists' % (self.doc.doctype, self.doc.name))

# prompt if cancelled
if frappe.db.get_value(self.doc.doctype, self.doc.name, 'docstatus')==2:
frappe.msgprint('[%s "%s" has been cancelled]' % (self.doc.doctype, self.doc.name))
frappe.errprint(frappe.utils.get_traceback())
raise

def save_children(self):
child_map = {}
for d in self.doclist[1:]:
if d.fields.get("parent") or d.fields.get("parentfield"):
d.parent = self.doc.name # rename if reqd
d.parenttype = self.doc.doctype
d.save(check_links=False, ignore_fields = self.ignore_fields)
child_map.setdefault(d.doctype, []).append(d.name)
# delete all children in database that are not in the child_map
# get all children types
tablefields = frappe.model.meta.get_table_fields(self.doc.doctype)
for dt in tablefields:
if dt[0] not in self.ignore_children_type:
cnames = child_map.get(dt[0]) or []
if cnames:
frappe.db.sql("""delete from `tab%s` where parent=%s and parenttype=%s and
name not in (%s)""" % (dt[0], '%s', '%s', ','.join(['%s'] * len(cnames))),
tuple([self.doc.name, self.doc.doctype] + cnames))
else:
frappe.db.sql("""delete from `tab%s` where parent=%s and parenttype=%s""" \
% (dt[0], '%s', '%s'), (self.doc.name, self.doc.doctype))
def delete(self):
frappe.delete_doc(self.doc.doctype, self.doc.name)

def no_permission_to(self, ptype):
frappe.msgprint(("%s (%s): " % (self.doc.name, _(self.doc.doctype))) + \
_("No Permission to ") + ptype, raise_exception=BeanPermissionError)
def check_no_back_links_exist(self):
from frappe.model.delete_doc import check_if_doc_is_linked
check_if_doc_is_linked(self.doc.doctype, self.doc.name, method="Cancel")
def check_mandatory(self):
if self.ignore_mandatory:
return
missing = []
for doc in self.doclist:
for df in self.meta:
if df.doctype=="DocField" and df.reqd and df.parent==doc.doctype and df.fieldname!="naming_series":
msg = ""
if df.fieldtype == "Table":
if not self.get(df.fieldname):
msg = _("Error") + ": " + _("Data missing in table") + ": " + _(df.label)
elif doc.fields.get(df.fieldname) is None:
msg = _("Error") + ": "
if doc.parentfield:
msg += _("Row") + (" # %s: " % (doc.idx,))
msg += _("Value missing for") + ": " + _(df.label)
if msg:
missing.append([msg, df.fieldname])
if missing:
for msg, fieldname in missing:
msgprint(msg)

raise frappe.MandatoryError, ", ".join([fieldname for msg, fieldname in missing])
def extract_images_from_text_editor(self):
from frappe.utils.file_manager import extract_images_from_html
if self.doc.doctype != "DocType":
for df in self.meta.get({"doctype": "DocField", "parent": self.doc.doctype, "fieldtype":"Text Editor"}):
extract_images_from_html(self.doc, df.fieldname)
def validate_doclist(self):
self.check_mandatory()
self.validate_restrictions()
self.check_links()
def check_links(self):
if self.ignore_links:
return
ref, err_list = {}, []
for d in self.doclist:
if not ref.get(d.doctype):
ref[d.doctype] = d.make_link_list()

err_list += d.validate_links(ref[d.doctype])
if err_list:
frappe.msgprint("""[Link Validation] Could not find the following values: %s.
Please correct and resave. Document Not Saved.""" % ', '.join(err_list), raise_exception=1)
def validate_restrictions(self):
if self.ignore_restrictions:
return
has_restricted_data = False
for d in self.doclist:
if not frappe.permissions.has_unrestricted_access(d):
has_restricted_data = True
if has_restricted_data:
raise BeanPermissionError

def clone(source_wrapper):
""" make a clone of a document"""
if isinstance(source_wrapper, list):
source_wrapper = Bean(source_wrapper)
new_wrapper = Bean(source_wrapper.doclist.copy())
if new_wrapper.doc.fields.get("amended_from"):
new_wrapper.doc.fields["amended_from"] = None

if new_wrapper.doc.fields.get("amendment_date"):
new_wrapper.doc.fields["amendment_date"] = None
for d in new_wrapper.doclist:
d.fields.update({
"name": None,
"__islocal": 1,
"docstatus": 0,
})
return new_wrapper

# for bc
def getlist(doclist, parentfield):
import frappe.model.utils
return frappe.model.utils.getlist(doclist, parentfield)

def copy_doclist(doclist, no_copy = []):
"""
Make a copy of the doclist
"""
import frappe.model.utils
return frappe.model.utils.copy_doclist(doclist, no_copy)


+ 1
- 1
frappe/model/create_new.py 파일 보기

@@ -18,7 +18,7 @@ def get_new_doc(doctype, parent_doc = None, parentfield = None):
"docstatus": 0
})
meta = frappe.get_doctype(doctype)
meta = frappe.get_meta(doctype)
restrictions = frappe.defaults.get_restrictions()


+ 0
- 735
frappe/model/doc.py 파일 보기

@@ -1,735 +0,0 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
"""
Contains the Document class representing an object / record
"""

import frappe
from frappe import _
import frappe.model.meta
from frappe.utils import *

class Document:
"""
The frappe(meta-data)framework equivalent of a Database Record.
Stores,Retrieves,Updates the record in the corresponding table.
Runs the triggers required.

The `Document` class represents the basic Object-Relational Mapper (ORM). The object type is defined by
`DocType` and the object ID is represented by `name`::
Please note the anamoly in the Web Notes Framework that `ID` is always called as `name`

If both `doctype` and `name` are specified in the constructor, then the object is loaded from the database.
If only `doctype` is given, then the object is not loaded
If `fielddata` is specfied, then the object is created from the given dictionary.
**Note 1:**
The getter and setter of the object are overloaded to map to the fields of the object that
are loaded when it is instantiated.
For example: doc.name will be the `name` field and doc.owner will be the `owner` field

**Note 2 - Standard Fields:**
* `name`: ID / primary key
* `owner`: creator of the record
* `creation`: datetime of creation
* `modified`: datetime of last modification
* `modified_by` : last updating user
* `docstatus` : Status 0 - Saved, 1 - Submitted, 2- Cancelled
* `parent` : if child (table) record, this represents the parent record
* `parenttype` : type of parent record (if any)
* `parentfield` : table fieldname of parent record (if any)
* `idx` : Index (sequence) of the child record
"""
def __init__(self, doctype = None, name = None, fielddata = None):
self._roles = []
self._perms = []
self._user_defaults = {}
self._new_name_set = False
self._meta = None
if isinstance(doctype, dict):
fielddata = doctype
doctype = None
if doctype and isinstance(name, dict):
name = frappe.db.get_value(doctype, name, "name") or None
if fielddata:
self.fields = fielddata
else:
self.fields = {}
if not self.fields.has_key('name'):
self.fields['name']='' # required on save
if not self.fields.has_key('doctype'):
self.fields['doctype']='' # required on save
if not self.fields.has_key('owner'):
self.fields['owner']='' # required on save

if doctype:
self.fields['doctype'] = doctype
if name:
self.fields['name'] = name
self.__initialized = 1

if (doctype and name):
self._loadfromdb(doctype, name)
else:
if not fielddata:
self.fields['__islocal'] = 1
if not self.fields.get("docstatus"):
self.fields["docstatus"] = 0

def __nonzero__(self):
return True

def __str__(self):
return str(self.fields)
def __repr__(self):
return repr(self.fields)
def __unicode__(self):
return unicode(self.fields)
def __eq__(self, other):
if isinstance(other, Document):
return self.fields == other.fields
else:
return False

def __getstate__(self):
return self.fields
def __setstate__(self, d):
self.fields = d

def encode(self, encoding='utf-8'):
"""convert all unicode values to utf-8"""
from frappe.utils import encode_dict
encode_dict(self.fields)

def _loadfromdb(self, doctype = None, name = None):
if name: self.name = name
if doctype: self.doctype = doctype
is_single = False
try:
is_single = frappe.model.meta.is_single(self.doctype)
except Exception, e:
pass
if is_single:
self._loadsingle()
else:
try:
dataset = frappe.db.sql('select * from `tab%s` where name=%s' %
(self.doctype, "%s"), self.name)
except frappe.SQLError, e:
if e.args[0]==1146:
dataset = None
else:
raise

if not dataset:
raise frappe.DoesNotExistError, '[WNF] %s %s does not exist' % (self.doctype, self.name)
self._load_values(dataset[0], frappe.db.get_description())

def is_new(self):
return self.fields.get("__islocal")

def _load_values(self, data, description):
if '__islocal' in self.fields:
del self.fields['__islocal']
for i in range(len(description)):
v = data[i]
self.fields[description[i][0]] = frappe.db.convert_to_simple_type(v)

def _merge_values(self, data, description):
for i in range(len(description)):
v = data[i]
if v: # only if value, over-write
self.fields[description[i][0]] = frappe.db.convert_to_simple_type(v)
def _loadsingle(self):
self.name = self.doctype
self.fields.update(getsingle(self.doctype))
self.cast_floats_and_ints()
def cast_floats_and_ints(self):
for df in frappe.get_doctype(self.doctype).get_docfields():
if df.fieldtype in ("Int", "Check"):
self.fields[df.fieldname] = cint(self.fields.get(df.fieldname))
elif df.fieldtype in ("Float", "Currency"):
self.fields[df.fieldname] = flt(self.fields.get(df.fieldname))
if self.docstatus is not None:
self.docstatus = cint(self.docstatus)
def __setattr__(self, name, value):
# normal attribute
if not self.__dict__.has_key('_Document__initialized'):
self.__dict__[name] = value
elif self.__dict__.has_key(name):
self.__dict__[name] = value
else:
# field attribute
f = self.__dict__['fields']
f[name] = value

def __getattr__(self, name):
if self.__dict__.has_key(name):
return self.__dict__[name]
elif self.fields.has_key(name):
return self.fields[name]
else:
if name.startswith("__"):
raise AttributeError()
return None
def get(self, name, value=None):
return self.fields.get(name, value)

def update(self, d):
self.fields.update(d)
return self

def update_if_missing(self, d):
fields = self.get_valid_fields()
for key, value in d.iteritems():
if (key in fields) and (not self.fields.get(key)):
self.fields[key] = value

def insert(self):
self.fields['__islocal'] = 1
self.save()
return self
def save(self, new=0, check_links=1, ignore_fields=0, make_autoname=1,
keep_timestamps=False):
if new:
self.fields["__islocal"] = 1

# add missing parentinfo (if reqd)
if self.parent and not (self.parenttype and self.parentfield):
self.update_parentinfo()

if self.parent and not self.idx:
self.set_idx()

# if required, make new
if not self.meta.issingle:
if self.is_new():
r = self._insert(make_autoname=make_autoname, keep_timestamps = keep_timestamps)
if r:
return r
else:
if not frappe.db.exists(self.doctype, self.name):
frappe.msgprint(frappe._("Cannot update a non-exiting record, try inserting.") + ": " + self.doctype + " / " + self.name,
raise_exception=1)
# save the values
self._update_values(self.meta.issingle,
check_links and self.make_link_list() or {}, ignore_fields=ignore_fields,
keep_timestamps=keep_timestamps)
self._clear_temp_fields()

def _get_amended_name(self):
am_id = 1
am_prefix = self.amended_from
if frappe.db.get_value(self.doctype, self.amended_from, "amended_from"):
am_id = cint(self.amended_from.split('-')[-1]) + 1
am_prefix = '-'.join(self.amended_from.split('-')[:-1]) # except the last hyphen
self.name = am_prefix + '-' + str(am_id)

def set_new_name(self, bean=None):
if self._new_name_set:
# already set by bean
return

self._new_name_set = True

autoname = self.meta.autoname
self.localname = self.name


# amendments
if self.amended_from:
return self._get_amended_name()

# by method
else:
# get my object
if not bean:
bean = frappe.bean([self])
bean.run_method("autoname")
if self.name and self.localname != self.name:
return

# based on a field
if autoname and autoname.startswith('field:'):
n = self.fields[autoname[6:]]
if not n:
raise Exception, 'Name is required'
self.name = n.strip()
elif autoname and autoname.startswith("naming_series:"):
self.set_naming_series()
if not self.naming_series:
frappe.msgprint(frappe._("Naming Series mandatory"), raise_exception=True)
self.name = make_autoname(self.naming_series+'.#####')
# call the method!
elif autoname and autoname!='Prompt':
self.name = make_autoname(autoname, self.doctype)
# given
elif self.fields.get('__newname',''):
self.name = self.fields['__newname']

# default name for table
elif self.meta.istable:
self.name = make_autoname('#########', self.doctype)
# unable to determine a name, use global series
if not self.name:
self.name = make_autoname('#########', self.doctype)
def set_naming_series(self):
if not self.naming_series:
# pick default naming series
self.naming_series = get_default_naming_series(self.doctype)
def append_number_if_name_exists(self):
if frappe.db.exists(self.doctype, self.name):
last = frappe.db.sql("""select name from `tab{}`
where name regexp '{}-[[:digit:]]+'
order by name desc limit 1""".format(self.doctype, self.name))
if last:
count = str(cint(last[0][0].rsplit("-", 1)[1]) + 1)
else:
count = "1"
self.name = "{0}-{1}".format(self.name, count)
def set(self, key, value):
self.modified = now()
self.modified_by = frappe.session["user"]
frappe.db.set_value(self.doctype, self.name, key, value, self.modified, self.modified_by)
self.fields[key] = value
def _insert(self, make_autoname=True, keep_timestamps=False):
# set name
if make_autoname:
self.set_new_name()
# validate name
self.name = validate_name(self.doctype, self.name, self.meta.name_case)
# insert!
if not keep_timestamps:
if not self.owner:
self.owner = frappe.session['user']
self.modified_by = frappe.session['user']
if not self.creation:
self.creation = self.modified = now()
else:
self.modified = now()
frappe.db.sql("insert into `tab%(doctype)s`" % self.fields \
+ """ (name, owner, creation, modified, modified_by)
values (%(name)s, %(owner)s, %(creation)s, %(modified)s,
%(modified_by)s)""", self.fields)

def _update_single(self, link_list):
self.modified = now()
update_str, values = [], []
frappe.db.sql("delete from tabSingles where doctype=%s", self.doctype)
for f in self.fields.keys():
if not (f in ('modified', 'doctype', 'name', 'perm', 'localname', 'creation'))\
and (not f.startswith('__')): # fields not saved

# validate links
if link_list and link_list.get(f):
self.fields[f] = self._validate_link(link_list, f)

if self.fields[f]==None:
update_str.append("(%s,%s,NULL)")
values.append(self.doctype)
values.append(f)
else:
update_str.append("(%s,%s,%s)")
values.append(self.doctype)
values.append(f)
values.append(self.fields[f])
frappe.db.sql("insert into tabSingles(doctype, field, value) values %s" % (', '.join(update_str)), values)

def validate_links(self, link_list):
err_list = []
for f in self.fields.keys():
# validate links
old_val = self.fields[f]
if link_list and link_list.get(f):
self.fields[f] = self._validate_link(link_list, f)

if old_val and not self.fields[f]:
err_list.append("{}: {}".format(link_list[f][1], old_val))
return err_list

def make_link_list(self):
res = frappe.model.meta.get_link_fields(self.doctype)

link_list = {}
for i in res: link_list[i[0]] = (i[1], i[2]) # options, label
return link_list
def _validate_link(self, link_list, f):
dt = link_list[f][0]
dn = self.fields.get(f)
if not dt:
frappe.throw("Options not set for link field: " + f)
if not dt: return dn
if not dn: return None
if dt=="[Select]": return dn
if dt.lower().startswith('link:'):
dt = dt[5:]
if '\n' in dt:
dt = dt.split('\n')[0]
tmp = frappe.db.sql("""SELECT name FROM `tab%s`
WHERE name = %s""" % (dt, '%s'), (dn,))
return tmp and tmp[0][0] or ''# match case
def _update_values(self, issingle, link_list, ignore_fields=0, keep_timestamps=False):
self.validate_constants()
if issingle:
self._update_single(link_list)
else:
update_str, values = [], []
# set modified timestamp
if self.modified and not keep_timestamps:
self.modified = now()
self.modified_by = frappe.session['user']
fields_list = ignore_fields and self.get_valid_fields() or self.fields.keys()
for f in fields_list:
if (not (f in ('doctype', 'name', 'perm', 'localname',
'creation','_user_tags', "file_list", "_comments"))) and (not f.startswith('__')):
# fields not saved
# validate links
if link_list and link_list.get(f):
self.fields[f] = self._validate_link(link_list, f)

if self.fields.get(f) is None or self.fields.get(f)=='':
update_str.append("`%s`=NULL" % f)
else:
values.append(self.fields.get(f))
update_str.append("`%s`=%s" % (f, '%s'))
if values:
values.append(self.name)
r = frappe.db.sql("update `tab%s` set %s where name=%s" % \
(self.doctype, ', '.join(update_str), "%s"), values)
def get_valid_fields(self):
import frappe.model.doctype
if getattr(frappe.local, "valid_fields_map", None) is None:
frappe.local.valid_fields_map = {}
valid_fields_map = frappe.local.valid_fields_map
if not valid_fields_map.get(self.doctype):
if cint(self.meta.issingle):
doctypelist = frappe.model.doctype.get(self.doctype)
valid_fields_map[self.doctype] = doctypelist.get_fieldnames({
"fieldtype": ["not in", frappe.model.no_value_fields]})
else:
valid_fields_map[self.doctype] = \
frappe.db.get_table_columns(self.doctype)
return valid_fields_map.get(self.doctype)

def validate_constants(self):
if frappe.flags.in_import:
return
meta = frappe.get_doctype(self.doctype)
constants = [d.fieldname for d in meta.get({"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.fields.get(fieldname) != values.get(fieldname):
frappe.throw("{0}: {1}".format(_("Value cannot be changed for"),
_(meta.get_field(fieldname).label)), frappe.CannotChangeConstantError)
@property
def meta(self):
if not self._meta:
self._meta = frappe.db.get_value("DocType", self.doctype, "*", as_dict=True) \
or frappe._dict()
return self._meta
def update_parentinfo(self):
"""update parent type and parent field, if not explicitly specified"""

tmp = frappe.db.sql("""select parent, fieldname from tabDocField
where fieldtype='Table' and options=%s""", (self.doctype,))
if len(tmp)==0:
raise Exception, 'Incomplete parent info in child table (%s, %s)' \
% (self.doctype, self.fields.get('name', '[new]'))
elif len(tmp)>1:
raise Exception, 'Ambiguous parent info (%s, %s)' \
% (self.doctype, self.fields.get('name', '[new]'))

else:
self.parenttype = tmp[0][0]
self.parentfield = tmp[0][1]

def set_idx(self):
"""set idx"""
self.idx = (frappe.db.sql("""select max(idx) from `tab%s`
where parent=%s and parentfield=%s""" % (self.doctype, '%s', '%s'),
(self.parent, self.parentfield))[0][0] or 0) + 1
def _clear_temp_fields(self):
# clear temp stuff
keys = self.fields.keys()
for f in keys:
if f.startswith('__'):
del self.fields[f]

def clear_table(self, doclist, tablefield, save=0):
"""
Clears the child records from the given `doclist` for a particular `tablefield`
"""
from frappe.model.utils import getlist
table_list = getlist(doclist, tablefield)
delete_list = [d.name for d in table_list]
if delete_list:
#filter doclist
doclist = filter(lambda d: d.name not in delete_list, doclist)
# delete from db
frappe.db.sql("""\
delete from `tab%s`
where parent=%s and parenttype=%s"""
% (table_list[0].doctype, '%s', '%s'),
(self.name, self.doctype))

self.fields['__unsaved'] = 1
return frappe.doclist(doclist)
def get_values(self):
"""get non-null fields dict withouth standard fields"""
from frappe.model import default_fields
ret = {}
for key in self.fields:
if key not in default_fields and self.fields[key]:
ret[key] = self.fields[key]
return ret
def get_db_value(self, key):
return frappe.db.get_value(self.doctype, self.name, key)
def make_autoname(key, doctype=''):
"""
Creates an autoname from the given key:
**Autoname rules:**
* The key is separated by '.'
* '####' represents a series. The string before this part becomes the prefix:
Example: ABC.#### creates a series ABC0001, ABC0002 etc
* 'MM' represents the current month
* 'YY' and 'YYYY' represent the current year

*Example:*
* DE/./.YY./.MM./.##### will create a series like
DE/09/01/0001 where 09 is the year, 01 is the month and 0001 is the series
"""
if key=="hash":
return frappe.generate_hash(doctype)
if not "#" in key:
key = key + ".#####"
n = ''
l = key.split('.')
series_set = False
today = now_datetime()
for e in l:
en = ''
if e.startswith('#'):
if not series_set:
digits = len(e)
en = getseries(n, digits, doctype)
series_set = True
elif e=='YY':
en = today.strftime('%y')
elif e=='MM':
en = today.strftime('%m')
elif e=='DD':
en = today.strftime("%d")
elif e=='YYYY':
en = today.strftime('%Y')
else: en = e
n+=en
return n

def getseries(key, digits, doctype=''):
# series created ?
current = frappe.db.sql("select `current` from `tabSeries` where name=%s for update", (key,))
if current and current[0][0] is not None:
current = current[0][0]
# yes, update it
frappe.db.sql("update tabSeries set current = current+1 where name=%s", (key,))
current = cint(current) + 1
else:
# no, create it
frappe.db.sql("insert into tabSeries (name, current) values (%s, 1)", (key,))
current = 1
return ('%0'+str(digits)+'d') % current

def getchildren(name, childtype, field='', parenttype='', from_doctype=0):
import frappe
from frappe.model.doclist import DocList
condition = ""
values = []
if field:
condition += ' and parentfield=%s '
values.append(field)
if parenttype:
condition += ' and parenttype=%s '
values.append(parenttype)

dataset = frappe.db.sql("""select * from `tab%s` where parent=%s %s order by idx""" \
% (childtype, "%s", condition), tuple([name]+values))
desc = frappe.db.get_description()

l = DocList()
for i in dataset:
d = Document()
d.doctype = childtype
d._load_values(i, desc)
l.append(d)
return l

def check_page_perm(doc):
if doc.name=='Login Page':
return
if doc.publish:
return

if not frappe.db.sql("select name from `tabPage Role` where parent=%s and role='Guest'", (doc.name,)):
frappe.response['403'] = 1
raise frappe.PermissionError, '[WNF] No read permission for %s %s' % ('Page', doc.name)

def get(dt, dn='', with_children = 1, from_controller = 0):
"""
Returns a doclist containing the main record and all child records
"""
import frappe
import frappe.model
from frappe.model.doclist import DocList
dn = dn or dt

# load the main doc
doc = Document(dt, dn)

if dt=='Page' and frappe.session['user'] == 'Guest':
check_page_perm(doc)

if not with_children:
# done
return DocList([doc,])
# get all children types
tablefields = frappe.model.meta.get_table_fields(dt)

# load chilren
doclist = DocList([doc,])
for t in tablefields:
doclist += getchildren(doc.name, t[0], t[1], dt)

return doclist

def getsingle(doctype):
"""get single doc as dict"""
dataset = frappe.db.sql("select field, value from tabSingles where doctype=%s", (doctype,))
return dict(dataset)
def copy_common_fields(from_doc, to_doc):
from frappe.model import default_fields
doctype_list = frappe.get_doctype(to_doc.doctype)
for fieldname, value in from_doc.fields.items():
if fieldname in default_fields:
continue
if doctype_list.get_field(fieldname) and to_doc.fields[fieldname] != value:
to_doc.fields[fieldname] = value
def validate_name(doctype, name, case=None, merge=False):
if not merge:
if frappe.db.sql('select name from `tab%s` where name=%s' % (doctype,'%s'), (name,)):
raise NameError, 'Name %s already exists' % name
# no name
if not name: return 'No Name Specified for %s' % doctype
# new..
if name.startswith('New '+doctype):
raise NameError, 'There were some errors setting the name, please contact the administrator'
if case=='Title Case': name = name.title()
if case=='UPPER CASE': name = name.upper()
name = name.strip() # no leading and trailing blanks

return name
def get_default_naming_series(doctype):
"""get default value for `naming_series` property"""
from frappe.model.doctype import get_property
naming_series = get_property(doctype, "options", "naming_series")
if naming_series:
naming_series = naming_series.split("\n")
return naming_series[0] or naming_series[1]
else:
return None

+ 0
- 143
frappe/model/doclist.py 파일 보기

@@ -1,143 +0,0 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

import frappe
import frappe.model
from frappe.model.doc import Document
from frappe import _

class DocList(list):
"""DocList object as a wrapper around a list"""
def get(self, filters, limit=0):
"""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 doc in self:
d = isinstance(getattr(doc, "fields", None), dict) and doc.fields or doc
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(doc)
if limit and (len(out)-1)==limit:
break
return DocList(out)
def get_distinct_values(self, fieldname):
return filter(None, list(set(map(lambda d: d.fields.get(fieldname), self))))

def remove_items(self, filters):
for d in self.get(filters):
self.remove(d)

def getone(self, filters):
return self.get(filters, limit=1)[0]

def copy(self):
out = []
for d in self:
if isinstance(d, dict):
fielddata = d
else:
fielddata = d.fields
fielddata.update({"name": None})
out.append(Document(fielddata=fielddata))
return DocList(out)
def get_item_value(self, d, name):
if isinstance(d, dict):
return d.get(name)
else:
return d.fields.get(name)
def filter_valid_fields(self):
import frappe.model
fieldnames = {}
for d in self:
remove = []
for f in d:
if f not in fieldnames.setdefault(d.doctype,
frappe.model.get_fieldnames(d.doctype)):
remove.append(f)
for f in remove:
del d[f]
def append(self, doc):
if not isinstance(doc, Document):
doc = Document(fielddata=doc)
self._prepare_doc(doc)

super(DocList, self).append(doc)
def extend(self, doclist):
doclist = objectify(doclist)
for doc in doclist:
self._prepare_doc(doc)
super(DocList, self).extend(doclist)
return self
def _prepare_doc(self, doc):
if not doc.name:
doc.fields["__islocal"] = 1
doc.docstatus = 0
if doc.parentfield:
if not doc.parenttype:
doc.parenttype = self[0].doctype
if not doc.parent:
doc.parent = self[0].name
if not doc.idx:
siblings = [int(self.get_item_value(d, "idx") or 0) for d in self.get({"parentfield": doc.parentfield})]
doc.idx = (max(siblings) + 1) if siblings else 1
def update(self, doclist):
for i, d in enumerate(self):
if d.get("parent") and d.get("name") not in [t.get("name") for t in doclist]:
del self[i]
for d in doclist:
if not d["name"]:
d["__islocal"] = 1
self.append(d)
else:
# child
found_in_existing = False

for ref in self:
if d["name"] and ref.name and ref.name == d["name"]:
ref.fields.update(d)
found_in_existing = True
break
if not found_in_existing:
d["__islocal"] = 1
d["name"] = None
self.append(d)
return self
def objectify(doclist):
from frappe.model.doc import Document
return map(lambda d: isinstance(d, Document) and d or Document(d), doclist)

+ 0
- 425
frappe/model/doctype.py 파일 보기

@@ -1,425 +0,0 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

"""
Get metadata (main doctype with fields and permissions with all table doctypes)

- if exists in cache, get it from cache
- add custom fields
- override properties from PropertySetter
- sort based on prev_field
- optionally, post process (add js, css, select fields), or without

"""
from __future__ import unicode_literals

# imports
import json, os
import frappe
import frappe.model
import frappe.model.doc
import frappe.model.doclist
from frappe.utils import cint, cstr

doctype_cache = frappe.local('doctype_doctype_cache')
docfield_types = frappe.local('doctype_docfield_types')

# doctype_cache = {}
# docfield_types = None

def get(doctype, processed=False, cached=True):
"""return doclist"""
if cached:
doclist = from_cache(doctype, processed)
if doclist:
if processed:
update_language(doclist)
return DocTypeDocList(doclist)
load_docfield_types()
# main doctype doclist
doclist = get_doctype_doclist(doctype)

# add doctypes of table fields
table_types = [d.options for d in doclist \
if d.doctype=='DocField' and d.fieldtype=='Table']
for table_doctype in table_types:
doclist += get_doctype_doclist(table_doctype)
if processed:
add_code(doctype, doclist)
expand_selects(doclist)
add_print_formats(doclist)
add_search_fields(doclist)
add_workflows(doclist)
add_linked_with(doclist)
to_cache(doctype, processed, doclist)

if processed:
update_language(doclist)

return DocTypeDocList(doclist)

def load_docfield_types():
frappe.local.doctype_docfield_types = dict(frappe.db.sql("""select fieldname, fieldtype from tabDocField
where parent='DocField'"""))

def add_workflows(doclist):
from frappe.model.workflow import get_workflow_name
doctype = doclist[0].name
# get active workflow
workflow_name = get_workflow_name(doctype)

if workflow_name and frappe.db.exists("Workflow", workflow_name):
doclist += frappe.get_doclist("Workflow", workflow_name)
# add workflow states (for icons and style)
for state in map(lambda d: d.state, doclist.get({"doctype":"Workflow Document State"})):
doclist += frappe.get_doclist("Workflow State", state)
def get_doctype_doclist(doctype):
"""get doclist of single doctype"""
doclist = frappe.get_doclist('DocType', doctype)
add_custom_fields(doctype, doclist)
apply_property_setters(doctype, doclist)
sort_fields(doclist)
return doclist

def sort_fields(doclist):
"""sort on basis of previous_field"""

from frappe.model.doclist import DocList
newlist = DocList([])
pending = doclist.get({"doctype":"DocField"})

if doclist[0].get("_idx"):
for fieldname in json.loads(doclist[0].get("_idx")):
d = doclist.get({"fieldname": fieldname})
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

doclist.get({"doctype":["!=", "DocField"]}).extend(newlist)
def apply_property_setters(doctype, doclist):
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'])
doclist[0].fields[ps['property']] = ps['value']
else:
docfield = filter(lambda d: d.doctype=="DocField" and d.fieldname==ps['field_name'],
doclist)
if not docfield: continue
if docfield_types.get(ps['property'], None) in ('Int', 'Check'):
ps['value'] = cint(ps['value'])
docfield[0].fields[ps['property']] = ps['value']

def add_custom_fields(doctype, doclist):
try:
res = 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 doclist
else:
raise

for r in res:
custom_field = frappe.model.doc.Document(fielddata=r)
# convert to DocField
custom_field.fields.update({
'doctype': 'DocField',
'parent': doctype,
'parentfield': 'fields',
'parenttype': 'DocType',
'__custom_field': 1
})
doclist.append(custom_field)

return doclist

def add_linked_with(doclist):
"""add list of doctypes this doctype is 'linked' with"""
doctype = doclist[0].name
links = frappe.db.sql("""select parent, fieldname from tabDocField
where (fieldtype="Link" and options=%s)
or (fieldtype="Select" and options=%s)""", (doctype, "link:"+ doctype))
links += frappe.db.sql("""select dt as parent, fieldname from `tabCustom Field`
where (fieldtype="Link" and options=%s)
or (fieldtype="Select" and options=%s)""", (doctype, "link:"+ doctype))

links = dict(links)

if not links:
return {}

ret = {}

for dt in links:
ret[dt] = { "fieldname": links[dt] }

for grand_parent, options in frappe.db.sql("""select parent, options from tabDocField
where fieldtype="Table"
and options in (select name from tabDocType
where istable=1 and name in (%s))""" % ", ".join(["%s"] * len(links)) ,tuple(links)):

ret[grand_parent] = {"child_doctype": options, "fieldname": links[options] }
if options in ret:
del ret[options]
doclist[0].fields["__linked_with"] = ret

def from_cache(doctype, processed):
""" load doclist from cache.
sets flag __from_cache in first doc of doclist if loaded from cache"""

# from memory
if doctype_cache and not processed and doctype in doctype_cache:
doclist = doctype_cache[doctype]
doclist[0].fields["__from_cache"] = 1
return doclist

doclist = frappe.cache().get_value(cache_name(doctype, processed))
if doclist:
from frappe.model.doclist import DocList
doclist = DocList([frappe.model.doc.Document(fielddata=d)
for d in doclist])
doclist[0].fields["__from_cache"] = 1
return doclist

def to_cache(doctype, processed, doclist):

if not doctype_cache:
frappe.local.doctype_doctype_cache = {}

frappe.cache().set_value(cache_name(doctype, processed),
[d.fields for d in doclist])

if not processed:
doctype_cache[doctype] = doclist

def cache_name(doctype, processed):
"""returns cache key"""
suffix = ""
if processed:
suffix = ":Raw"
return "doctype:" + doctype + suffix

def clear_cache(doctype=None):
def clear_single(dt):
frappe.cache().delete_value(cache_name(dt, False))
frappe.cache().delete_value(cache_name(dt, True))
frappe.cache().delete_value("meta:" + dt)
frappe.cache().delete_value("form_meta:" + dt)

if doctype_cache and (dt in doctype_cache):
del doctype_cache[dt]

if doctype:
clear_single(doctype)
# clear all parent doctypes
for dt in frappe.db.sql("""select parent from tabDocField
where fieldtype="Table" and options=%s""", (doctype,)):
clear_single(dt[0])
# clear all notifications
from frappe.core.doctype.notification_count.notification_count import delete_notification_count_for
delete_notification_count_for(doctype)
else:
# clear all
for dt in frappe.db.sql("""select name from tabDocType"""):
clear_single(dt[0])
frappe.cache().delete_value("is_table")

def add_code(doctype, doclist):
import os
from frappe.modules import scrub, get_module_path
doc = doclist[0]
path = os.path.join(get_module_path(doc.module), 'doctype', scrub(doc.name))
def _get_path(fname):
return os.path.join(path, scrub(fname))
_add_code(doc, _get_path(doc.name + '.js'), '__js')
_add_code(doc, _get_path(doc.name + '.css'), "__css")
_add_code(doc, _get_path(doc.name + '_list.js'), '__list_js')
_add_code(doc, _get_path(doc.name + '_calendar.js'), '__calendar_js')
_add_code(doc, _get_path(doc.name + '_map.js'), '__map_js')
add_custom_script(doc)
add_code_via_hook(doc, "doctype_js", "__js")
def _add_code(doc, path, fieldname):
js = frappe.read_file(path)
if js:
doc.fields[fieldname] = (doc.fields.get(fieldname) or "") + "\n\n" + render_jinja(js)
def add_code_via_hook(doc, hook, fieldname):
hook = "{}:{}".format(hook, doc.name)
for app_name in frappe.get_installed_apps():
for file in frappe.get_hooks(hook, app_name=app_name):
path = frappe.get_app_path(app_name, *file.strip("/").split("/"))
_add_code(doc, path, fieldname)
def add_custom_script(doc):
"""embed all require files"""
# custom script
custom = frappe.db.get_value("Custom Script", {"dt": doc.name,
"script_type": "Client"}, "script") or ""
doc.fields["__js"] = (doc.fields.get('__js') or '') + "\n\n" + custom
def render_jinja(content):
if "{% include" in content:
content = frappe.get_jenv().from_string(content).render()
return content
def expand_selects(doclist):
for d in filter(lambda d: d.fieldtype=='Select' \
and (d.options or '').startswith('link:'), doclist):
doctype = d.options.split("\n")[0][5:]
d.link_doctype = doctype
d.options = '\n'.join([''] + [o.name for o in frappe.db.sql("""select
name from `tab%s` where docstatus<2 order by name asc""" % doctype, as_dict=1)])

def add_print_formats(doclist):
print_formats = frappe.db.sql("""select * FROM `tabPrint Format`
WHERE doc_type=%s AND docstatus<2""", (doclist[0].name,), as_dict=1)
for pf in print_formats:
doclist.append(frappe.model.doc.Document('Print Format', fielddata=pf))

def get_property(dt, prop, fieldname=None):
"""get a doctype property"""
doctypelist = get(dt)
if fieldname:
field = doctypelist.get_field(fieldname)
return field and field.fields.get(prop) or None
else:
return doctypelist[0].fields.get(prop)
def get_link_fields(doctype):
"""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",
"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):
doclist.extend(frappe.get_doclist('DocType Validator', validator.name))
def add_search_fields(doclist):
"""add search fields found in the doctypes indicated by link fields' options"""
for lf in doclist.get({"fieldtype": "Link", "options":["!=", "[Select]"]}):
if lf.options:
search_fields = get(lf.options)[0].search_fields
if search_fields:
lf.search_fields = map(lambda sf: sf.strip(), search_fields.split(","))

def update_language(doclist):
"""update language"""
if frappe.local.lang != 'en':
doclist[0].fields["__messages"] = frappe.get_lang_dict("doctype", doclist[0].name)

class DocTypeDocList(frappe.model.doclist.DocList):
def get_field(self, fieldname, parent=None, parentfield=None):
filters = {"doctype":"DocField"}
if isinstance(fieldname, dict):
filters.update(fieldname)
else:
filters["fieldname"] = fieldname
# if parentfield, get the name of the parent table
if parentfield:
parent = self.get_options(parentfield)

if parent:
filters["parent"] = parent
else:
filters["parent"] = self[0].name
fields = self.get(filters)
if fields:
return fields[0]
def has_field(self, fieldname):
return fieldname in self.get_fieldnames()
def get_fieldnames(self, filters=None):
return map(lambda df: df.fieldname, self.get_docfields(filters))
def get_docfields(self, filters=None):
if not filters: filters = {}
filters.update({"doctype": "DocField", "parent": self[0].name})
return self.get(filters)
def get_options(self, fieldname, parent=None, parentfield=None):
return self.get_field(fieldname, parent, parentfield).options
def get_label(self, fieldname, parent=None, parentfield=None):
return self.get_field(fieldname, parent, parentfield).label
def get_table_fields(self):
return self.get({"doctype": "DocField", "fieldtype": "Table"})
def get_parent_doclist(self):
return frappe.doclist([self[0]] + self.get({"parent": self[0].name}))
def get_restricted_fields(self, restricted_types):
restricted_fields = self.get({
"doctype":"DocField",
"fieldtype":"Link",
"parent": self[0].name,
"ignore_restrictions":("!=", 1),
"options":("in", restricted_types)
})
if self[0].name in restricted_types:
restricted_fields.append(frappe._dict({"label":"Name", "fieldname":"name", "options": self[0].name}))
return restricted_fields
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)]
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}))

+ 11
- 1
frappe/model/document.py 파일 보기

@@ -12,7 +12,9 @@ from frappe.model.base_document import BaseDocument
# methods

def get_doc(arg1, arg2=None):
if isinstance(arg1, basestring):
if isinstance(arg1, BaseDocument):
return arg1
elif isinstance(arg1, basestring):
doctype = arg1
else:
doctype = arg1.get("doctype")
@@ -315,6 +317,14 @@ class Document(BaseDocument):
if hasattr(self, method):
fn = lambda self, *args, **kwargs: getattr(self, method)(*args, **kwargs)
return Document.hook(fn)(self, *args, **kwargs)
def submit(self):
self.docstatus = 1
self.save()

def cancel(self):
self.docstatus = 2
self.save()
def run_before_save_methods(self):
if self._action=="save":


+ 2
- 2
frappe/model/mapper.py 파일 보기

@@ -20,8 +20,8 @@ def get_mapped_doclist(from_doctype, from_docname, table_maps, target_doclist=No
if not ignore_permissions and not frappe.has_permission(from_doctype, "read", source.doc):
frappe.msgprint("No Permission", raise_exception=frappe.PermissionError)

source_meta = frappe.get_doctype(from_doctype)
target_meta = frappe.get_doctype(table_maps[from_doctype]["doctype"])
source_meta = frappe.get_meta(from_doctype)
target_meta = frappe.get_meta(table_maps[from_doctype]["doctype"])
# main
if target_doclist:


+ 11
- 5
frappe/model/meta.py 파일 보기

@@ -46,7 +46,17 @@ class Meta(Document):
def get_table_field_doctype(self, fieldname):
return { "fields": "DocField", "permissions": "DocPerm"}.get(fieldname)
def get_field(self, fieldname):
fields = self.get("fields", {"fieldname":fieldname})
return fields[0] if fields else None

def get_label(self, fieldname):
return self.get_field(fieldname).label
def get_options(self, fieldname):
return self.get_field(fieldname).options
def process(self):
self.add_custom_fields()
self.apply_property_setters()
@@ -174,11 +184,7 @@ def get_table_fields(doctype):

def has_field(doctype, fieldname, parent=None, parentfield=None):
return get_field(doctype, fieldname, parent=None, parentfield=None) and True or False
def get_field(doctype, fieldname, parent=None, parentfield=None):
doclist = frappe.get_doctype(doctype)
return doclist.get_field(fieldname, parent, parentfield)
def get_field_currency(df, doc):
"""get currency based on DocField options and fieldvalue in doc"""
currency = None


+ 1
- 1
frappe/model/naming.py 파일 보기

@@ -12,7 +12,7 @@ def set_new_name(doc):
return

doc._new_name_set = True
autoname = frappe.get_doctype(doc.doctype)[0].autoname
autoname = frappe.get_meta(doc.doctype).autoname
doc.localname = doc.name # for passing back to client

# amendments


+ 0
- 103
frappe/model/utils.py 파일 보기

@@ -4,113 +4,10 @@
from __future__ import unicode_literals
import frappe, json
from frappe import _
from frappe.model.doc import Document
"""
Model utilities, unclassified functions
"""

def expand(docs):
"""
Expand a doclist sent from the client side. (Internally used by the request handler)
"""
def xzip(a,b):
d = {}
for i in range(len(a)):
d[a[i]] = b[i]
return d

docs = json.loads(docs)
clist = []
for d in docs['_vl']:
doc = xzip(docs['_kl'][d[0]], d);
clist.append(doc)
return clist

def compress(doclist):
"""
Compress a doclist before sending it to the client side. (Internally used by the request handler)

"""
docs = [isinstance(d, Document) and d.fields or d for d in doclist]

kl, vl = {}, []
forbidden = ['server_code_compiled']

# scan for keys & values
for d in docs:
dt = d['doctype']
if not (dt in kl.keys()):
kl[dt] = ['doctype','localname','__oldparent','__unsaved']

# add client script for doctype, doctype due to ambiguity
if dt=='DocType' and '__client_script' not in kl[dt]:
kl[dt].append('__client_script')

for f in d.keys():
if not (f in kl[dt]) and not (f in forbidden):
# if key missing, then append
kl[dt].append(f)

# build values
tmp = []
for f in kl[dt]:
v = d.get(f)
if type(v)==long:
v=int(v)
tmp.append(v)

vl.append(tmp)
return {'_vl':vl,'_kl':kl}


def getlist(doclist, field):
from frappe.utils import cint
l = []
for d in doclist:
if d.parentfield == field:
l.append(d)
l.sort(lambda a, b: cint(a.idx) - cint(b.idx))
return l

def copy_doclist(doclist, no_copy = []):
"""
Save & return a copy of the given doclist
Pass fields that are not to be copied in `no_copy`
"""

cl = []

# main doc
c = Document(fielddata = doclist[0].fields.copy())

# clear no_copy fields
for f in no_copy:
if c.fields.has_key(f):
c.fields[f] = None

c.name = None
c.save(1)
cl.append(c)

# new parent name
parent = c.name

# children
for d in doclist[1:]:
c = Document(fielddata = d.fields.copy())
c.name = None

# clear no_copy fields
for f in no_copy:
if c.fields.has_key(f):
c.fields[f] = None

c.parent = parent
c.save(1)
cl.append(c)

return cl

def set_default(doc, key):
if not doc.is_default:
frappe.db.set(doc, "is_default", 1)


+ 3
- 3
frappe/test_runner.py 파일 보기

@@ -165,10 +165,10 @@ def make_test_objects(doctype, test_records, verbose=None):
def print_mandatory_fields(doctype):
print "Please setup make_test_records for: " + doctype
print "-" * 60
doctype_obj = frappe.get_doctype(doctype)
print "Autoname: " + (doctype_obj[0].autoname or "")
meta = frappe.get_meta(doctype)
print "Autoname: " + (meta.autoname or "")
print "Mandatory Fields: "
for d in doctype_obj.get({"reqd":1}):
for d in meta.get({"reqd":1}):
print d.parent + ":" + d.fieldname + " | " + d.fieldtype + " | " + (d.options or "")
print



+ 4
- 4
frappe/translate.py 파일 보기

@@ -141,11 +141,11 @@ def get_messages_for_app(app):

def get_messages_from_doctype(name):
messages = []
meta = frappe.get_doctype(name)
meta = frappe.get_meta(name)
messages = [meta[0].name, meta[0].description, meta[0].module]
messages = [meta.name, meta.description, meta.module]
for d in meta.get({"doctype":"DocField"}):
for d in meta.get("fields"):
messages.extend([d.label, d.description])
if d.fieldtype=='Select' and d.options \
@@ -156,7 +156,7 @@ def get_messages_from_doctype(name):
messages.extend(options)
# extract from js, py files
doctype_file_path = frappe.get_module_path(meta[0].module, "doctype", meta[0].name, meta[0].name)
doctype_file_path = frappe.get_module_path(meta.module, "doctype", meta.name, meta.name)
messages.extend(get_messages_from_file(doctype_file_path + ".js"))
return clean(messages)


+ 3
- 7
frappe/utils/datautils.py 파일 보기

@@ -92,17 +92,13 @@ class UnicodeWriter:
def getvalue(self):
return self.queue.getvalue()
def check_record(d, parenttype=None, doctype_dl=None):
def check_record(d):
"""check for mandatory, select options, dates. these should ideally be in doclist"""
from frappe.utils.dateutils import parse_date
if parenttype and not d.get('parent'):
frappe.msgprint(_("Parent is required."), raise_exception=1)

if not doctype_dl:
doctype_dl = frappe.model.doctype.get(d.doctype)
d = frappe.get_doc(d)

for key in d:
docfield = doctype_dl.get_field(key)
docfield = d.meta.get_field(key)
val = d[key]
if docfield:
if docfield.reqd and (val=='' or val==None):


+ 1
- 1
frappe/website/doctype/blog_post/test_blog_post.py 파일 보기

@@ -163,7 +163,7 @@ class TestBlogPost(unittest.TestCase):
self.assertTrue(bean.has_read_perm())
def test_set_only_once(self):
blog_post = frappe.get_doctype("Blog Post")
blog_post = frappe.get_meta("Blog Post")
blog_post.get_field("title").set_only_once = 1
bean = frappe.bean("Blog Post", "_test-blog-post-1")
bean.doc.title = "New"


+ 1
- 1
frappe/website/doctype/website_group/website_group.py 파일 보기

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe.website.website_generator import WebsiteGenerator
from frappe.templates.generators.website_group import clear_cache
from frappe.model.doc import make_autoname
from frappe.model.naming import make_autoname

class WebsiteGroup(WebsiteGenerator):


+ 5
- 4
frappe/widgets/form/utils.py 파일 보기

@@ -3,6 +3,7 @@

from __future__ import unicode_literals
import frappe, json
import frappe.widgets.form.meta

from frappe import _

@@ -87,13 +88,13 @@ def get_next(doctype, name, prev):
@frappe.whitelist()
def get_linked_docs(doctype, name, metadata_loaded=None):
if not metadata_loaded: metadata_loaded = []
meta = frappe.get_doctype(doctype, True)
linkinfo = meta[0].get("__linked_with")
meta = frappe.widgets.form.meta.get_meta(doctype)
linkinfo = meta.get("__linked_with")
results = {}
for dt, link in linkinfo.items():
link["doctype"] = dt
linkmeta = frappe.get_doctype(dt, True)
if not linkmeta[0].get("issingle"):
linkmeta = frappe.widgets.form.meta.get_meta(dt)
if not linkmeta.get("issingle"):
fields = [d.fieldname for d in linkmeta.get({"parent":dt, "in_list_view":1,
"fieldtype": ["not in", ["Image", "HTML", "Button", "Table"]]})] \
+ ["name", "modified", "docstatus"]


+ 1
- 2
frappe/widgets/reportview.py 파일 보기

@@ -52,8 +52,7 @@ def compress(data):
@frappe.whitelist()
def save_report():
"""save report"""
from frappe.model.doc import Document
data = frappe.local.form_dict
if frappe.db.exists('Report', data['name']):
d = Document('Report', data['name'])


+ 3
- 3
frappe/widgets/search.py 파일 보기

@@ -22,7 +22,7 @@ def search_widget(doctype, txt, query=None, searchfield="name", start=0,
import json
filters = json.loads(filters)

meta = frappe.get_doctype(doctype)
meta = frappe.get_meta(doctype)

standard_queries = frappe.get_hooks().standard_queries or []
if standard_queries:
@@ -68,12 +68,12 @@ def search_widget(doctype, txt, query=None, searchfield="name", start=0,

def get_std_fields_list(meta, key):
# get additional search fields
sflist = meta[0].search_fields and meta[0].search_fields.split(",") or []
sflist = meta.search_fields and meta.search_fields.split(",") or []
sflist = ['name'] + sflist
if not key in sflist:
sflist = sflist + [key]

return ['`tab%s`.`%s`' % (meta[0].name, f.strip()) for f in sflist]
return ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in sflist]

def build_for_autosuggest(res):
results = []


불러오는 중...
취소
저장