Merge branch 'develop' of github.com:frappe/frappe into develop

This commit is contained in:
Nabin Hait 2014-02-19 11:07:11 +05:30
commit ee0d67f5c4
17 changed files with 299 additions and 198 deletions

View file

@ -402,9 +402,10 @@ def get_pymodule_path(modulename, *joins):
def get_module_list(app_name):
return get_file_items(os.path.join(os.path.dirname(get_module(app_name).__file__), "modules.txt"))
def get_all_apps(with_frappe=False):
apps = get_file_items(os.path.join(local.sites_path, "apps.txt")) \
+ get_file_items(os.path.join(local.site_path, "apps.txt"))
def get_all_apps(with_frappe=False, with_internal_apps=True):
apps = get_file_items(os.path.join(local.sites_path, "apps.txt"))
if with_internal_apps:
apps.extend(get_file_items(os.path.join(local.site_path, "apps.txt")))
if with_frappe:
apps.insert(0, 'frappe')
return apps

View file

@ -8,7 +8,14 @@ import sys, os
import frappe
site_arg_optional = []
site_arg_optional = ['serve']
def get_site(parsed_args):
if not parsed_args.get("site") and os.path.exists(os.path.join(parsed_args["sites_path"], "currentsite.txt")):
with open(os.path.join(parsed_args["sites_path"], "currentsite.txt"), "r") as sitefile:
parsed_args["site"] = sitefile.read().strip()
return parsed_args["site"]
return parsed_args.get("site")
def main():
parsed_args = frappe._dict(vars(setup_parser()))
@ -28,21 +35,11 @@ def main():
frappe.init(site, sites_path=sites_path)
run(fn, args)
else:
if not fn in site_arg_optional:
if not parsed_args.get("site") and os.path.exists(os.path.join(sites_path, "currentsite.txt")):
with open(os.path.join(sites_path, "currentsite.txt"), "r") as sitefile:
parsed_args["site"] = sitefile.read().strip()
site = parsed_args.get("site")
if not site:
print "Site argument required"
exit(1)
if fn != 'install' and not os.path.exists(os.path.join(parsed_args["sites_path"], site)):
print "Did not find folder '{}'. Are you in sites folder?".format(parsed_args.get("site"))
exit(1)
site = get_site(parsed_args)
if fn not in site_arg_optional and not site:
print 'site argument required'
exit(1)
elif site:
frappe.init(site, sites_path=sites_path)
run(fn, parsed_args)
else:
@ -639,7 +636,7 @@ def run_tests(app=None, module=None, doctype=None, verbose=False):
exit(1)
@cmd
def serve(port=8000, profile=False, sites_path='.'):
def serve(port=8000, profile=False, sites_path='.', site=None):
import frappe.app
frappe.app.serve(port=port, profile=profile, site=frappe.local.site, sites_path=sites_path)

View file

@ -292,10 +292,7 @@ class Bean:
def has_permission(self, permtype):
return frappe.has_permission(self.doc.doctype, permtype, self.doc)
def update_value(self, field, value):
frappe.conn.set(self.doc, field, value)
def save(self, check_links=1, ignore_permissions=None):
if ignore_permissions:
self.ignore_permissions = ignore_permissions

View file

@ -0,0 +1,14 @@
import frappe
def execute():
frappe.conn.sql("""update `tabWebsite Sitemap` set idx=null""")
# frappe.conn.sql("""update `tabWeb Page` set idx=null""")
# frappe.conn.sql("""update `tabBlog Post` set idx=null""")
# frappe.conn.sql("""update `tabBlog Category` set idx=null""")
# frappe.conn.sql("""update `tabWebsite Group` set idx=null""")
# frappe.conn.sql("""delete from `tabTable of Contents`""")
for doctype in ["Blog Category", "Blog Post", "Web Page", "Website Group"]:
for name in frappe.conn.get_values("Website Sitemap", {"ref_doctype":doctype}, "docname"):
frappe.bean(doctype, name[0]).save()

View file

