From bc6524b0b4971370d94b841f04f0012e17bdf96a Mon Sep 17 00:00:00 2001 From: Philipp L Date: Fri, 5 Jun 2026 10:42:41 +0200 Subject: [PATCH] [IMP] Support nested note_directories in case templates Case template note directories can now contain nested note_directories to create sub-directory trees of arbitrary depth. A directory can hold notes, sub-directories, both, or neither - only title is required. --- .../manage/manage_case_templates_routes.py | 11 ++ .../manage/templates/modal_case_template.html | 13 ++- .../templates/modal_upload_case_template.html | 13 ++- .../manage/manage_case_templates_db.py | 108 ++++++++++++------ ui/src/pages/manage.case.templates.js | 8 +- 5 files changed, 112 insertions(+), 41 deletions(-) diff --git a/source/app/blueprints/pages/manage/manage_case_templates_routes.py b/source/app/blueprints/pages/manage/manage_case_templates_routes.py index 12af3d3d1..2a35e4aee 100644 --- a/source/app/blueprints/pages/manage/manage_case_templates_routes.py +++ b/source/app/blueprints/pages/manage/manage_case_templates_routes.py @@ -116,6 +116,17 @@ def add_template_modal(caseid, url_redir): "title": "Note 1", "content": "Note 1 content" } + ], + "note_directories": [ + { + "title": "Sub directory 1", + "notes": [ + { + "title": "Sub note 1", + "content": "Sub note 1 content" + } + ] + } ] } ] diff --git a/source/app/blueprints/pages/manage/templates/modal_case_template.html b/source/app/blueprints/pages/manage/templates/modal_case_template.html index 65321530c..7f7a1cf34 100644 --- a/source/app/blueprints/pages/manage/templates/modal_case_template.html +++ b/source/app/blueprints/pages/manage/templates/modal_case_template.html @@ -50,7 +50,7 @@

