Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions source/app/blueprints/pages/manage/manage_case_templates_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
]
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ <h4>Field types</h4>
<li>summary: content to prefill the summary.</li>
<li>tags: A list of case tags.</li>
<li>tasks: A list of dictionaries defining tasks. Tasks are defined by title (required), description, and list of tags.</li>
<li>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</li>
<li>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 <code>note_directories</code> list to create sub-directory trees of arbitrary depth.</li>
</ul>
</div>
</div>
Expand Down Expand Up @@ -102,6 +102,17 @@ <h4>Field types</h4>
"title": "Identify the compromised accounts",
"content": "# Observations\n\n"
}
],
"note_directories": [
{
"title": "Sub-investigation",
"notes": [
{
"title": "Detailed findings",
"content": "# Findings\n\n"
}
]
}
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ <h4>Upload case template</h4>
"title": "Identify the compromised accounts",
"content": "# Observations\n\n"
}
],
"note_directories": [
{
"title": "Sub-investigation",
"notes": [
{
"title": "Detailed findings",
"content": "# Findings\n\n"
}
]
}
]
},
{
Expand Down Expand Up @@ -93,7 +104,7 @@ <h4>Field types</h4>
<li>summary: content to prefill the summary.</li>
<li>tags: A list of case tags.</li>
<li>tasks: A list of dictionaries defining tasks. Tasks are defined by title (required), description, and list of tags.</li>
<li>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</li>
<li>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 <code>note_directories</code> list to create sub-directory trees of arbitrary depth.</li>
</ul>
</div>
</div>
Expand Down
108 changes: 73 additions & 35 deletions source/app/datamgmt/manage/manage_case_templates_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions ui/src/pages/manage.case.templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
]);
},
Expand Down Expand Up @@ -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'},
]);
},
Expand Down