reverted how we compile the ignore list. The longer numbers will actually handle fine so we dont need to ignore. For example 01230 we need to handle but 01230TS we dont. Also we created a separate script to handle clashing numbers specifically as well as helper function that will dump control id's

This commit is contained in:
2026-07-09 20:33:07 -05:00
parent b5862a7ea7
commit 1b300eacb3
4 changed files with 118 additions and 27 deletions

View File

@@ -20,6 +20,7 @@ Extracts and organizes documents from FileCabinet CS (Thomson Reuters) using GUI
| `fccs_export.py` | Step 2: Automate FCCS GUI to export all drawers | | `fccs_export.py` | Step 2: Automate FCCS GUI to export all drawers |
| `fccs_reorganize.py` | Step 3: Parse filenames and rebuild folder structure | | `fccs_reorganize.py` | Step 3: Parse filenames and rebuild folder structure |
| `fccs_verify.py` | Step 4 (optional): Compare manifests against exported files | | `fccs_verify.py` | Step 4 (optional): Compare manifests against exported files |
| `fccs_dump_controls.py` | Utility: Dump control identifiers of an on-screen dialog |
## Setup Per Engagement ## Setup Per Engagement
@@ -36,12 +37,12 @@ python fccs_scan.py
Reads the restored FCCS backup directory and writes all drawer IDs (subfolder names) to `drawer_ids.txt`. It also: Reads the restored FCCS backup directory and writes all drawer IDs (subfolder names) to `drawer_ids.txt`. It also:
- **Flags prefix clashes** — if one drawer ID is a prefix of another (e.g. `02218` and `02218A`), searching them in FCCS can pop up a selection box that breaks automated navigation. These clashes are reported, and **every drawer in each clash group** is auto-seeded into `ignore.txt` for review. - **Flags prefix clashes** — if one drawer ID is a prefix of another (e.g. `02218` and `02218A`), searching the **base** ID in FCCS pops up a selection box that breaks plain automated navigation. These clashes are reported, and the base (shorter) IDs are auto-seeded into `ignore.txt`. The longer, more-specific IDs (`02218A`) search fine and export normally; the base IDs are handled by the separate clash-export script.
- **Reports ignored drawers** — any IDs listed in `ignore.txt` that exist in this backup are shown as ones the export will skip. - **Reports ignored drawers** — any IDs listed in `ignore.txt` that exist in this backup are shown as ones the export will skip.
**Ignoring drawers:** The scan creates `ignore.txt` (at `ignore_file`, default `C:\Migration\ignore.txt`) if it doesn't exist and pre-fills it with every drawer in each clash group — searching those in FCCS can show a selection box that stalls the automation, so they're skipped by default. Open the file and: **Ignoring drawers:** The scan creates `ignore.txt` (at `ignore_file`, default `C:\Migration\ignore.txt`) if it doesn't exist and pre-fills it with the clash base IDs — searching those in FCCS shows a selection box that stalls the plain export, so they're skipped by the main export. Open the file and:
- **Delete or comment out** any clashing drawer you actually want exported (then handle it manually in FCCS). - **Delete or comment out** any clash base you'd rather handle fully by hand.
- **Add** any other drawers to skip (e.g. password-protected folders), one ID per line. - **Add** any other drawers to skip (e.g. password-protected folders), one ID per line.
Re-running the scan never overwrites your edits — it only appends newly-discovered clashes. `drawer_ids.txt` stays a full inventory; the export skips anything active in the ignore list. Lines starting with `#` are comments. Re-running the scan never overwrites your edits — it only appends newly-discovered clashes. `drawer_ids.txt` stays a full inventory; the export skips anything active in the ignore list. Lines starting with `#` are comments.

95
fccs_dump_controls.py Normal file
View File

