[feature] checkboxes in grid #2451version-14
@@ -7,6 +7,25 @@ from frappe import _ | |||||
from frappe.utils import cstr | from frappe.utils import cstr | ||||
from frappe.model import default_fields | from frappe.model import default_fields | ||||
@frappe.whitelist() | |||||
def make_mapped_doc(method, source_name, selected_children=None): | |||||
'''Returns the mapped document calling the given mapper method. | |||||
Sets selected_children as flags for the `get_mapped_doc` method. | |||||
Called from `open_mapped_doc` from create_new.js''' | |||||
method = frappe.get_attr(method) | |||||
if method not in frappe.whitelisted: | |||||
raise frappe.PermissionError | |||||
if selected_children: | |||||
selected_children = json.loads(selected_children) | |||||
frappe.flags.selected_children = selected_children or None | |||||
return method(source_name) | |||||
def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, | def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, | ||||
postprocess=None, ignore_permissions=False, ignore_child_tables=False): | postprocess=None, ignore_permissions=False, ignore_child_tables=False): | ||||
@@ -51,6 +70,13 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, | |||||
if not table_map["condition"](source_d): | if not table_map["condition"](source_d): | ||||
continue | continue | ||||
# if children are selected (checked from UI) for this table type, | |||||
# and this record is not in the selected children, then continue | |||||
if (frappe.flags.selected_children | |||||
and (df.fieldname in frappe.flags.selected_children) | |||||
and source_d.name not in frappe.flags.selected_children[df.fieldname]): | |||||
continue | |||||
target_child_doctype = table_map["doctype"] | target_child_doctype = table_map["doctype"] | ||||
target_parentfield = target_doc.get_parentfield_of_doctype(target_child_doctype) | target_parentfield = target_doc.get_parentfield_of_doctype(target_child_doctype) | ||||
@@ -58,10 +58,45 @@ frappe.ui.form.Grid = Class.extend({ | |||||
this.custom_buttons = {}; | this.custom_buttons = {}; | ||||
this.grid_buttons = this.wrapper.find('.grid-buttons'); | this.grid_buttons = this.wrapper.find('.grid-buttons'); | ||||
this.remove_rows_button = this.grid_buttons.find('.grid-remove-rows') | |||||
this.setup_allow_bulk_edit(); | this.setup_allow_bulk_edit(); | ||||
this.setup_check(); | |||||
}, | }, | ||||
setup_check: function() { | |||||
var me = this; | |||||
this.wrapper.on('click', '.grid-row-check', function(e) { | |||||
$check = $(this); | |||||
if($check.parents('.grid-heading-row:first').length!==0) { | |||||
$check.parents('.form-grid:first').find('.grid-row-check').prop('checked', $check.prop('checked')); | |||||
} | |||||
me.refresh_remove_rows_button(); | |||||
}); | |||||
this.remove_rows_button.on('click', function() { | |||||
me.get_selected().forEach(function(docname) { | |||||
me.grid_rows_by_docname[docname].remove(); | |||||
}); | |||||
setTimeout(function() { me.refresh_remove_rows_button(); }, 100); | |||||
}); | |||||
}, | |||||
refresh_remove_rows_button: function() { | |||||
this.remove_rows_button.toggleClass('hide', | |||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); | |||||
}, | |||||
get_selected: function() { | |||||
var selected = []; | |||||
var me = this; | |||||
this.wrapper.find('.grid-body .grid-row-check:checked').each(function() { | |||||
selected.push($(this).parents('.grid-row:first').attr('data-name')); | |||||
}); | |||||
return selected; | |||||
}, | |||||
refresh_checks: function() { | |||||
var show = this.is_editable() || this.frm.has_mapper(); | |||||
this.wrapper.find('.grid-row-check').toggle(show); | |||||
}, | |||||
make_head: function() { | make_head: function() { | ||||
// labels | // labels | ||||
if(!this.header_row) { | if(!this.header_row) { | ||||
@@ -132,6 +167,7 @@ frappe.ui.form.Grid = Class.extend({ | |||||
// toolbar | // toolbar | ||||
this.setup_toolbar(); | this.setup_toolbar(); | ||||
this.refresh_checks(); | |||||
// sortable | // sortable | ||||
if(this.is_sortable() && !this.sortable_setup_done) { | if(this.is_sortable() && !this.sortable_setup_done) { | ||||
@@ -143,6 +179,8 @@ frappe.ui.form.Grid = Class.extend({ | |||||
this.last_docname = this.frm.docname; | this.last_docname = this.frm.docname; | ||||
frappe.utils.scroll_to(_scroll_y); | frappe.utils.scroll_to(_scroll_y); | ||||
} | } | ||||
this.refresh_remove_rows_button(); | |||||
}, | }, | ||||
setup_toolbar: function() { | setup_toolbar: function() { | ||||
if(this.is_editable()) { | if(this.is_editable()) { | ||||
@@ -216,7 +254,6 @@ frappe.ui.form.Grid = Class.extend({ | |||||
return; | return; | ||||
} | } | ||||
new Sortable($rows.get(0), { | new Sortable($rows.get(0), { | ||||
group: {name: 'row'}, | group: {name: 'row'}, | ||||
handle: ".sortable-handle", | handle: ".sortable-handle", | ||||
@@ -545,6 +582,7 @@ frappe.ui.form.GridRow = Class.extend({ | |||||
init: function(opts) { | init: function(opts) { | ||||
this.on_grid_fields_dict = {}; | this.on_grid_fields_dict = {}; | ||||
this.on_grid_fields = []; | this.on_grid_fields = []; | ||||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">'; | |||||
this.columns = {}; | this.columns = {}; | ||||
this.columns_list = []; | this.columns_list = []; | ||||
$.extend(this, opts); | $.extend(this, opts); | ||||
@@ -552,9 +590,13 @@ frappe.ui.form.GridRow = Class.extend({ | |||||
}, | }, | ||||
make: function() { | make: function() { | ||||
var me = this; | var me = this; | ||||
this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this); | this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this); | ||||
this.row = $('<div class="data-row row sortable-handle"></div>').appendTo(this.wrapper) | this.row = $('<div class="data-row row sortable-handle"></div>').appendTo(this.wrapper) | ||||
.on("click", function() { | |||||
.on("click", function(e) { | |||||
if($(e.target).hasClass('grid-row-check')) { | |||||
return; | |||||
} | |||||
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) { | if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) { | ||||
// pass | // pass | ||||
} else { | } else { | ||||
@@ -563,6 +605,11 @@ frappe.ui.form.GridRow = Class.extend({ | |||||
} | } | ||||
}); | }); | ||||
// no checkboxes if too small | |||||
if(this.is_too_small()) { | |||||
this.row_check_html = ''; | |||||
} | |||||
if(this.grid.template && !this.grid.meta.editable_grid) { | if(this.grid.template && !this.grid.meta.editable_grid) { | ||||
this.render_template(); | this.render_template(); | ||||
} else { | } else { | ||||
@@ -582,7 +629,7 @@ frappe.ui.form.GridRow = Class.extend({ | |||||
this.wrapper | this.wrapper | ||||
.attr('data-name', this.doc.name) | .attr('data-name', this.doc.name) | ||||
.attr("data-idx", this.doc.idx) | .attr("data-idx", this.doc.idx) | ||||
.find(".row-index, .grid-form-row-index").html(this.doc.idx) | |||||
.find(".row-index span, .grid-form-row-index").html(this.doc.idx) | |||||
} | } | ||||
}, | }, | ||||
@@ -640,9 +687,9 @@ frappe.ui.form.GridRow = Class.extend({ | |||||
if(this.doc) { | if(this.doc) { | ||||
if(!this.row_index) { | if(!this.row_index) { | ||||
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \ | this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \ | ||||
margin-right: -20px;"></div>').appendTo(this.row); | |||||
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row); | |||||
} | } | ||||
this.row_index.html(this.doc.idx); | |||||
this.row_index.find('span').html(this.doc.idx); | |||||
} | } | ||||
this.row_display = $('<div class="row-data template-row">'+ | this.row_display = $('<div class="row-data template-row">'+ | ||||
@@ -659,11 +706,18 @@ frappe.ui.form.GridRow = Class.extend({ | |||||
// index (1, 2, 3 etc) | // index (1, 2, 3 etc) | ||||
if(!this.row_index) { | if(!this.row_index) { | ||||
this.row_index = $('<div class="row-index col col-xs-1">' + (this.doc ? this.doc.idx : " ")+ '</div>') | |||||
var txt = (this.doc ? this.doc.idx : " "); | |||||
this.row_index = $('<div class="row-index col col-xs-1">' + | |||||
this.row_check_html + | |||||
' <span>' + txt + '</span></div>') | |||||
.appendTo(this.row) | .appendTo(this.row) | ||||
.on('click', function() { me.toggle_view(); }); | |||||
.on('click', function(e) { | |||||
if(!$(e.target).hasClass('grid-row-check')) { | |||||
me.toggle_view(); | |||||
} | |||||
}); | |||||
} else { | } else { | ||||
this.row_index.html(this.doc ? this.doc.idx : " "); | |||||
this.row_index.find('span').html(txt); | |||||
} | } | ||||
this.setup_columns(); | this.setup_columns(); | ||||
@@ -678,6 +732,10 @@ frappe.ui.form.GridRow = Class.extend({ | |||||
this.row.toggleClass('editable-row', this.grid.is_editable()); | this.row.toggleClass('editable-row', this.grid.is_editable()); | ||||
}, | }, | ||||
is_too_small: function() { | |||||
return this.row.width() < 400; | |||||
}, | |||||
add_open_form_button: function() { | add_open_form_button: function() { | ||||
var me = this; | var me = this; | ||||
if(this.doc) { | if(this.doc) { | ||||
@@ -688,7 +746,7 @@ frappe.ui.form.GridRow = Class.extend({ | |||||
.appendTo($('<div class="col col-xs-1"></div>').appendTo(this.row)) | .appendTo($('<div class="col col-xs-1"></div>').appendTo(this.row)) | ||||
.on('click', function() { me.toggle_view(); return false; }); | .on('click', function() { me.toggle_view(); return false; }); | ||||
if(this.row.width() < 400) { | |||||
if(this.is_too_small()) { | |||||
// narrow | // narrow | ||||
this.open_form_button.css({'margin-right': '-2px'}); | this.open_form_button.css({'margin-right': '-2px'}); | ||||
} | } | ||||
@@ -99,11 +99,11 @@ frappe.ui.form.LinkSelector = Class.extend({ | |||||
}) | }) | ||||
}) | }) | ||||
} else { | } else { | ||||
$('<div class="alert alert-info">' + __("No Results") | |||||
+ (frappe.model.can_read(me.doctype) ? | |||||
('. <a class="new-doc">' | |||||
+ __("Make a new") + " " + __(me.doctype) + "</a>") : '') | |||||
+ '</div>').appendTo(parent).find(".new-doc").click(function() { | |||||
$('<p><br><span class="text-muted">' + __("No Results") + '</span>' | |||||
+ (frappe.model.can_create(me.doctype) ? | |||||
('<br><br><a class="new-doc btn btn-default btn-sm">' | |||||
+ __("Make a new {0}", [__(me.doctype)]) + "</a>") : '') | |||||
+ '</p>').appendTo(parent).find(".new-doc").click(function() { | |||||
me.target.new_doc(); | me.target.new_doc(); | ||||
}); | }); | ||||
} | } | ||||
@@ -7,6 +7,8 @@ | |||||
<div class="small form-clickable-section grid-footer"> | <div class="small form-clickable-section grid-footer"> | ||||
<div class="row"> | <div class="row"> | ||||
<div class="col-sm-6 grid-buttons"> | <div class="col-sm-6 grid-buttons"> | ||||
<button type="reset" class="btn btn-xs btn-danger grid-remove-rows hide"> | |||||
{%= __("Delete") %}</button> | |||||
<button type="reset" | <button type="reset" | ||||
class="grid-add-multiple-rows btn btn-xs btn-default hide" | class="grid-add-multiple-rows btn btn-xs btn-default hide" | ||||
style="margin-right: 10px;"> | style="margin-right: 10px;"> | ||||
@@ -275,9 +275,11 @@ $.extend(frappe.model, { | |||||
return frappe.call({ | return frappe.call({ | ||||
type: "POST", | type: "POST", | ||||
method: opts.method, | |||||
method: 'frappe.model.mapper.make_mapped_doc', | |||||
args: { | args: { | ||||
"source_name": opts.source_name | |||||
method: opts.method, | |||||
source_name: opts.source_name, | |||||
selected_children: opts.frm.get_selected() | |||||
}, | }, | ||||
freeze: true, | freeze: true, | ||||
callback: function(r) { | callback: function(r) { | ||||
@@ -398,6 +398,28 @@ _f.Frm.prototype.get_title = function() { | |||||
} | } | ||||
} | } | ||||
_f.Frm.prototype.get_selected = function() { | |||||
// returns list of children that are selected. returns [parentfield, name] for each | |||||
var selected = {}, me = this; | |||||
frappe.meta.get_table_fields(this.doctype).forEach(function(df) { | |||||
var _selected = me.fields_dict[df.fieldname].grid.get_selected(); | |||||
if(_selected.length) { | |||||
selected[df.fieldname] = _selected; | |||||
} | |||||
}); | |||||
return selected; | |||||
} | |||||
_f.Frm.prototype.has_mapper = function() { | |||||
// hackalert! | |||||
// if open_mapped_doc is mentioned in the custom script, then mapper exists | |||||
if(this._has_mapper === undefined) { | |||||
this._has_mapper = (this.meta.__js && this.meta.__js.search('open_mapped_doc')!==-1) ? | |||||
true: false; | |||||
} | |||||
return this._has_mapper; | |||||
} | |||||
_f.Frm.prototype.set_indicator_formatter = function(fieldname, get_color, get_text) { | _f.Frm.prototype.set_indicator_formatter = function(fieldname, get_color, get_text) { | ||||
// get doctype from parent | // get doctype from parent | ||||
if(frappe.meta.docfield_map[this.doctype][fieldname]) { | if(frappe.meta.docfield_map[this.doctype][fieldname]) { | ||||
@@ -26,8 +26,7 @@ class TestScheduler(TestCase): | |||||
self.assertTrue("all" in frappe.flags.ran_schedulers) | self.assertTrue("all" in frappe.flags.ran_schedulers) | ||||
def test_enabled_events(self): | def test_enabled_events(self): | ||||
val = json.dumps(["hourly", "hourly_long", "daily", "daily_long", "weekly", "weekly_long", "monthly", "monthly_long"]) | |||||
frappe.db.set_global('enabled_scheduler_events', val) | |||||
frappe.flags.enabled_events = ["hourly", "hourly_long", "daily", "daily_long", "weekly", "weekly_long", "monthly", "monthly_long"] | |||||
# maintain last_event and next_event on the same day | # maintain last_event and next_event on the same day | ||||
last_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0) | last_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0) | ||||
@@ -44,8 +43,7 @@ class TestScheduler(TestCase): | |||||
self.assertTrue("all" in frappe.flags.ran_schedulers) | self.assertTrue("all" in frappe.flags.ran_schedulers) | ||||
self.assertTrue("hourly" in frappe.flags.ran_schedulers) | self.assertTrue("hourly" in frappe.flags.ran_schedulers) | ||||
frappe.db.set_global('enabled_scheduler_events', "") | |||||
del frappe.flags['enabled_events'] | |||||
def test_enabled_events_day_change(self): | def test_enabled_events_day_change(self): | ||||
val = json.dumps(["daily", "daily_long", "weekly", "weekly_long", "monthly", "monthly_long"]) | val = json.dumps(["daily", "daily_long", "weekly", "weekly_long", "monthly", "monthly_long"]) | ||||
@@ -144,6 +144,9 @@ def trigger(site, event, queued_jobs=(), now=False): | |||||
if not queued_jobs and not now: | if not queued_jobs and not now: | ||||
queued_jobs = get_jobs(site=site, queue=queue) | queued_jobs = get_jobs(site=site, queue=queue) | ||||
if frappe.flags.in_test: | |||||
frappe.flags.ran_schedulers.append(event) | |||||
events = get_scheduler_events(event) | events = get_scheduler_events(event) | ||||
if not events: | if not events: | ||||
return | return | ||||
@@ -155,8 +158,6 @@ def trigger(site, event, queued_jobs=(), now=False): | |||||
else: | else: | ||||
scheduler_task(site=site, event=event, handler=handler, now=True) | scheduler_task(site=site, event=event, handler=handler, now=True) | ||||
if frappe.flags.in_test: | |||||
frappe.flags.ran_schedulers.append(event) | |||||
def get_scheduler_events(event): | def get_scheduler_events(event): | ||||
'''Get scheduler events from hooks and integrations''' | '''Get scheduler events from hooks and integrations''' | ||||
@@ -193,6 +194,9 @@ def log(method, message=None): | |||||
return message | return message | ||||
def get_enabled_scheduler_events(): | def get_enabled_scheduler_events(): | ||||
if 'enabled_events' in frappe.flags: | |||||
return frappe.flags.enabled_events | |||||
enabled_events = frappe.db.get_global("enabled_scheduler_events") | enabled_events = frappe.db.get_global("enabled_scheduler_events") | ||||
if enabled_events: | if enabled_events: | ||||
if isinstance(enabled_events, basestring): | if isinstance(enabled_events, basestring): | ||||