| 10 |
|
|
| 11 |
|
""" |
| 12 |
|
|
| 13 |
– |
|
| 13 |
|
import csv |
| 14 |
|
import xml.etree.ElementTree as ET |
| 15 |
< |
from typing import Any, Dict, List |
| 15 |
> |
from typing import Any, Dict, List, Optional |
| 16 |
> |
|
| 17 |
> |
|
| 18 |
> |
# ------------------------------------------------------------ |
| 19 |
> |
# Helper: load choice map (artifactID -> choice name) |
| 20 |
> |
# ------------------------------------------------------------ |
| 21 |
> |
|
| 22 |
> |
def load_choice_map( |
| 23 |
> |
choice_csv_path: str, |
| 24 |
> |
artifact_id_col: str = "ArtifactID", |
| 25 |
> |
name_col: str = "Name", |
| 26 |
> |
) -> Dict[str, str]: |
| 27 |
> |
""" |
| 28 |
> |
Load a CSV containing workspace choices into a dict: |
| 29 |
> |
{ artifact_id (str) -> choice_name (str) } |
| 30 |
> |
|
| 31 |
> |
By default expects columns named: |
| 32 |
> |
ArtifactID, Name |
| 33 |
> |
|
| 34 |
> |
If your file uses different headers, adjust artifact_id_col / name_col. |
| 35 |
> |
""" |
| 36 |
> |
choice_map: Dict[str, str] = {} |
| 37 |
> |
|
| 38 |
> |
with open(choice_csv_path, newline="", encoding="utf-8-sig") as f: |
| 39 |
> |
reader = csv.DictReader(f) |
| 40 |
> |
for row in reader: |
| 41 |
> |
artifact_id = (row.get(artifact_id_col) or "").strip() |
| 42 |
> |
name = (row.get(name_col) or "").strip() |
| 43 |
> |
if artifact_id and name: |
| 44 |
> |
choice_map[artifact_id] = name |
| 45 |
|
|
| 46 |
+ |
return choice_map |
| 47 |
+ |
|
| 48 |
+ |
|
| 49 |
+ |
# ------------------------------------------------------------ |
| 50 |
+ |
# Main flattener |
| 51 |
+ |
# ------------------------------------------------------------ |
| 52 |
|
|
| 53 |
|
class RelativityAuditFlattenerXml: |
| 54 |
|
""" |
| 56 |
|
each <field> modification becomes its own row, with the original |
| 57 |
|
columns (Name through Request Origination) repeated. |
| 58 |
|
|
| 59 |
< |
XML schema (from your inspector): |
| 60 |
< |
- Root: <auditElement> |
| 27 |
< |
- Repeated per-field nodes: <field id="..." type="..." name="..." formatstring="..."> |
| 28 |
< |
- Inside <field>: |
| 29 |
< |
<setChoice>...</setChoice> |
| 30 |
< |
<unSetChoice>...</unSetChoice> |
| 31 |
< |
<unsetChoice>...</unsetChoice> |
| 32 |
< |
<oldValue>...</oldValue> |
| 33 |
< |
<newValue>...</newValue> |
| 59 |
> |
For choice fields, setChoice/unSetChoice/unsetChoice values are |
| 60 |
> |
artifactIDs; we map them to choice names using a choice_map dict. |
| 61 |
|
""" |
| 62 |
|
|
| 63 |
|
# The base columns we want to carry through from the CSV |
| 77 |
|
DETAIL_COLUMNS = [ |
| 78 |
|
"Field Name", # from <field name="..."> |
| 79 |
|
"Field Type", # from <field type="..."> |
| 80 |
< |
"unSetChoice", # combined <unSetChoice> and <unsetChoice> |
| 81 |
< |
"setChoice", # <setChoice> |
| 82 |
< |
"oldValue", # <oldValue> |
| 83 |
< |
"newValue", # <newValue> |
| 80 |
> |
"unSetChoice", # choice names removed |
| 81 |
> |
"setChoice", # choice names added |
| 82 |
> |
"oldValue", # free-text oldValue |
| 83 |
> |
"newValue", # free-text newValue |
| 84 |
|
] |
| 85 |
|
|
| 86 |
< |
def __init__(self, csv_path: str) -> None: |
| 86 |
> |
def __init__(self, csv_path: str, choice_map: Optional[Dict[str, str]] = None) -> None: |
| 87 |
|
self.csv_path = csv_path |
| 88 |
|
self.rows: List[Dict[str, Any]] = [] |
| 89 |
+ |
self.choice_map: Dict[str, str] = choice_map or {} |
| 90 |
|
|
| 91 |
< |
# ------------------------------------------------------------ |
| 91 |
> |
# -------------------------------------------------------- |
| 92 |
|
# Public API |
| 93 |
< |
# ------------------------------------------------------------ |
| 93 |
> |
# -------------------------------------------------------- |
| 94 |
|
|
| 95 |
|
def load_and_flatten(self) -> None: |
| 96 |
|
""" |
| 182 |
|
wb.Close(SaveChanges=True) |
| 183 |
|
xlApp.Quit() |
| 184 |
|
|
| 185 |
< |
# ------------------------------------------------------------ |
| 185 |
> |
# -------------------------------------------------------- |
| 186 |
|
# Internal helpers |
| 187 |
< |
# ------------------------------------------------------------ |
| 187 |
> |
# -------------------------------------------------------- |
| 188 |
|
|
| 189 |
|
def _copy_base_columns(self, src_row: Dict[str, Any]) -> Dict[str, Any]: |
| 190 |
|
""" |
| 195 |
|
out[col] = src_row.get(col, "") |
| 196 |
|
return out |
| 197 |
|
|
| 198 |
+ |
def _map_choice_id_to_name(self, choice_id: str) -> str: |
| 199 |
+ |
""" |
| 200 |
+ |
Convert a choice artifactID to its choice name using self.choice_map. |
| 201 |
+ |
If not found, fall back to the original ID string. |
| 202 |
+ |
""" |
| 203 |
+ |
choice_id = (choice_id or "").strip() |
| 204 |
+ |
if not choice_id: |
| 205 |
+ |
return "" |
| 206 |
+ |
return self.choice_map.get(choice_id, choice_id) |
| 207 |
+ |
|
| 208 |
|
def _parse_details_xml(self, xml_str: str) -> List[Dict[str, str]]: |
| 209 |
|
""" |
| 210 |
|
Parse the XML in Details and return a list of dicts with keys: |
| 211 |
|
- field_name |
| 212 |
|
- field_type |
| 213 |
< |
- unset_choice (combined unSetChoice/unsetChoice) |
| 214 |
< |
- set_choice |
| 213 |
> |
- unset_choice (names, combined) |
| 214 |
> |
- set_choice (names, combined) |
| 215 |
|
- old_value |
| 216 |
|
- new_value |
| 217 |
|
""" |
| 233 |
|
field_name = attrib.get("name", "") |
| 234 |
|
field_type = attrib.get("type", "") |
| 235 |
|
|
| 236 |
< |
# Collect choice changes |
| 237 |
< |
set_choices = [ |
| 236 |
> |
# Collect choice changes as IDs |
| 237 |
> |
set_choice_ids = [ |
| 238 |
|
(child.text or "").strip() |
| 239 |
|
for child in field_elem.findall("setChoice") |
| 240 |
|
if (child.text or "").strip() |
| 241 |
|
] |
| 242 |
|
|
| 243 |
< |
# Note: both <unSetChoice> and <unsetChoice> appear in your data |
| 206 |
< |
unset_choices = [ |
| 243 |
> |
unset_choice_ids = [ |
| 244 |
|
(child.text or "").strip() |
| 245 |
|
for child in field_elem.findall("unSetChoice") |
| 246 |
|
if (child.text or "").strip() |
| 247 |
|
] |
| 248 |
< |
unset_choices += [ |
| 248 |
> |
unset_choice_ids += [ |
| 249 |
|
(child.text or "").strip() |
| 250 |
|
for child in field_elem.findall("unsetChoice") |
| 251 |
|
if (child.text or "").strip() |
| 252 |
|
] |
| 253 |
|
|
| 254 |
< |
unset_choice_str = "; ".join(unset_choices) if unset_choices else "" |
| 255 |
< |
set_choice_str = "; ".join(set_choices) if set_choices else "" |
| 254 |
> |
# Map IDs -> names |
| 255 |
> |
set_choice_names = [self._map_choice_id_to_name(cid) for cid in set_choice_ids] |
| 256 |
> |
unset_choice_names = [self._map_choice_id_to_name(cid) for cid in unset_choice_ids] |
| 257 |
> |
|
| 258 |
> |
set_choice_str = "; ".join(name for name in set_choice_names if name) |
| 259 |
> |
unset_choice_str = "; ".join(name for name in unset_choice_names if name) |
| 260 |
|
|
| 261 |
|
# Gather old/new value tags (for non-choice cases) |
| 262 |
|
old_val_node = field_elem.find("oldValue") |
| 279 |
|
|
| 280 |
|
|
| 281 |
|
# ------------------------------------------------------------ |
| 282 |
< |
# Example usage |
| 282 |
> |
# Script entrypoint |
| 283 |
|
# ------------------------------------------------------------ |
| 284 |
|
|
| 285 |
|
if __name__ == "__main__": |
| 286 |
< |
input_csv = r"C:\path\to\RelativityAuditReport.csv" |
| 286 |
> |
# TODO: update these three paths for your environment |
| 287 |
> |
audit_csv_path = r"C:\path\to\RelativityAuditReport.csv" |
| 288 |
> |
choice_csv_path = r"C:\path\to\AllWorkspaceChoices.csv" |
| 289 |
|
output_excel = r"C:\path\to\RelativityAuditReport_Flattened_XML.xlsx" |
| 290 |
|
|
| 291 |
< |
flattener = RelativityAuditFlattenerXml(input_csv) |
| 292 |
< |
flattener.load_and_flatten() |
| 291 |
> |
# 1) Load the choice map (artifactID -> choice name) |
| 292 |
> |
choice_map = load_choice_map(choice_csv_path) |
| 293 |
> |
print(f"Loaded {len(choice_map)} choices from {choice_csv_path}") |
| 294 |
|
|
| 295 |
+ |
# 2) Flatten the audit CSV, using the choice map |
| 296 |
+ |
flattener = RelativityAuditFlattenerXml(audit_csv_path, choice_map=choice_map) |
| 297 |
+ |
flattener.load_and_flatten() |
| 298 |
|
print(f"Total field-level changes: {flattener.total_field_changes()}") |
| 299 |
|
|
| 300 |
+ |
# 3) Export to Excel |
| 301 |
|
flattener.export_to_excel(output_excel) |
| 302 |
|
print(f"Excel report written to: {output_excel}") |
| 255 |
– |
|