@@ -0,0 +1,95 @@
"""
Utility: Dump control identifiers of on-screen windows.
Use this to discover the control identifiers of a dialog we don't have mapped
yet — e.g. the drawer-selection box that FCCS shows when you search a base ID
that clashes with a longer one (02218 -> 02218A).
USAGE
-----
1. In FCCS, get the target screen on-screen (e.g. type the clashing base ID and
click Go so the selection box is showing).
2. From another cmd window (with the venv active), run:
python fccs_dump_controls.py
This lists every visible top-level window and writes the full control
identifiers of each to control_dump.txt.
To narrow it down, filter by title or class:
python fccs_dump_controls.py --title Select
python fccs_dump_controls.py --class #32770
3. Open control_dump.txt, find the selection box, and copy its identifiers
into the references folder (like references/send_to_file_dialog.txt).
Notes:
- Run from 32-bit Python (same as the rest of the tool).
- Running this from a separate cmd window means the FCCS dialog stays open;
this script does not need focus.
"""
import argparse
import sys
from pywinauto import Desktop
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--title", default=None,
help="only dump windows whose title contains this text")
ap.add_argument("--class", dest="cls", default=None,
help="only dump windows with this exact class name")
ap.add_argument("--out", default="control_dump.txt",
help="output file (default: control_dump.txt)")
args = ap.parse_args()
targets = []
for w in Desktop(backend="win32").windows():
try:
if not w.is_visible():
continue
title = w.window_text()
cls = w.class_name()
except Exception:
continue
if args.title and args.title.lower() not in title.lower():
continue
if args.cls and args.cls != cls:
continue
targets.append(w)
print(f"Found {len(targets)} matching visible window(s):")
for w in targets:
print(f" handle={w.handle} class={w.class_name()!r} "
f"title={w.window_text()!r}")
if not targets:
print("Nothing to dump. Is the target window visible? "
"Try without filters, or adjust --title/--class.")
return
with open(args.out, "w", encoding="utf-8") as f:
old_stdout = sys.stdout
sys.stdout = f
try:
for w in targets:
print("=" * 72)
print(f"WINDOW handle={w.handle} class={w.class_name()!r} "
f"title={w.window_text()!r}")
print("=" * 72)
try:
w.print_control_identifiers()
except Exception as e:
print(f" (could not dump this window: {e})")
print()
finally:
sys.stdout = old_stdout
print(f"\nControl identifiers written to: {args.out}")
if __name__ == "__main__":
main()

View File

@@ -19,45 +19,40 @@ _IGNORE_HEADER = [
"#", "#",
"# Add password-protected or otherwise-excluded drawers here.", "# Add password-protected or otherwise-excluded drawers here.",
"#", "#",
"# The IDs auto-added below are prefix clashes: searching any of them in", "# The IDs auto-added below are prefix clashes: searching the base ID in",
"# FCCS can pop a selection box that breaks the automation, so ALL drawers", "# FCCS pops a selection box that breaks the plain export. Only the base",
"# in each clash group are skipped by default. DELETE or comment out any", "# (shorter) ID is listed — the longer, more-specific IDs export normally.",
"# you actually DO want exported (then handle them manually), keep the rest.", "# The base IDs are exported separately by fccs_export_clashes.py, which",
"# handles the selection box. DELETE or comment out any you'd rather skip.",
"", "",
] ]
def update_ignore_file(ignore_file, clashes, log): def update_ignore_file(ignore_file, clashes, log):
"""Create ignore.txt if missing and seed it with clash-group IDs. """Create ignore.txt if missing and seed it with clash base (prefix) IDs.
Every drawer involved in a clash (both the prefix and each longer ID that Only the shorter (prefix) ID of each clash is added — that's the one that
matches it) is added, since any of them can trip the FCCS selection box. triggers the FCCS selection box and needs special handling. The longer,
Never overwrites existing entries — only appends IDs not already present, more-specific IDs (e.g. '02218A') search fine and export normally.
and de-dupes so no ID is written twice. Returns the list of IDs added. Never overwrites existing entries — only appends prefixes not already
present. Returns the list of IDs added.
""" """
existing = set(load_lines(ignore_file)) existing = set(load_lines(ignore_file))
file_exists = os.path.exists(ignore_file) file_exists = os.path.exists(ignore_file)
seen = set(existing) # never re-add anything already listed to_add = [(short, matches) for short, matches in clashes
blocks = [] # (comment, [new_ids]) to append, in clash order if short not in existing]
for short, matches in clashes:
group = [short] + list(matches)
new_ids = [g for g in group if g not in seen]
if not new_ids:
continue
seen.update(new_ids)
blocks.append((f"# clash group: {', '.join(group)}", new_ids))
# Existing file already covers every clash — leave it untouched. # Existing file already covers every clash — leave it untouched.
if file_exists and not blocks: if file_exists and not to_add:
return [] return []
lines = [] lines = []
if not file_exists: if not file_exists:
lines.extend(_IGNORE_HEADER) lines.extend(_IGNORE_HEADER)
for comment, new_ids in blocks: for short, matches in to_add:
lines.append(comment) lines.append(f"# clashes with: {', '.join(matches)}")
lines.extend(new_ids) lines.append(short)
mode = "a" if file_exists else "w" mode = "a" if file_exists else "w"
with open(ignore_file, mode, encoding="utf-8") as f: with open(ignore_file, mode, encoding="utf-8") as f:
@@ -65,11 +60,11 @@ def update_ignore_file(ignore_file, clashes, log):
f.write("\n") # separate the new block from prior content f.write("\n") # separate the new block from prior content
f.write("\n".join(lines) + "\n") f.write("\n".join(lines) + "\n")
added = [i for _, ids in blocks for i in ids] added = [short for short, _ in to_add]
if not file_exists: if not file_exists:
log(f"Created ignore file: {ignore_file}") log(f"Created ignore file: {ignore_file}")
if added: if added:
log(f"Added {len(added)} clash-group ID(s) to ignore list: " log(f"Added {len(added)} clash base ID(s) to ignore list: "
f"{', '.join(added)}") f"{', '.join(added)}")
return added return added