@ -41,6 +41,7 @@
"public/css/navbar.css",
"public/css/slickgrid.css",
"public/css/tree_grid.css",
"public/css/tree.css",
"public/css/nprogress.css"
],
"js/frappe.min.js": [

View file

@ -344,13 +344,6 @@ ul.linked-with-list li {
font-weight: bold;
}
.tree-node-toolbar {
padding: 2px 5px;
margin-left: 15px;
border-radius: 3px;
background-color: #ddd;
}
.print-preview {
padding: 50px 20px;
margin: 0px -15px;

View file

@ -0,0 +1,31 @@
.tree li {
list-style: none;
}
.tree ul {
margin-top: 2px;
}
.tree-link {
cursor: pointer;
}
.tree-hover {
background-color: #eee;
min-height: 20px;
border: 1px solid #ddd;
}
.tree-node-toolbar {
display: inline-block;
padding: 0px 5px;
margin-left: 15px;
border-radius: 3px;
background-color: #ddd;
}
.tree-toolbar-item {
display: inline-block;
padding: 0px 5px;
padding-top: 1px;
border-right: 1px solid #aaa;
font-size: 90%;
}
.tree-toolbar-item:last-child {
border-right: 0px;
}

View file

@ -16,43 +16,58 @@ frappe.ui.Tree = Class.extend({
parent_label: null,
expandable: true
});
this.set_style();
this.rootnode.toggle();
},
get_selected_node: function() {
return this.$w.find('.tree-link.selected').data('node');
return this.selected_node;
},
set_style: function() {
frappe.dom.set_style("\
.tree li { list-style: none; }\
.tree ul { margin-top: 2px; }\
.tree-link { cursor: pointer; }\
")
toggle: function() {
this.get_selected_node().toggle();
}
})
frappe.ui.TreeNode = Class.extend({
init: function(args) {
var me = this;
$.extend(this, args);
this.loaded = false;
this.expanded = false;
this.tree.nodes[this.label] = this;
if(this.parent_label)
this.parent_node = this.tree.nodes[this.parent_label];
this.make();
this.setup_drag_drop();
if(this.tree.onrender) {
this.tree.onrender(this);
}
},
make: function() {
var me = this;
this.$a = $('<span class="tree-link">')
.click(function() {
if(me.expandable && me.tree.method && !me.loaded) {
me.load()
} else {
me.selectnode();
me.tree.selected_node = me;
if(me.tree.toolbar) {
me.show_toolbar();
}
if(me.tree.click) me.tree.click(this);
if(me.toggle_on_click) {
me.toggle();
}
if(me.tree.click)
me.tree.click(this);
})
.bind('reload', function() { me.reload(); })
.data('label', this.label)
.data('node', this)
.appendTo(this.parent);
this.$ul = $('<ul class="tree-children">')
.toggle(false).appendTo(this.parent);
this.make_icon();
},
make_icon: function() {
// label with icon
var icon_html = '<i class="icon-fixed-width icon-file"></i>';
if(this.expandable) {
@ -60,19 +75,77 @@ frappe.ui.TreeNode = Class.extend({
}
$(icon_html + ' <a class="tree-label">' + this.label + "</a>").
appendTo(this.$a);
if(this.tree.onrender) {
this.tree.onrender(this);
},
toggle: function(callback) {
if(this.expandable && this.tree.method && !this.loaded) {
this.load(callback)
} else {
this.toggle_node(callback);
}
},
selectnode: function() {
show_toolbar: function() {
if(this.tree.cur_toolbar)
$(this.tree.cur_toolbar).toggle(false);
if(!this.toolbar)
this.make_toolbar();
this.tree.cur_toolbar = this.toolbar;
this.toolbar.toggle(true);
},
make_toolbar: function() {
var me = this;
this.toolbar = $('<span class="tree-node-toolbar"></span>').insertAfter(this.$a);
$.each(this.tree.toolbar, function(i, d) {
$("<a class='tree-toolbar-item'>")
.html(d.label)
.appendTo(me.toolbar)
.click(function() { d.click(me, this); return false; });
})
},
setup_drag_drop: function() {
// experimental
var me = this;
if(this.tree.drop && this.parent_label) {
this.$ul.droppable({
hoverClass: "tree-hover",
greedy: true,
drop: function(event, ui) {
event.preventDefault();
var dragged_node = $(ui.draggable).find(".tree-link:first").data("node");
var dropped_node = $(this).parent().find(".tree-link:first").data("node");
me.tree.drop(dragged_node, dropped_node, $(ui.draggable), $(this));
return false;
}
});
}
},
addnode: function(data) {
var $li = $('<li class="tree-node">');
if(this.tree.drop) $li.draggable({revert:true});
return new frappe.ui.TreeNode({
tree:this.tree,
parent: $li.appendTo(this.$ul),
parent_label: this.label,
label: data.value,
expandable: data.expandable,
data: data
});
},
toggle_node: function(callback) {
// expand children
if(this.$ul) {
this.$ul.toggle();
if(this.$ul.children().length) {
this.$ul.toggle(!this.expanded);
}
// open close icon
this.$a.find('i').removeClass();
if(this.$ul.css('display').toLowerCase()=='block') {
if(!this.expanded) {
this.$a.find('i').addClass('icon-fixed-width icon-folder-open');
} else {
this.$a.find('i').addClass('icon-fixed-width icon-folder-close');
@ -84,43 +157,22 @@ frappe.ui.TreeNode = Class.extend({
.removeClass('selected');
this.$a.toggleClass('selected');
this.expanded = !this.expanded;
if(callback) callback();
},
reload: function() {
if(this.expanded) {
this.$a.click(); // collapse
}
if(this.$ul) {
this.$ul.empty();
}
this.load();
},
addnode: function(data) {
if(!this.$ul) {
this.$ul = $('<ul>').toggle(false).appendTo(this.parent);
}
return new frappe.ui.TreeNode({
tree:this.tree,
parent: $('<li>').appendTo(this.$ul),
parent_label: this.label,
label: data.value,
expandable: data.expandable,
data: data
});
},
load: function() {
load: function(callback) {
var me = this;
args = $.extend(this.tree.args || {}, {
parent: this.data ? this.data.value : null
});
$(me.$a).set_working();
return frappe.call({
method: this.tree.method,
args: args,
callback: function(r) {
$(me.$a).done_working();
me.$ul.empty();
if (r.message) {
$.each(r.message, function(i, v) {
node = me.addnode(v);
@ -130,10 +182,10 @@ frappe.ui.TreeNode = Class.extend({
});
}
if(!me.expanded)
me.toggle_node(callback);
me.loaded = true;
// expand
me.selectnode();
}
})
}

View file

@ -9,6 +9,8 @@ from frappe.utils import global_date_format, get_fullname, cint
doctype = "Blog Post"
condition_field = "published"
sort_by = "published_on"
sort_order = "desc"
def get_context(context):
blog_post = context.bean.doc

View file

@ -36,7 +36,7 @@ def get_lang_info():
def rebuild_all_translation_files():
for lang in get_all_languages():
for app in get_all_apps():
for app in frappe.get_all_apps():
write_translations_file(app, lang)
def write_translations_file(app, lang, full_dict=None):
@ -195,7 +195,7 @@ def get_server_messages(app):
def get_messages_from_include_files(app_name=None):
messages = []
hooks = frappe.get_hooks(app_name)
for file in (hooks.app_include_js or []) + (hooks.web_include_js or []):
for file in (frappe.get_hooks("app_include_js") or []) + (frappe.get_hooks("web_include_js") or []):
messages.extend(get_messages_from_file(os.path.join(frappe.local.sites_path, file)))
return messages

View file

@ -2,7 +2,7 @@
{
"creation": "2013-03-08 09:41:11",
"docstatus": 0,
"modified": "2014-02-13 17:31:52",
"modified": "2014-02-18 15:25:04",
"modified_by": "Administrator",
"owner": "Administrator"
},
@ -24,6 +24,7 @@
"permlevel": 0
},
{
"cancel": 0,
"doctype": "DocPerm",
"email": 1,
"name": "__common__",
@ -74,10 +75,10 @@
"label": "Page Name"
},
{
"cancel": 0,
"create": 1,
"delete": 1,
"doctype": "DocPerm",
"report": 1,
"role": "Website Manager",
"write": 1
},

View file

@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint
from frappe.utils.nestedset import DocTypeNestedSet
sitemap_fields = ("page_name", "ref_doctype", "docname", "page_or_generator", "idx",
@ -29,22 +30,38 @@ class DocType(DocTypeNestedSet):
self.rename()
self.check_if_page_name_is_unique()
self.make_private_if_parent_is_private()
if not self.doc.is_new():
self.renumber_if_moved()
self.set_idx()
def renumber_if_moved(self):
if self.doc.old_parent != self.doc.parent_website_sitemap:
frappe.conn.sql("""update `tabWebsite Sitemap` set idx=idx-1
where parent_website_sitemap=%s and idx>%s""", (self.doc.old_parent, self.doc.idx))
frappe.conn.sql("""update `tab{}` set idx=idx-1
where parent_website_sitemap=%s and idx>%s""".format(self.doc.ref_doctype),
(self.doc.old_parent, self.doc.idx))
self.doc.idx = None
def set_idx(self):
if self.doc.idx==None:
self.doc.idx = int(frappe.conn.sql("""select max(idx) from `tabWebsite Sitemap`
where parent_website_sitemap=%s and name!=%s""", (self.doc.parent_website_sitemap,
self.doc.idx = int(frappe.conn.sql("""select max(ifnull(idx, -1)) from `tabWebsite Sitemap`
where ifnull(parent_website_sitemap, '')=%s and name!=%s""",
(self.doc.parent_website_sitemap or '',
self.doc.name))[0][0] or 0) + 1
else:
if self.doc.idx != 0 and self.doc.parent_website_sitemap:
if not frappe.conn.get_value("Website Sitemap", {
"idx": self.doc.idx -1,
"parent_website_sitemap":self.doc.parent_website_sitemap
}):
if cint(self.doc.idx) != 0 and self.doc.parent_website_sitemap:
if not frappe.conn.sql("""select name from `tabWebsite Sitemap` where
ifnull(parent_website_sitemap, '')=%s and ifnull(idx, -1)=%s""",
(self.doc.parent_website_sitemap or '', cint(self.doc.idx) - 1)):
frappe.throw("{}: {}".format(
_("Sitemap Ordering Error. Index missing"), self.doc.idx-1))
def on_update(self):
if not frappe.flags.in_rebuild_config:
DocTypeNestedSet.on_update(self)
def rename(self):
from frappe.website.render import clear_cache
self.old_name = self.doc.name
@ -54,14 +71,14 @@ class DocType(DocTypeNestedSet):
self.rename_links()
self.rename_descendants()
clear_cache(self.old_name)
def rename_links(self):
for doctype in frappe.conn.sql_list("""select parent from tabDocField where fieldtype='Link' and
fieldname='parent_website_sitemap' and options='Website Sitemap'"""):
for name in frappe.conn.sql_list("""select name from `tab{}`
where parent_website_sitemap=%s""".format(doctype), self.old_name):
frappe.conn.set_value(doctype, name, "parent_website_sitemap", self.doc.name)
def rename_descendants(self):
# rename children
for name in frappe.conn.sql_list("""select name from `tabWebsite Sitemap`
@ -69,10 +86,6 @@ class DocType(DocTypeNestedSet):
child = frappe.bean("Website Sitemap", name)
child.doc.parent_website_sitemap = self.doc.name
child.save()
def on_update(self):
if not frappe.flags.in_rebuild_config:
DocTypeNestedSet.on_update(self)
def check_if_page_name_is_unique(self):
exists = False

View file

@ -2,7 +2,7 @@
{
"creation": "2013-11-18 15:38:40",
"docstatus": 0,
"modified": "2014-02-10 18:10:11",
"modified": "2014-02-18 15:23:12",
"modified_by": "Administrator",
"owner": "Administrator"
},
@ -22,7 +22,9 @@
"permlevel": 0
},
{
"cancel": 0,
"create": 0,
"delete": 1,
"doctype": "DocPerm",
"name": "__common__",
"parent": "Website Sitemap",

View file

@ -45,7 +45,7 @@ class DocType:
def rebuild_website_sitemap_config():
# TODO
frappe.flags.in_rebuild_config = True
frappe.conn.sql("""delete from `tabWebsite Sitemap Config`""")
for app in frappe.get_installed_apps():
if app=="webnotes": app="frappe"
@ -64,15 +64,25 @@ def build_website_sitemap_config(app):
config = {"pages": {}, "generators":{}}
basepath = frappe.get_pymodule_path(app)
# pages
pages = []
generators = []
for config_type in ("pages", "generators"):
path = os.path.join(basepath, "templates", config_type)
if os.path.exists(path):
for fname in os.listdir(path):
fname = frappe.utils.cstr(fname)
if fname.split(".")[-1] in ("html", "xml", "js", "css"):
name = add_website_sitemap_config("Page" if config_type=="pages" else "Generator",
app, path, fname, basepath)
if config_type=="pages":
pages.append(["Page", app, path, fname, basepath])
else:
generators.append(["Generator", app, path, fname, basepath])
for args in pages:
add_website_sitemap_config(*args)
for args in generators:
add_website_sitemap_config(*args)
frappe.conn.commit()
@ -100,6 +110,8 @@ def add_website_sitemap_config(page_or_generator, app, path, fname, basepath):
wsc.ref_doctype = getattr(module, "doctype", None)
wsc.page_name_field = getattr(module, "page_name_field", "page_name")
wsc.condition_field = getattr(module, "condition_field", None)
wsc.sort_by = getattr(module, "sort_by", "name")
wsc.sort_order = getattr(module, "sort_order", "asc")
wsc.base_template_path = getattr(module, "base_template_path", None)
wsc.page_title = getattr(module, "page_title", _(name.title()))

View file

@ -2,7 +2,7 @@
{
"creation": "2013-11-18 15:35:00",
"docstatus": 0,
"modified": "2014-02-14 12:48:54",
"modified": "2014-02-18 15:22:32",
"modified_by": "Administrator",
"owner": "Administrator"
},
@ -22,7 +22,9 @@
"permlevel": 0
},
{
"cancel": 0,
"create": 0,
"delete": 1,
"doctype": "DocPerm",
"export": 0,
"name": "__common__",
@ -129,6 +131,18 @@
"label": "Condition Field",
"read_only": 1
},
{
"doctype": "DocField",
"fieldname": "sort_by",
"fieldtype": "Data",
"label": "Sort By"
},
{
"doctype": "DocField",
"fieldname": "sort_order",
"fieldtype": "Data",
"label": "Sort Order"
},
{
"doctype": "DocPerm"
}

View file

@ -9,7 +9,7 @@ frappe.pages['sitemap-browser'].onload = function(wrapper) {
wrapper.appframe.add_module_icon("Website")
wrapper.appframe.set_title_right('Refresh', function() {
wrapper.make_tree();
frappe.website.sitemap.tree.rootnode.reload();
});
$(wrapper)
@ -25,7 +25,6 @@ frappe.pages['sitemap-browser'].onload = function(wrapper) {
"min-height": "300px",
"padding-bottom": "25px"
}));
}
frappe.provide("frappe.website");
@ -38,92 +37,54 @@ frappe.website.SitemapBrowser = Class.extend({
parent: $(parent),
label: "Sitemap",
method: 'frappe.website.page.sitemap_browser.sitemap_browser.get_children',
click: function(link) {
if(me.cur_toolbar)
$(me.cur_toolbar).toggle(false);
if(!link.toolbar)
me.make_link_toolbar(link);
if(link.toolbar) {
me.cur_toolbar = link.toolbar;
$(me.cur_toolbar).toggle(true);
toolbar: [
{
label:frappe._("Expand"),
click:function(node, btn) {
node.toggle(function() {
$(btn).html(node.expanded ? frappe._("Collapse") : frappe._("Expand"));
});
}
},
{
label: frappe._("Update Parent"),
click: function(node, btn) {
me.update_parent();
}
},
{
label: frappe._("Up"),
click: function(node, btn) {
me.up_or_down("up");
}
},
{
label: frappe._("Down"),
click: function(node, btn) {
me.up_or_down("down");
}
},
{
label: frappe._("Open"),
click: function(node, btn) {
frappe.set_route("Form", node.data.ref_doctype, node.data.docname);
}
}
}
]
// drop: function(dragged_node, dropped_node, dragged_element, dropped_element) {
// frappe.website.sitemap.update_parent(dragged_node.label, dropped_node.label, function(r) {
// if(!r.exc) {
// dragged_element.remove();
// dropped_node.reload();
// }
// });
// }
});
this.tree.rootnode.$a
.data('node-data', {value: "Sitemap", expandable:1})
.click();
},
make_link_toolbar: function(link) {
var data = $(link).data('node-data');
if(!data) return;
link.toolbar = $('<span class="tree-node-toolbar"></span>').insertAfter(link);
// edit
var node_links = [];
node_links.push('<a onclick="frappe.website.sitemap.open();">'+frappe._('Edit')+'</a>');
node_links.push('<a onclick="frappe.website.sitemap.move();">'+frappe._('Move')+'</a>');
node_links.push('<a onclick="frappe.website.sitemap.up_or_down(\'up\');">'+frappe._('Up')+'</a>');
node_links.push('<a onclick="frappe.website.sitemap.up_or_down(\'down\');">'+frappe._('Down')+'</a>');
// if(data.expandable) {
// node_links.push('<a onclick="frappe.website.sitemap.new_node();">' + frappe._('Add Child') + '</a>');
// }
//
// node_links.push('<a onclick="frappe.website.sitemap.delete()">' + frappe._('Delete') + '</a>');
link.toolbar.append(node_links.join(" | "));
},
new_node: function() {
var me = this;
var fields = [
{fieldtype:'Data', fieldname: 'name_field',
label:'New ' + me.ctype + ' Name', reqd:true},
{fieldtype:'Select', fieldname:'is_group', label:'Group Node', options:'No\nYes',
description: frappe._("Further nodes can be only created under 'Group' type nodes")},
{fieldtype:'Button', fieldname:'create_new', label:'Create New' }
]
if(me.ctype == "Sales Person") {
fields.splice(-1, 0, {fieldtype:'Link', fieldname:'employee', label:'Employee',
options:'Employee', description: frappe._("Please enter Employee Id of this sales parson")});
}
// the dialog
var d = new frappe.ui.Dialog({
title: frappe._('New ') + frappe._(me.ctype),
fields: fields
})
d.set_value("is_group", "No");
// create
$(d.fields_dict.create_new.input).click(function() {
var btn = this;
$(btn).set_working();
var v = d.get_values();
if(!v) return;
var node = me.selected_node();
v.parent = node.data('label');
v.ctype = me.ctype;
return frappe.call({
method: 'erpnext.selling.page.sales_browser.sales_browser.add_node',
args: v,
callback: function() {
$(btn).done_working();
d.hide();
node.trigger('reload');
}
})
});
d.show();
},
selected_node: function() {
return this.tree.$w.find('.tree-link.selected');
},
@ -140,13 +101,16 @@ frappe.website.SitemapBrowser = Class.extend({
"up_or_down": up_or_down
},
callback: function(r) {
(node.parent_node || node).reload();
if(r.message==="ok") {
node.parent.insertBefore(up_or_down==="up" ?
node.parent.prev() : node.parent.next().next());
//(node.parent_node || node).reload();
}
}
});
},
move: function() {
update_parent: function() {
var me = this;
var node = this.selected_node();
if(!this.move_dialog) {
this.move_dialog = new frappe.ui.Dialog({
title: frappe._("Move"),
@ -166,23 +130,29 @@ frappe.website.SitemapBrowser = Class.extend({
]
});
this.move_dialog.get_input("update").on("click", function() {
var node = me.tree.get_selected_node();
var values = me.move_dialog.get_values();
if(!values) return;
frappe.call({
method: "frappe.website.page.sitemap_browser.sitemap_browser.update_parent",
args: {
"name": node.data("label"),
"new_parent": values.new_parent
},
callback: function(r) {
me.move_dialog.hide();
me.tree.rootnode.reload();
}
});
me._update_parent(node.label, values.new_parent, function(r) {
me.move_dialog.hide();
(node.parent_node || node).reload();
})
});
}
this.move_dialog.show();
this.move_dialog.get_input("new_parent").val("");
},
_update_parent: function(name, parent, callback) {
frappe.call({
method: "frappe.website.page.sitemap_browser.sitemap_browser.update_parent",
args: {
"name": name,
"new_parent": parent
},
callback: function(r) {
callback(r);
}
});
}
});

View file

@ -14,7 +14,7 @@ def get_children(parent=None):
parent = ""
return frappe.conn.sql("""select name as value, 1 as expandable from `tabWebsite Sitemap` where
ifnull(parent_website_sitemap, '')=%s order by -idx desc""", parent, as_dict=True)
ifnull(parent_website_sitemap, '')=%s and idx is not null order by -idx desc""", parent, as_dict=True)
@frappe.whitelist()
def move(name, up_or_down):
@ -51,6 +51,7 @@ def move(name, up_or_down):
ret = "ok"
clear_cache()
return ret
@frappe.whitelist()
def update_parent(name, new_parent):