|
| 1 | +from ipyautoui.custom.editgrid import ( |
| 2 | + EditGrid, |
| 3 | + DataHandler, |
| 4 | +) |
| 5 | +from ipyautoui.custom.edittsv import Changes |
| 6 | +import pathlib |
| 7 | +import traitlets as tr |
| 8 | +import json |
| 9 | +from pydantic import BaseModel, RootModel, Field |
| 10 | +import typing as ty |
| 11 | +import ipywidgets as w |
| 12 | +import random |
| 13 | +import functools |
| 14 | + |
| 15 | + |
| 16 | +# + |
| 17 | +# --- LOAD & SAVE HELPERS --- |
| 18 | +def load_json(fpth: pathlib.Path = pathlib.Path("text.json")) -> list[dict]: |
| 19 | + """Load JSON list data safely.""" |
| 20 | + with fpth.open("r", encoding="utf-8") as f: |
| 21 | + data = json.load(f) |
| 22 | + if not isinstance(data, list): |
| 23 | + raise ValueError("Expected list of dicts in JSON file.") |
| 24 | + return data |
| 25 | + |
| 26 | + |
| 27 | +def save_json(data: list[dict], fpth: pathlib.Path) -> pathlib.Path: |
| 28 | + """Save JSON list data safely.""" |
| 29 | + with fpth.open("w", encoding="utf-8") as f: |
| 30 | + json.dump(data, f, indent=4) |
| 31 | + return fpth |
| 32 | + |
| 33 | + |
| 34 | +# --- DELETE --- |
| 35 | +def delete_rows(primary_keys_list: list[str | int], primary_key_name, fpth: pathlib.Path) -> list[dict]: |
| 36 | + """Delete rows from JSON based on keys in 'primary_keys' (list of strings or ints).""" |
| 37 | + data = load_json(fpth) |
| 38 | + |
| 39 | + # Normalize all keys to strings for consistent comparison |
| 40 | + delete_keys = [str(k) for k in primary_keys_list] |
| 41 | + |
| 42 | + # Keep only rows whose 'primary_key_name' is NOT in delete_ids |
| 43 | + updated_data = [row for row in data if str(row.get(primary_key_name)) not in delete_keys] |
| 44 | + |
| 45 | + save_json(updated_data, fpth) |
| 46 | + print(f"🗑️ Deleted rows with IDs: {delete_keys}") |
| 47 | + return updated_data |
| 48 | + |
| 49 | + |
| 50 | +# --- ADD --- |
| 51 | +def add_rows(additions: list[dict], primary_key_name = "id", fpth: pathlib.Path = pathlib.Path("text.json")) -> list[dict]: |
| 52 | + """Add new rows from 'insert' (dict-of-dicts form).""" |
| 53 | + data = load_json(fpth) |
| 54 | + maxId = max(data, key=lambda x:x[primary_key_name])[primary_key_name] |
| 55 | + |
| 56 | + for addition in additions: |
| 57 | + new_addition = {} |
| 58 | + new_addition[primary_key_name] = maxId + 1 |
| 59 | + addition.pop(primary_key_name, None) |
| 60 | + new_addition.update(addition) |
| 61 | + data.append(new_addition) |
| 62 | + |
| 63 | + save_json(data, fpth) |
| 64 | + print(f"➕ Added {len(additions)} new row(s)") |
| 65 | + return data |
| 66 | + |
| 67 | + |
| 68 | +# --- EDIT --- |
| 69 | +def edit_rows(edits: dict[str | int, dict], primary_key_name, fpth: pathlib.Path) -> list[dict]: |
| 70 | + """Update existing rows based on 'update' section.""" |
| 71 | + data = load_json(fpth) |
| 72 | + |
| 73 | + for key, update_fields_dict in edits.items(): |
| 74 | + for record in data: |
| 75 | + if str(record.get(primary_key_name)) == str(key): |
| 76 | + record.update(update_fields_dict) |
| 77 | + |
| 78 | + save_json(data, fpth) |
| 79 | + print(f"✏️ Updated {len(edits)} row(s)") |
| 80 | + return data |
| 81 | + |
| 82 | + |
| 83 | +# --- COMBINED HANDLER --- |
| 84 | +def handle_crud(primary_key_name, changes: Changes, fpth: pathlib.Path): |
| 85 | + """Apply delete → insert → update in order.""" |
| 86 | + if changes.deletions: |
| 87 | + delete_rows(changes.deletions, primary_key_name, fpth) |
| 88 | + if changes.additions: |
| 89 | + add_rows(changes.additions, primary_key_name, fpth) |
| 90 | + if changes.edits: |
| 91 | + edit_rows(changes.edits, primary_key_name, fpth) |
| 92 | +# --- COMBINED HANDLER --- |
| 93 | + |
| 94 | +def delete_row(value: dict, primary_key_name = "id", fpth: pathlib.Path = pathlib.Path("text.json")) -> list[dict]: |
| 95 | + data = load_json(fpth) |
| 96 | + deleted_id = value[primary_key_name] |
| 97 | + # Keep only rows whose 'primary_key_name' is NOT delete_id |
| 98 | + updated_data = [row for row in data if str(row.get(primary_key_name)) != str(deleted_id)] |
| 99 | + save_json(updated_data, fpth) |
| 100 | + return updated_data |
| 101 | + |
| 102 | +def edit_row(value: dict, primary_key_name = "id", fpth: pathlib.Path = pathlib.Path("text.json")) -> list[dict]: |
| 103 | + data = load_json(fpth) |
| 104 | + row_id = value[primary_key_name] |
| 105 | + |
| 106 | + # Update matching record |
| 107 | + for record in data: |
| 108 | + if str(record.get(primary_key_name)) == str(row_id): |
| 109 | + # update all keys except 'primary_key_name' |
| 110 | + record.update({k: v for k, v in value.items() if k != primary_key_name}) |
| 111 | + break |
| 112 | + |
| 113 | + save_json(data, fpth) |
| 114 | + return data |
| 115 | + |
| 116 | +def add_row(addition: dict, primary_key_name = "id", fpth: pathlib.Path = pathlib.Path("text.json")) -> list[dict]: |
| 117 | + data = load_json(fpth) |
| 118 | + maxId = max(data, key=lambda x:x[primary_key_name])[primary_key_name] |
| 119 | + new_addition = {} |
| 120 | + new_addition[primary_key_name] = maxId + 1 |
| 121 | + addition.pop(primary_key_name, None) |
| 122 | + new_addition.update(addition) |
| 123 | + data.append(new_addition) |
| 124 | + save_json(data, fpth) |
| 125 | + print(f"➕ Added new row") |
| 126 | + return data |
| 127 | + |
| 128 | +class EditGridFile(EditGrid): |
| 129 | + primary_key_name = tr.Unicode(default_value="id") |
| 130 | + fpth = tr.Instance(klass=pathlib.Path, allow_none=False) |
| 131 | + |
| 132 | + @tr.observe("fpth") |
| 133 | + def update_handler(self, change): |
| 134 | + self.datahandler.fn_get_all_data = functools.partial(load_json, fpth=self.fpth) |
| 135 | + self.datahandler.fn_post = functools.partial(add_row, primary_key_name=self.primary_key_name, fpth=self.fpth) |
| 136 | + self.datahandler.fn_patch = functools.partial(edit_row, primary_key_name=self.primary_key_name, fpth=self.fpth) |
| 137 | + self.datahandler.fn_delete = functools.partial(delete_row, primary_key_name=self.primary_key_name, fpth=self.fpth) |
| 138 | + self.datahandler.fn_copy = functools.partial(add_row, primary_key_name=self.primary_key_name, fpth=self.fpth) |
| 139 | + self.datahandler.fn_io = functools.partial(self.handle_crud) |
| 140 | + |
| 141 | + self._set_datahandler(self.datahandler) |
| 142 | + |
| 143 | + def __init__( |
| 144 | + self, |
| 145 | + **kwargs, |
| 146 | + ): |
| 147 | + |
| 148 | + datahandler = DataHandler( |
| 149 | + fn_get_all_data=lambda v: print(v), |
| 150 | + fn_post=lambda v: print(v), |
| 151 | + fn_patch=lambda v: v, |
| 152 | + fn_delete=lambda v: print(v), |
| 153 | + fn_copy=lambda v: print(v), |
| 154 | + fn_io = lambda v: print("io") |
| 155 | + ) |
| 156 | + super().__init__( |
| 157 | + datahandler=datahandler, |
| 158 | + warn_on_delete=True, |
| 159 | + layout=w.Layout(height="800px"), |
| 160 | + **kwargs, |
| 161 | + ) |
| 162 | + |
| 163 | + if hasattr(self, "ui_io") and hasattr(self.ui_io, "primary_key_name"): |
| 164 | + self.ui_io.primary_key_name = self.primary_key_name |
| 165 | + |
| 166 | + self.update_handler("") |
| 167 | + |
| 168 | + def set_value_from_tsv(self, value): |
| 169 | + self.value=value |
| 170 | + if self.ui_io is not None: |
| 171 | + self.datahandler.fn_io(self.ui_io.changes) |
| 172 | + |
| 173 | + # --- HANDLERS --- |
| 174 | + def handle_crud(self, changes: Changes): |
| 175 | + handle_crud(self.ui_io.primary_key_name, changes, self.fpth) |
| 176 | + |
| 177 | + |
| 178 | +if __name__ == "__main__": |
| 179 | + json_path = pathlib.Path("../../..") / "tests" / "test_data" / "edit-grid-file-data.json" |
| 180 | + with json_path.open('r', encoding='utf-8') as file: |
| 181 | + data = json.load(file) |
| 182 | + |
| 183 | + # Test: EditGrid instance with multi-indexing. |
| 184 | + AUTO_GRID_DEFAULT_VALUE = data |
| 185 | + |
| 186 | + class DataFrameCols(BaseModel): |
| 187 | + id: int = Field(1, json_schema_extra=dict(column_width=80, section="a")) |
| 188 | + string: str = Field( |
| 189 | + "string", json_schema_extra=dict(column_width=400, section="a") |
| 190 | + ) |
| 191 | + integer: int = Field(1, json_schema_extra=dict(column_width=80, section="a")) |
| 192 | + floater: float = Field( |
| 193 | + None, json_schema_extra=dict(column_width=70, section="b") |
| 194 | + ) |
| 195 | + |
| 196 | + class TestDataFrame(RootModel): |
| 197 | + """a description of TestDataFrame""" |
| 198 | + |
| 199 | + root: ty.List[DataFrameCols] = Field( |
| 200 | + default=AUTO_GRID_DEFAULT_VALUE, |
| 201 | + json_schema_extra=dict( |
| 202 | + format="dataframe", datagrid_index_name=("section", "title") |
| 203 | + ), |
| 204 | + ) |
| 205 | + |
| 206 | + title = "Testing Crud Info Grid" |
| 207 | + description = "Useful for all editing purposes whatever they may be 👍" |
| 208 | + edit_grid_file = EditGridFile( |
| 209 | + schema=TestDataFrame, |
| 210 | + title=title, |
| 211 | + description=description, |
| 212 | + ui_add=None, |
| 213 | + ui_edit=None, |
| 214 | + show_copy_dialogue=False, |
| 215 | + close_crud_dialogue_on_action=False, |
| 216 | + global_decimal_places=1, |
| 217 | + column_width={"String": 400}, |
| 218 | + fpth=json_path |
| 219 | + ) |
| 220 | + display(edit_grid_file) |
| 221 | + |
| 222 | + |
| 223 | + |
| 224 | + |
0 commit comments