Field types

  • summary: content to prefill the summary.
  • tags: A list of case tags.
  • tasks: A list of dictionaries defining tasks. Tasks are defined by title (required), description, and list of tags.
  • -
  • note_directories: A list of dictionaries defining note directories. Note directories are defined by title (required), and list of notes. Notes have title (required) and content
  • +
  • note_directories: A list of dictionaries defining note directories. Each directory is defined by title (required), an optional list of notes (each with title (required) and optional content), and an optional nested note_directories list to create sub-directory trees of arbitrary depth.
  • @@ -102,6 +102,17 @@

    Field types

    "title": "Identify the compromised accounts", "content": "# Observations\n\n" } + ], + "note_directories": [ + { + "title": "Sub-investigation", + "notes": [ + { + "title": "Detailed findings", + "content": "# Findings\n\n" + } + ] + } ] }, { diff --git a/source/app/blueprints/pages/manage/templates/modal_upload_case_template.html b/source/app/blueprints/pages/manage/templates/modal_upload_case_template.html index 7b5e9cd1b..9886d0084 100644 --- a/source/app/blueprints/pages/manage/templates/modal_upload_case_template.html +++ b/source/app/blueprints/pages/manage/templates/modal_upload_case_template.html @@ -64,6 +64,17 @@

    Upload case template

    "title": "Identify the compromised accounts", "content": "# Observations\n\n" } + ], + "note_directories": [ + { + "title": "Sub-investigation", + "notes": [ + { + "title": "Detailed findings", + "content": "# Findings\n\n" + } + ] + } ] }, { @@ -93,7 +104,7 @@

    Field types

  • summary: content to prefill the summary.
  • tags: A list of case tags.
  • tasks: A list of dictionaries defining tasks. Tasks are defined by title (required), description, and list of tags.
  • -
  • note_groups: A list of dictionaries defining note groups. Note groups are defined by title (required), and list of notes. Notes have title (required) and content
  • +
  • note_directories: A list of dictionaries defining note directories. Each directory is defined by title (required), an optional list of notes (each with title (required) and optional content), and an optional nested note_directories list to create sub-directory trees of arbitrary depth.
  • diff --git a/source/app/datamgmt/manage/manage_case_templates_db.py b/source/app/datamgmt/manage/manage_case_templates_db.py index db58aedbd..6bccf90fe 100644 --- a/source/app/datamgmt/manage/manage_case_templates_db.py +++ b/source/app/datamgmt/manage/manage_case_templates_db.py @@ -82,6 +82,37 @@ def delete_case_template_by_id(case_template_id: int): CaseTemplate.query.filter_by(id=case_template_id).delete() +def _validate_note_dir_entry(note_dir: dict) -> Optional[str]: + """Recursively validate a note directory entry from a case template. + + Args: + note_dir (dict): The note directory entry to validate. + + Returns: + Optional[str]: An error message if validation fails, or None if successful. + """ + if not isinstance(note_dir, dict): + return "Each note directory must be a dictionary." + if "title" not in note_dir: + return "Each note directory must have a 'title' field." + if "notes" in note_dir: + if not isinstance(note_dir["notes"], list): + return "Notes must be a list." + for note in note_dir["notes"]: + if not isinstance(note, dict): + return "Each note must be a dictionary." + if "title" not in note: + return "Each note must have a 'title' field." + if "note_directories" in note_dir: + if not isinstance(note_dir["note_directories"], list): + return "Nested note_directories must be a list." + for sub_dir in note_dir["note_directories"]: + error = _validate_note_dir_entry(sub_dir) + if error: + return error + return None + + def validate_case_template(data: dict, update: bool = False) -> Optional[str]: try: if not update: @@ -139,18 +170,9 @@ def validate_case_template(data: dict, update: bool = False) -> Optional[str]: if not isinstance(data["note_directories"], list): return "Note directories must be a list." for note_dir in data["note_directories"]: - if not isinstance(note_dir, dict): - return "Each note directory must be a dictionary." - if "title" not in note_dir: - return "Each note directory must have a 'title' field." - if "notes" in note_dir: - if not isinstance(note_dir["notes"], list): - return "Notes must be a list." - for note in note_dir["notes"]: - if not isinstance(note, dict): - return "Each note must be a dictionary." - if "title" not in note: - return "Each note must have a 'title' field." + error = _validate_note_dir_entry(note_dir) + if error: + return error # If all checks succeeded, we return None to indicate everything is has been validated return None @@ -243,36 +265,52 @@ def case_template_populate_notes(case: Cases, note_dir_template: dict, ng: NoteD return logs -def case_template_populate_note_groups(case: Cases, case_template: CaseTemplate): +def _create_note_directory_recursive(case: Cases, note_dir_template: dict, parent_id: Optional[int]) -> list: + """Recursively create a note directory and its children from a template entry. + + Args: + case (Cases): The target case. + note_dir_template (dict): The template entry for this directory. + parent_id (Optional[int]): The DB id of the parent directory, or None for root. + + Returns: + list: Any error messages encountered during creation. + """ logs = [] - # Update case tasks - if case_template.note_directories: - case_template.note_directories = case_template.note_directories + try: + note_dir_schema = CaseNoteDirectorySchema() - for note_dir_template in case_template.note_directories: - try: - # validate before saving - note_dir_schema = CaseNoteDirectorySchema() + mapped_note_dir_template = { + "name": note_dir_template['title'], + "parent_id": parent_id, + "case_id": case.case_id + } - # Remap case task template fields - # Set status to "To Do" which is ID 1 - mapped_note_dir_template = { - "name": note_dir_template['title'], - "parent_id": None, - "case_id": case.case_id - } + note_dir = note_dir_schema.load(mapped_note_dir_template) + db_create(note_dir) - note_dir = note_dir_schema.load(mapped_note_dir_template) - db_create(note_dir) + if not note_dir: + logs.append("Unable to add note directory for internal reasons") + return logs - if not note_dir: - logs.append("Unable to add note group for internal reasons") - break + logs += case_template_populate_notes(case, note_dir_template, note_dir) - logs = case_template_populate_notes(case, note_dir_template, note_dir) + for sub_dir_template in note_dir_template.get("note_directories", []): + logs += _create_note_directory_recursive(case, sub_dir_template, note_dir.id) - except marshmallow.exceptions.ValidationError as e: - logs.append(e.messages) + except marshmallow.exceptions.ValidationError as e: + logs.append(e.messages) + + return logs + + +def case_template_populate_note_groups(case: Cases, case_template: CaseTemplate): + logs = [] + if case_template.note_directories: + case_template.note_directories = case_template.note_directories + + for note_dir_template in case_template.note_directories: + logs += _create_note_directory_recursive(case, note_dir_template, None) return logs diff --git a/ui/src/pages/manage.case.templates.js b/ui/src/pages/manage.case.templates.js index 438eabfad..2286d2d66 100644 --- a/ui/src/pages/manage.case.templates.js +++ b/ui/src/pages/manage.case.templates.js @@ -34,8 +34,8 @@ function add_case_template() { {value: 'summary', score: 1, meta: 'summary of the case'}, {value: 'tags', score: 1, meta: 'tags of the case or the tasks'}, {value: 'tasks', score: 1, meta: 'tasks of the case'}, - {value: 'note_groups', score: 1, meta: 'groups of notes'}, - {value: 'title', score: 1, meta: 'title of the task or the note group or the note'}, + {value: 'note_directories', score: 1, meta: 'note directories (supports nested note_directories)'}, + {value: 'title', score: 1, meta: 'title of the task, note directory, or note'}, {value: 'content', score: 1, meta: 'content of the note'}, ]); }, @@ -198,8 +198,8 @@ function case_template_detail(ctempl_id) { {value: 'summary', score: 1, meta: 'summary of the case'}, {value: 'tags', score: 1, meta: 'tags of the case or the tasks'}, {value: 'tasks', score: 1, meta: 'tasks of the case'}, - {value: 'note_groups', score: 1, meta: 'groups of notes'}, - {value: 'title', score: 1, meta: 'title of the task or the note group or the note'}, + {value: 'note_directories', score: 1, meta: 'note directories (supports nested note_directories)'}, + {value: 'title', score: 1, meta: 'title of the task, note directory, or note'}, {value: 'content', score: 1, meta: 'content of the note'}, ]); },