From 842d54f69b31cd74fb2ca9a7d2a1f4930e0e2513 Mon Sep 17 00:00:00 2001 From: "Abigail Hahn (Harvey Nash)" Date: Wed, 25 Apr 2018 17:28:05 -0700 Subject: [PATCH 1/6] Implemented the ability to configure conditions under which columns and rows are editable --- js/src/qgrid.widget.js | 67 ++++++++++++++++++++++++++++++++++++++++-- qgrid/grid.py | 39 ++++++++++++++++++++---- 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index 9d9b8fbf..e7009930 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -203,6 +203,8 @@ class QgridView extends widgets.DOMWidgetView { var columns = this.model.get('_columns'); this.data_view = this.create_data_view(df_json.data); this.grid_options = this.model.get('grid_options'); + this.column_definitions = this.model.get('column_definitions'); + this.row_edit_conditions = this.model.get('row_edit_conditions'); this.index_col_name = this.model.get("_index_col_name"); this.columns = []; @@ -321,7 +323,8 @@ class QgridView extends widgets.DOMWidgetView { id: cur_column.name, sortable: false, resizable: true, - cssClass: cur_column.type + cssClass: cur_column.type, + toolTip: cur_column.toolTip }; Object.assign(slick_column, type_info); @@ -345,10 +348,17 @@ class QgridView extends widgets.DOMWidgetView { // don't allow editing index columns if (cur_column.is_index) { slick_column.editor = editors.IndexEditor; - slick_column.cssClass += ' idx-col'; + if (this.grid_options.boldIndex) { + slick_column.cssClass += ' idx-col'; + } this.index_columns.push(slick_column); continue; } + + if ( ! (cur_column.editable) ) { + slick_column.editor = null; + } + this.columns.push(slick_column); } @@ -473,6 +483,59 @@ class QgridView extends widgets.DOMWidgetView { }); // set up callbacks + + // evaluate conditions under which cells should be disabled -- this occurs on a per-row basis, i.e., + var evaluateRowEditConditions = function(current_row, obj) { + var result; + + for (var op in obj) { + if (op == 'AND') { + if (result == null) { + result = true; + } + var and_result = true; + for (var cond in obj[op]) { + if (cond == 'AND' || cond == 'OR' || cond == 'NOT') { + and_result = and_result && evaluateRowEditConditions(current_row, {[cond]: obj[op][cond]}); + } else { + and_result = and_result && (current_row[cond] == obj[op][cond]); + } + } + result = result && and_result; + } else if (op == 'OR') { + if (result == null) { + result = false; + } + var or_result = false; + for (var cond in obj[op]) { + if (cond == 'AND' || cond == 'OR' || cond == 'NOT') { + or_result = or_result || evaluateRowEditConditions(current_row, {[cond]: obj[op][cond]}); + } else { + or_result = or_result || (current_row[cond] == obj[op][cond]); + } + } + result = result || or_result; + + } else if (op == 'NOT') { + if (result == null) { + result = true; + } + result = result && !evaluateRowEditConditions(current_row, {'AND': obj[op]}); + } else { + alert("Unsupported operation '" + op + "' found in cell edit conditions!") + } + } + return result; + } + + if ( ! (this.row_edit_conditions == null)) { + var conditions = this.row_edit_conditions; + var grid = this.slick_grid; + this.slick_grid.onBeforeEditCell.subscribe(function(e, args) { + return evaluateRowEditConditions(grid.getDataItem(args.row), conditions); + }); + } + this.slick_grid.onCellChange.subscribe((e, args) => { var column = this.columns[args.cell].name; var data_item = this.slick_grid.getDataItem(args.row); diff --git a/qgrid/grid.py b/qgrid/grid.py index 62105730..eb1b625a 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -38,7 +38,12 @@ def __init__(self): 'sortable': True, 'filterable': True, 'highlightSelectedCell': False, - 'highlightSelectedRow': True + 'highlightSelectedRow': True, + 'boldIndex': True + } + self._column_options = { + 'editable': True, + 'toolTip': "", } self._show_toolbar = False self._precision = None # Defer to pandas.get_option @@ -47,13 +52,15 @@ def set_grid_option(self, optname, optvalue): self._grid_options[optname] = optvalue def set_defaults(self, show_toolbar=None, precision=None, - grid_options=None): + grid_options=None, column_options=None): if show_toolbar is not None: self._show_toolbar = show_toolbar if precision is not None: self._precision = precision if grid_options is not None: self._grid_options = grid_options + if column_options is not None: + self._column_options = column_options @property def show_toolbar(self): @@ -67,11 +74,15 @@ def grid_options(self): def precision(self): return self._precision or pd.get_option('display.precision') - 1 + @property + def column_options(self): + return self._column_options + defaults = _DefaultSettings() -def set_defaults(show_toolbar=None, precision=None, grid_options=None): +def set_defaults(show_toolbar=None, precision=None, grid_options=None, column_options=None): """ Set the default qgrid options. The options that you can set here are the same ones that you can pass into ``QgridWidget`` constructor, with the @@ -94,7 +105,7 @@ def set_defaults(show_toolbar=None, precision=None, grid_options=None): The widget whose default behavior is changed by ``set_defaults``. """ defaults.set_defaults(show_toolbar=show_toolbar, precision=precision, - grid_options=grid_options) + grid_options=grid_options, column_options=column_options) def set_grid_option(optname, optvalue): @@ -166,7 +177,9 @@ def disable(): def show_grid(data_frame, show_toolbar=None, - precision=None, grid_options=None): + precision=None, grid_options=None, + column_options=None, column_definitions=None, + row_edit_conditions=None): """ Renders a DataFrame or Series as an interactive qgrid, represented by an instance of the ``QgridWidget`` class. The ``QgridWidget`` instance @@ -196,6 +209,12 @@ def show_grid(data_frame, show_toolbar=None, precision = defaults.precision if not isinstance(precision, Integral): raise TypeError("precision must be int, not %s" % type(precision)) + if column_options is None: + column_options = defaults.column_options + else: + options = defaults.column_options.copy() + options.update(column_options) + column_options = options if grid_options is None: grid_options = defaults.grid_options else: @@ -218,6 +237,9 @@ def show_grid(data_frame, show_toolbar=None, # create a visualization for the dataframe return QgridWidget(df=data_frame, precision=precision, grid_options=grid_options, + column_options=column_options, + column_definitions=column_definitions, + row_edit_conditions=row_edit_conditions, show_toolbar=show_toolbar) @@ -364,6 +386,9 @@ class QgridWidget(widgets.DOMWidget): df = Instance(pd.DataFrame) precision = Integer(6, sync=True) grid_options = Dict(sync=True) + column_options = Dict(sync=True) + column_definitions = Dict({}) + row_edit_conditions = Dict(sync=True) show_toolbar = Bool(False, sync=True) def __init__(self, *args, **kwargs): @@ -507,6 +532,10 @@ def _update_table(self, cur_column['position'] = i columns[col_name] = cur_column + columns[col_name].update(self.column_options) + if col_name in self.column_definitions.keys(): + columns[col_name].update(self.column_definitions[col_name]) + self._columns = columns # special handling for interval columns: convert to a string column From 773d94ed13e5c0070df54c5c82a6526ce483007d Mon Sep 17 00:00:00 2001 From: "Abigail Hahn (Harvey Nash)" Date: Wed, 25 Apr 2018 18:33:06 -0700 Subject: [PATCH 2/6] Implemented the ability to add a row programmatically, with support for non-integer indexes; improved the row edit condition evaluation --- js/src/qgrid.widget.js | 27 +++++++++++++++------------ qgrid/grid.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index e7009930..750049a2 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -484,7 +484,7 @@ class QgridView extends widgets.DOMWidgetView { // set up callbacks - // evaluate conditions under which cells should be disabled -- this occurs on a per-row basis, i.e., + // evaluate conditions under which cells in a row should be disabled (contingent on values of other cells in the same row) var evaluateRowEditConditions = function(current_row, obj) { var result; @@ -493,36 +493,39 @@ class QgridView extends widgets.DOMWidgetView { if (result == null) { result = true; } - var and_result = true; + //var and_result = true; for (var cond in obj[op]) { if (cond == 'AND' || cond == 'OR' || cond == 'NOT') { - and_result = and_result && evaluateRowEditConditions(current_row, {[cond]: obj[op][cond]}); + result = result && evaluateRowEditConditions(current_row, {[cond]: obj[op][cond]}); } else { - and_result = and_result && (current_row[cond] == obj[op][cond]); + result = result && (current_row[cond] == obj[op][cond]); } } - result = result && and_result; } else if (op == 'OR') { if (result == null) { result = false; } var or_result = false; for (var cond in obj[op]) { - if (cond == 'AND' || cond == 'OR' || cond == 'NOT') { - or_result = or_result || evaluateRowEditConditions(current_row, {[cond]: obj[op][cond]}); + if (cond == 'AND' || cond == 'OR' || cond == 'NAND' || cond == 'NOR') { + result = result || evaluateRowEditConditions(current_row, {[cond]: obj[op][cond]}); } else { - or_result = or_result || (current_row[cond] == obj[op][cond]); + result = result || (current_row[cond] == obj[op][cond]); } } - result = result || or_result; - - } else if (op == 'NOT') { + } else if (op == 'NAND') { if (result == null) { result = true; } result = result && !evaluateRowEditConditions(current_row, {'AND': obj[op]}); + } else if (op == 'NOR') { + if (result == null) { + result = true; + } + result = result && !evaluateRowEditConditions(current_row, {'OR': obj[op]}); + } else { - alert("Unsupported operation '" + op + "' found in cell edit conditions!") + alert("Unsupported operation '" + op + "' found in row edit conditions!") } } return result; diff --git a/qgrid/grid.py b/qgrid/grid.py index eb1b625a..e6eb0ef2 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -1061,6 +1061,39 @@ def add_row(self): scroll_to_row=df.index.get_loc(last.name)) self._trigger_df_change_event() + def add_row_internally(self, row): + """ + Append a new row to the end of the dataframe given a list of 2-tuples of (column name, column value). + This feature will work for dataframes with arbitrary index types. + """ + df = self._df + + col_names, col_data = zip(*row) + col_names = list(col_names) + col_data = list(col_data) + index_col_val = dict(row)[df.index.name] + + # check that the given column names match what already exists in the dataframe + required_cols = set(df.columns.values).union({df.index.name}) - {self._index_col_name} + if set(col_names) != required_cols: + msg = "Cannot add row -- column names don't match in the existing dataframe" + self.send({ + 'type': 'show_error', + 'error_msg': msg, + 'triggered_by': 'add_row' + }) + return + + for i, s in enumerate(col_data): + if col_names[i] == df.index.name: + continue + + df.loc[index_col_val, col_names[i]] = s + self._unfiltered_df.loc[index_col_val, col_names[i]] = s + + self._update_table(triggered_by='add_row', scroll_to_row=df.index.get_loc(index_col_val)) + self._trigger_df_change_event() + def remove_row(self): """ Remove the current row from the table. From 649fcb2b0227f4b770e46aeed930d5f05b0c977a Mon Sep 17 00:00:00 2001 From: "Abigail Hahn (Harvey Nash)" Date: Tue, 1 May 2018 13:54:06 -0700 Subject: [PATCH 3/6] Implemented a mechanism to toggle the 'editable' option for the entire grid --- js/src/qgrid.widget.js | 13 ++++++------- qgrid/grid.py | 5 ++++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index 750049a2..56458433 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -493,7 +493,6 @@ class QgridView extends widgets.DOMWidgetView { if (result == null) { result = true; } - //var and_result = true; for (var cond in obj[op]) { if (cond == 'AND' || cond == 'OR' || cond == 'NOT') { result = result && evaluateRowEditConditions(current_row, {[cond]: obj[op][cond]}); @@ -518,12 +517,6 @@ class QgridView extends widgets.DOMWidgetView { result = true; } result = result && !evaluateRowEditConditions(current_row, {'AND': obj[op]}); - } else if (op == 'NOR') { - if (result == null) { - result = true; - } - result = result && !evaluateRowEditConditions(current_row, {'OR': obj[op]}); - } else { alert("Unsupported operation '" + op + "' found in row edit conditions!") } @@ -741,6 +734,12 @@ class QgridView extends widgets.DOMWidgetView { 'type': 'selection_change' }); }, 100); + } else if (msg.type == 'toggle_editable') { + if (this.slick_grid.getOptions().editable == false) { + this.slick_grid.setOptions({'editable': true}); + } else { + this.slick_grid.setOptions({'editable': false}); + } } else if (msg.col_info) { var filter = this.filters[msg.col_info.name]; filter.handle_msg(msg); diff --git a/qgrid/grid.py b/qgrid/grid.py index e6eb0ef2..ded78da5 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -234,6 +234,9 @@ def show_grid(data_frame, show_toolbar=None, "data_frame must be DataFrame or Series, not %s" % type(data_frame) ) + row_edit_conditions = (row_edit_conditions or {}) + column_definitions = (column_definitions or {}) + # create a visualization for the dataframe return QgridWidget(df=data_frame, precision=precision, grid_options=grid_options, @@ -1091,7 +1094,7 @@ def add_row_internally(self, row): df.loc[index_col_val, col_names[i]] = s self._unfiltered_df.loc[index_col_val, col_names[i]] = s - self._update_table(triggered_by='add_row', scroll_to_row=df.index.get_loc(index_col_val)) + self._update_table(triggered_by='add_row', scroll_to_row=df.index.get_loc(index_col_val), fire_data_change_event=True) self._trigger_df_change_event() def remove_row(self): From 6a950f585936d595f93148d396831989907659d6 Mon Sep 17 00:00:00 2001 From: "Abigail Hahn (Harvey Nash)" Date: Tue, 1 May 2018 14:06:38 -0700 Subject: [PATCH 4/6] Added a method for updating a value in the grid given an index value, column name value and data value --- qgrid/grid.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qgrid/grid.py b/qgrid/grid.py index ded78da5..5b4e9834 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -1097,6 +1097,12 @@ def add_row_internally(self, row): self._update_table(triggered_by='add_row', scroll_to_row=df.index.get_loc(index_col_val), fire_data_change_event=True) self._trigger_df_change_event() + def set_value_internally(self, index, column, value): + self._df.loc[index, column] = value + self._unfiltered_df.loc[index, column] = value + self._update_table(triggered_by='cell_change', fire_data_change_event=True) + self._trigger_df_change_event() + def remove_row(self): """ Remove the current row from the table. From 5c30b5f966b0709c4c2e1b3bc84e65ee80466eb3 Mon Sep 17 00:00:00 2001 From: "Abigail Hahn (Harvey Nash)" Date: Mon, 7 May 2018 13:29:24 -0700 Subject: [PATCH 5/6] Fixed a minor bug in the row edit conditions evaluation code --- js/src/qgrid.widget.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index 56458433..efa1417c 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -517,6 +517,11 @@ class QgridView extends widgets.DOMWidgetView { result = true; } result = result && !evaluateRowEditConditions(current_row, {'AND': obj[op]}); + } else if (op == 'NOR') { + if (result == null) { + result = false; + } + result = result || !evaluateRowEditConditions(current_row, {'OR': obj[op]}); } else { alert("Unsupported operation '" + op + "' found in row edit conditions!") } From 6ca7258487b3769f32077392aa9face12464b002 Mon Sep 17 00:00:00 2001 From: "Abigail Hahn (Harvey Nash)" Date: Mon, 7 May 2018 13:55:13 -0700 Subject: [PATCH 6/6] Added tests --- qgrid/tests/test_grid.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/qgrid/tests/test_grid.py b/qgrid/tests/test_grid.py index 3a73aad7..158b3e3e 100644 --- a/qgrid/tests/test_grid.py +++ b/qgrid/tests/test_grid.py @@ -409,3 +409,33 @@ def test_object_dtype_categorical(): }) assert len(widget._df) == 1 assert widget._df[0][0] == cat_series[0] + + +def test_add_row_internally(): + df = pd.DataFrame({'foo': ['hello'], 'bar': ['world'], 'baz': [42], 'boo': [57]}) + df.set_index('baz', inplace=True, drop=True) + + q = QgridWidget(df=df) + + new_row = [ + ('baz', 43), + ('bar', "new bar"), + ('boo', 58), + ('foo', "new foo") + ] + + q.add_row_internally(new_row) + + assert q._df.loc[43, 'foo'] == 'new foo' + assert q._df.loc[42, 'foo'] == 'hello' + + +def test_set_value_internally(): + df = pd.DataFrame({'foo': ['hello'], 'bar': ['world'], 'baz': [42], 'boo': [57]}) + df.set_index('baz', inplace=True, drop=True) + + q = QgridWidget(df=df) + + q.set_value_internally(42, 'foo', 'hola') + + assert q._df.loc[42, 'foo'] == 'hola'