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'},
]);
},