Skip to content

Updated ToPrint wfa to work with esign#113

Open
chris-adam wants to merge 1 commit intomasterfrom
PARAF-403/to_print_wfa
Open

Updated ToPrint wfa to work with esign#113
chris-adam wants to merge 1 commit intomasterfrom
PARAF-403/to_print_wfa

Conversation

@chris-adam
Copy link
Copy Markdown
Contributor

@chris-adam chris-adam commented Apr 8, 2026

J'ai branché cette PR sur la branche PARAF-402 parce que la nouvelle migration a été écrite là-bas...

Voir commentaire sur https://my.support.imio.be/browse/PARAF-403

Summary by CodeRabbit

  • Bug Fixes

    • Fixed workflow guard behavior so signing/printing actions respect approvals and e-sign settings.
    • Corrected approval flow to reliably reindex and update approval status.
  • Refactor

    • Approval now auto-resolves the next post-approval transition based on the workflow state.
    • Workflow transition wiring improved to keep approve/print/sign flows consistent.
  • Tests

    • Extended workflow and approval tests to cover new signing/printing scenarios.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Approve/signing workflow logic was changed: approvals now decide post-approval transition via a new helper; guards were renamed/added; workflow definitions, migrations, wfadaptations, call sites, registry steps, and tests were updated to align with the new behavior.

Changes

Cohort / File(s) Summary
Approval API & Utilities
imio/dms/mail/adapters.py, imio/dms/mail/utils.py
OMApprovalAdapter.approve_file signature default transition=False; approval now resolves post-approval transition via new get_post_approval_transition(context) and calls do_transitions(...) with that target. portal_catalog lookup was reordered. New utility get_post_approval_transition added.
Workflow Guard Conditions
imio/dms/mail/dmsmail.py
Renamed/exported guard can_be_handsignedcan_set_to_print with updated checks; added new public can_be_signed guard using catalog lookup (b_size=1) and approval-completion checks.
Workflow Definition & Migration
imio/dms/mail/profiles/default/workflows/.../definition.xml, imio/dms/mail/migrations/migrate_to_3_1_2.py
Updated guard expressions in workflow XML from can_be_handsigned()can_be_signed(). Migration adds guard-expression updates for existing workflows and conditional updates when wfadaptations applied (maps can_be_handsigned()can_be_signed() or can_set_to_print() as appropriate).
Workflow Adaptations
imio/dms/mail/wfadaptations.py
Refined OMToApprove/OMToPrint wiring: adjusted where back_to_approve/set_to_print transitions are added, skipped sent in certain loops, updated actbox guard expressions to can_set_to_print(), and moved volatile cache invalidation earlier in OMToPrint adaptation.
Approval Call Sites
imio/dms/mail/browser/iconified_category.py, imio/dms/mail/browser/views.py
Calls to approve_file() changed to pass transition=True (delegating transition resolution to adapter's new logic) instead of explicit "propose_to_be_signed".
Field Registration / Steps
imio/dms/mail/steps.py
activate_esigning now inserts ISigningBehavior.signers and ISigningBehavior.esign in separate steps, writing registry after each insertion to ensure esign is always added when missing.
Tests
imio/dms/mail/tests/test_dmsmail.py, imio/dms/mail/tests/test_wfadaptations.py
Renamed can_be_handsigned test → can_be_signed; added can_set_to_print test. Refactored wfadaptation tests into a shared base, added to_approve adaptation tests, used real import steps for applying adaptations, and added approval-driven flows (using approve_file(..., transition=True)).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    rect rgba(200,220,255,0.5)
    participant User
    end
    rect rgba(220,255,200,0.5)
    participant Adapter as OMApprovalAdapter
    participant Utils as Utils (get_post_approval_transition)
    participant WF as portal_workflow
    participant Catalog as portal_catalog
    end

    User->>Adapter: approve_file(afile, userid, ..., transition=True)
    Adapter->>Adapter: update_signers / approval state
    Adapter->>Utils: get_post_approval_transition(context)
    Utils->>WF: getTransitionsFor(context)
    WF-->>Utils: transitions list
    Utils-->>Adapter: selected_transition ("set_to_print" or "propose_to_be_signed")
    Adapter->>WF: do_transitions(context, [selected_transition])
    Adapter->>Catalog: reindexObject(context)
    Adapter-->>User: return approval result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • sgeulette

Poem

🐰 I nibbled lines of guard and flow,
Hopped through transitions soft and slow,
From handsigned name to printed gate,
Approvals pick their post-approve fate,
A little hop — the workflow grows.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Updated ToPrint wfa to work with esign' accurately describes the main focus of the changeset, which updates the ToPrint workflow adaptation to work with e-signing functionality across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch PARAF-403/to_print_wfa

Comment @coderabbitai help to get the list of available commands and usage tips.

@chris-adam chris-adam force-pushed the PARAF-403/to_print_wfa branch 4 times, most recently from feea82a to 679fd50 Compare April 8, 2026 12:13
@chris-adam chris-adam marked this pull request as ready for review April 8, 2026 12:22
@chris-adam chris-adam force-pushed the PARAF-403/to_print_wfa branch from 679fd50 to 0a0def5 Compare April 8, 2026 12:30
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
imio/dms/mail/dmsmail.py (1)

915-923: ⚠️ Potential issue | 🟠 Major

Don't gate the stock propose_to_be_signed transition on completed approvals.

outgoingmail_workflow now routes propose_to_be_signed through this helper, but the default workflow still has no to_approve state. OMApprovalAdapter.start_approval_process() only starts from to_approve, so once has_approvings() becomes true there is no path that can ever make has_approvings(all_done=True) flip to True; the mail is stuck in created. Please make this approval check conditional on the approval adaptation/state being present.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/dmsmail.py` around lines 915 - 923, The guard in can_be_signed
currently blocks the stock propose_to_be_signed transition by requiring
approvals to be completed; change it to only perform the approvals check when
the approval adaptation/state exists so default workflows without approvals
aren’t stuck. Concretely, in can_be_signed wrap the approval logic with a
presence check (e.g. if getattr(self.context, "has_approvings", None) is not
None and self.context.has_approvings()) before calling
has_approvings(all_done=True), or alternatively check for the approval
adapter/state (OMApprovalAdapter or a "to_approve" state) before evaluating
has_approvings(all_done=True); keep the existing catalog and brains checks
intact and only return False for incomplete approvals when that approval
capability is present.
🧹 Nitpick comments (1)
imio/dms/mail/tests/test_dmsmail.py (1)

568-580: Please cover the new approval gate and can_set_to_print() path.

This still only checks file presence. The risky behavior change in this PR is the has_approvings(all_done=True) branch inside can_be_signed() plus the new can_set_to_print() guard used by the to-print adaptation, and neither branch is asserted here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/tests/test_dmsmail.py` around lines 568 - 580, Extend the
test_ImioDmsOutgoingMailWfConditionsAdapter_can_be_signed test to also exercise
the approval-gate branch and the to-print guard: create and transition approving
workflow items so that adapted.has_approvings(all_done=True) becomes True and
assert adapted.can_be_signed() both when approvals are incomplete and when
all_done=True; additionally instantiate the to-print adaptation (or call the
can_set_to_print() path on the same adapter) and assert its behavior before and
after conditions that should allow printing. Target the
ImioDmsOutgoingMailWfConditionsAdapter instance (adapted), its can_be_signed()
method, the has_approvings(...) branch, and the can_set_to_print() guard when
adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@imio/dms/mail/adapters.py`:
- Around line 1308-1316: The state "to_print" is currently classified as "before
approval" by is_state_before_approve/is_state_before_or_approve but still
appears in the "after approval" buckets, causing overlap; update
is_state_after_approve and is_state_after_or_approve to exclude "to_print" from
their state checks (or otherwise align their allowed-state tuples with the
before-approve helpers) so "to_print" is only considered before approval; adjust
the tuple/list of states used in is_state_after_approve and
is_state_after_or_approve to remove "to_print" and keep consistency with
is_state_before_approve/is_state_before_or_approve.

In `@imio/dms/mail/migrations/migrate_to_3_1_2.py`:
- Around line 89-109: The migration currently overwrites any existing guards for
transitions "set_to_print" and "back_to_print" unconditionally; change the logic
in the block using applied_wfa_names / to_print_applied so that before calling
tr.setProperties(...) you check the existing guard expression and only replace
it when it equals the legacy expression (the old can_be_handsigned() check used
in the earlier loop). Locate the portal_workflow lookup (wf =
api.portal.get().portal_workflow["outgoingmail_workflow"]), inspect
tr.guard_expr or the equivalent property on the transition object, compare it to
the legacy "python:object.wf_conditions().can_be_handsigned()" string, and only
call tr.setProperties(...) with new_guard and guard_permissions when that
comparison matches; otherwise skip to preserve customized guards.

In `@imio/dms/mail/wfadaptations.py`:
- Around line 1306-1311: The transitions list in imio.dms.mail.wfadaptations
currently keeps "propose_to_be_signed" even when OMToApproveAdaptation is
applied, allowing a direct jump from to_print to to_be_signed and bypassing
approval; update the logic where transitions is built (the block using
get_applied_adaptations and the applied_wfa check) so that when
u"imio.dms.mail.wfadaptations.OMToApproveAdaptation" is present you remove or
replace "propose_to_be_signed" (e.g., remove it and ensure "propose_to_approve"
is present) to prevent the direct to_print→to_be_signed edge, and apply the same
change in the reverse-apply path that patches existing to_print states so the
edge is also removed when OMToApproveAdaptation is applied.

---

Outside diff comments:
In `@imio/dms/mail/dmsmail.py`:
- Around line 915-923: The guard in can_be_signed currently blocks the stock
propose_to_be_signed transition by requiring approvals to be completed; change
it to only perform the approvals check when the approval adaptation/state exists
so default workflows without approvals aren’t stuck. Concretely, in
can_be_signed wrap the approval logic with a presence check (e.g. if
getattr(self.context, "has_approvings", None) is not None and
self.context.has_approvings()) before calling has_approvings(all_done=True), or
alternatively check for the approval adapter/state (OMApprovalAdapter or a
"to_approve" state) before evaluating has_approvings(all_done=True); keep the
existing catalog and brains checks intact and only return False for incomplete
approvals when that approval capability is present.

---

Nitpick comments:
In `@imio/dms/mail/tests/test_dmsmail.py`:
- Around line 568-580: Extend the
test_ImioDmsOutgoingMailWfConditionsAdapter_can_be_signed test to also exercise
the approval-gate branch and the to-print guard: create and transition approving
workflow items so that adapted.has_approvings(all_done=True) becomes True and
assert adapted.can_be_signed() both when approvals are incomplete and when
all_done=True; additionally instantiate the to-print adaptation (or call the
can_set_to_print() path on the same adapter) and assert its behavior before and
after conditions that should allow printing. Target the
ImioDmsOutgoingMailWfConditionsAdapter instance (adapted), its can_be_signed()
method, the has_approvings(...) branch, and the can_set_to_print() guard when
adding these assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26e57cf3-e371-4180-acdf-5c01eee3cfbb

📥 Commits

Reviewing files that changed from the base of the PR and between 21d20e4 and 679fd50.

📒 Files selected for processing (6)
  • imio/dms/mail/adapters.py
  • imio/dms/mail/dmsmail.py
  • imio/dms/mail/migrations/migrate_to_3_1_2.py
  • imio/dms/mail/profiles/default/workflows/outgoingmail_workflow/definition.xml
  • imio/dms/mail/tests/test_dmsmail.py
  • imio/dms/mail/wfadaptations.py

Comment thread imio/dms/mail/adapters.py Outdated
Comment thread imio/dms/mail/migrations/migrate_to_3_1_2.py Outdated
Comment thread imio/dms/mail/wfadaptations.py Outdated
@chris-adam chris-adam force-pushed the PARAF-403/to_print_wfa branch from 0a0def5 to 31afdf0 Compare April 8, 2026 12:47
@chris-adam chris-adam requested a review from sgeulette April 8, 2026 13:03
@chris-adam chris-adam marked this pull request as draft April 9, 2026 08:50
@chris-adam chris-adam force-pushed the PARAF-403/to_print_wfa branch 3 times, most recently from 568d426 to 0075559 Compare April 10, 2026 07:12
Base automatically changed from PARAF-402/ff_table to master April 11, 2026 20:56
@chris-adam chris-adam force-pushed the PARAF-403/to_print_wfa branch 9 times, most recently from b95608a to 47452a2 Compare April 13, 2026 13:47
)
self.common_tests()

def test_OMToPrintAdaptationAfterNp1(self):
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je ne comprends pas bien à quoi servent ces deux tests. Même avant mes changements, ce deux tests appliquent la même wfa deux fois, alors que leurs noms suggèrent l'adaptation N+1

@chris-adam chris-adam marked this pull request as ready for review April 13, 2026 13:53
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
imio/dms/mail/steps.py (1)

137-149: ⚠️ Potential issue | 🟠 Major

Persist omail_fields when only signers is inserted.

Line 148 only writes the registry back inside the ISigningBehavior.esign branch. If a site already has ISigningBehavior.esign but is still missing ISigningBehavior.signers, Line 139 updates the local list and the step exits without saving it, so rerunning activate_esigning() leaves the form config inconsistent.

Proposed fix
     omf = api.portal.get_registry_record(
         "imio.dms.mail.browser.settings.IImioDmsMailConfig.omail_fields", default=[]
     )
     om_fns = [dic["field_name"] for dic in omf]
+    updated = False
     if "ISigningBehavior.signers" not in om_fns:
         pos = om_fns.index("internal_reference_no")
         omf.insert(pos + 1,
                    {"field_name": "ISigningBehavior.signers", "read_tal_condition": u"", "write_tal_condition": u""})
         om_fns = [dic["field_name"] for dic in omf]
+        updated = True
     if "ISigningBehavior.esign" not in om_fns:
         pos = om_fns.index("ISigningBehavior.signers")
         omf.insert(pos + 1,
                    {"field_name": "ISigningBehavior.esign", "read_tal_condition": u"", "write_tal_condition": u""})
+        updated = True
+    if updated:
         api.portal.set_registry_record("imio.dms.mail.browser.settings.IImioDmsMailConfig.omail_fields",
                                        omf)
         log.append("Updated omail_fields registry record")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/steps.py` around lines 137 - 149, The code adds
"ISigningBehavior.signers" into the local omf list but only persists changes to
the registry when "ISigningBehavior.esign" is also inserted; modify the logic in
activate_esigning (or the block manipulating omf/om_fns) so that after inserting
the "ISigningBehavior.signers" entry you call
api.portal.set_registry_record("imio.dms.mail.browser.settings.IImioDmsMailConfig.omail_fields",
omf) to persist the updated omf; keep using the same omf/om_fns variables and
the existing insertion position logic (pos =
om_fns.index("internal_reference_no")) and ensure you still insert and recompute
om_fns before persisting.
imio/dms/mail/migrations/migrate_to_3_1_2.py (1)

31-60: ⚠️ Potential issue | 🟠 Major

The substitute migration can lose entries on reordered or repeated rules.

Line 41 only searches existing_rules[idx + 1:], so if the undated base rule is earlier in the list you'll create a substitute with absent_signer=None. On top of that, Line 31 deduplicates only on (absent_signer, substitute_signer), which drops later date windows for the same pair. Both cases silently lose substitute data during upgrade.

Proposed fix
-                existing_pairs = {(s["absent_signer"], s["substitute_signer"]) for s in substitutes}
+                existing_pairs = {
+                    (
+                        s["absent_signer"],
+                        s["substitute_signer"],
+                        s.get("valid_from"),
+                        s.get("valid_until"),
+                    )
+                    for s in substitutes
+                }
@@
-                        for candidate in existing_rules[idx + 1:]:
+                        for c_idx, candidate in enumerate(existing_rules):
+                            if c_idx == idx:
+                                continue
                             if (not candidate.get("valid_from") and not candidate.get("valid_until")
                                     and candidate["signer"] != rule["signer"]
                                     and candidate["number"] == rule["number"]
                                     and candidate["treating_groups"] == rule["treating_groups"]
                                     and candidate["mail_types"] == rule["mail_types"]
                                     and candidate["send_modes"] == rule["send_modes"]
                                     and candidate["tal_condition"] == rule["tal_condition"]):
                                 absent_signer = candidate["signer"]
                                 break
-                        pair = (absent_signer, rule["signer"])
+                        if absent_signer is None:
+                            logger.warning(
+                                "Could not resolve absent signer for dated signer rule %r; skipping substitute migration",
+                                rule,
+                            )
+                            cleaned_rules.append(rule)
+                            continue
+                        pair = (absent_signer, rule["signer"], valid_from, valid_until)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/migrations/migrate_to_3_1_2.py` around lines 31 - 60, The
migration currently only looks for undated bases after the dated rule
(existing_rules[idx + 1:]) and deduplicates substitutes only by (absent_signer,
substitute_signer), which causes missing substitutes when the undated base is
earlier or when the same pair has multiple date windows; update the search over
existing_rules to scan the whole list (exclude the same entry) and pick the best
candidate (e.g., the nearest by index or first match) instead of only later
entries, ensure you don't create a substitute with absent_signer=None (skip if
no candidate found), and change existing_pairs to include the date bounds when
deduping (use a key like (absent_signer, substitute_signer, valid_from,
valid_until) rather than just (absent_signer, substitute_signer)) so multiple
dated windows for the same pair are preserved (refer to variables/functions:
existing_rules, substitutes, idx, candidate, pair, existing_pairs,
new_substitutes_count).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@imio/dms/mail/utils.py`:
- Around line 1527-1537: The transition lookup in get_post_approval_transition
uses pw.getTransitionsFor(context) which is permission-filtered and can differ
from the later execution under adopt_roles(["Reviewer"]); wrap the
getTransitionsFor call in the same elevation so the decision matches execution
(use api.user.adopt_roles(["Reviewer"]) around the pw.getTransitionsFor(context)
call in get_post_approval_transition) to ensure set_to_print is chosen when it
will be executable rather than falling back to propose_to_be_signed.

---

Outside diff comments:
In `@imio/dms/mail/migrations/migrate_to_3_1_2.py`:
- Around line 31-60: The migration currently only looks for undated bases after
the dated rule (existing_rules[idx + 1:]) and deduplicates substitutes only by
(absent_signer, substitute_signer), which causes missing substitutes when the
undated base is earlier or when the same pair has multiple date windows; update
the search over existing_rules to scan the whole list (exclude the same entry)
and pick the best candidate (e.g., the nearest by index or first match) instead
of only later entries, ensure you don't create a substitute with
absent_signer=None (skip if no candidate found), and change existing_pairs to
include the date bounds when deduping (use a key like (absent_signer,
substitute_signer, valid_from, valid_until) rather than just (absent_signer,
substitute_signer)) so multiple dated windows for the same pair are preserved
(refer to variables/functions: existing_rules, substitutes, idx, candidate,
pair, existing_pairs, new_substitutes_count).

In `@imio/dms/mail/steps.py`:
- Around line 137-149: The code adds "ISigningBehavior.signers" into the local
omf list but only persists changes to the registry when "ISigningBehavior.esign"
is also inserted; modify the logic in activate_esigning (or the block
manipulating omf/om_fns) so that after inserting the "ISigningBehavior.signers"
entry you call
api.portal.set_registry_record("imio.dms.mail.browser.settings.IImioDmsMailConfig.omail_fields",
omf) to persist the updated omf; keep using the same omf/om_fns variables and
the existing insertion position logic (pos =
om_fns.index("internal_reference_no")) and ensure you still insert and recompute
om_fns before persisting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31721781-813f-440f-b1cf-6f9ca25e6a83

📥 Commits

Reviewing files that changed from the base of the PR and between 679fd50 and 47452a2.

📒 Files selected for processing (11)
  • imio/dms/mail/adapters.py
  • imio/dms/mail/browser/iconified_category.py
  • imio/dms/mail/browser/views.py
  • imio/dms/mail/dmsmail.py
  • imio/dms/mail/migrations/migrate_to_3_1_2.py
  • imio/dms/mail/profiles/default/workflows/outgoingmail_workflow/definition.xml
  • imio/dms/mail/steps.py
  • imio/dms/mail/tests/test_dmsmail.py
  • imio/dms/mail/tests/test_wfadaptations.py
  • imio/dms/mail/utils.py
  • imio/dms/mail/wfadaptations.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • imio/dms/mail/tests/test_dmsmail.py
  • imio/dms/mail/wfadaptations.py

Comment thread imio/dms/mail/utils.py
@chris-adam chris-adam force-pushed the PARAF-403/to_print_wfa branch from 47452a2 to a5c0ff1 Compare April 13, 2026 14:08
@chris-adam
Copy link
Copy Markdown
Contributor Author

@sgeulette Voilà, c'est prêt pour review :)

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
imio/dms/mail/migrations/migrate_to_3_1_2.py (2)

27-29: ⚠️ Potential issue | 🟠 Major

valid_until-only legacy rules are treated as already migrated.

This fast-path only checks for valid_from, but the conversion below handles valid_from or valid_until. An instance that only stored an end date will skip the migration and keep pre-3.1.2 signer rules.

🛠️ Suggested fix
-            if existing_rules and not any("valid_from" in rule for rule in existing_rules):
+            if existing_rules and not any(
+                ("valid_from" in rule) or ("valid_until" in rule)
+                for rule in existing_rules
+            ):
                 logger.info("omail_signer_rules already migrated, skipping")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/migrations/migrate_to_3_1_2.py` around lines 27 - 29, The
fast-path mistakenly treats rules with only a valid_until as already migrated
because it only checks for "valid_from"; update the condition that uses
existing_rules (the line with not any("valid_from" in rule for rule in
existing_rules)) to require neither "valid_from" nor "valid_until" is present
(e.g., not any(("valid_from" in r) or ("valid_until" in r) for r in
existing_rules)) so rules that have only an end date will not skip the
migration, and keep the logger.info("omail_signer_rules already migrated,
skipping") behavior for true fully-migrated cases.

34-58: ⚠️ Potential issue | 🟠 Major

Don't create substitute entries without an absent signer.

This matching logic only scans later rules and still appends a substitute when no matching undated signer is found. On reordered or partially cleaned registries, the upgrade drops the dated rule from omail_signer_rules and writes absent_signer=None.

🛠️ Suggested fix
                 for idx, rule in enumerate(existing_rules):
-                    rule = dict(rule)
+                    original_rule = dict(rule)
+                    rule = dict(original_rule)
                     valid_from = rule.pop("valid_from", None)
                     valid_until = rule.pop("valid_until", None)
                     if valid_from or valid_until:
                         # Find the first undated rule after this one with same conditions
                         absent_signer = None
-                        for candidate in existing_rules[idx + 1:]:
+                        for candidate in existing_rules:
                             if (not candidate.get("valid_from") and not candidate.get("valid_until")
                                     and candidate["signer"] != rule["signer"]
                                     and candidate["number"] == rule["number"]
                                     and candidate["treating_groups"] == rule["treating_groups"]
                                     and candidate["mail_types"] == rule["mail_types"]
                                     and candidate["send_modes"] == rule["send_modes"]
                                     and candidate["tal_condition"] == rule["tal_condition"]):
                                 absent_signer = candidate["signer"]
                                 break
+                        if absent_signer is None:
+                            cleaned_rules.append(original_rule)
+                            logger.warning(
+                                "Could not infer absent signer for dated rule {}".format(original_rule)
+                            )
+                            continue
                         pair = (absent_signer, rule["signer"])
                         if pair not in existing_pairs:
                             substitutes.append({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/migrations/migrate_to_3_1_2.py` around lines 34 - 58, The
migration loop over existing_rules can append a substitute with
absent_signer=None when no undated candidate is found; update the logic in the
block that builds the substitute (inside the for idx, rule in
enumerate(existing_rules) loop) so you only create/append a substitute dict and
add to substitutes/existing_pairs if absent_signer is not None (i.e., after the
inner candidate scan check that a matching undated signer was actually found);
reference variables: existing_rules, candidate, absent_signer, substitutes,
existing_pairs, and the pair tuple to gate the append.
imio/dms/mail/wfadaptations.py (1)

1015-1025: ⚠️ Potential issue | 🟠 Major

to_approve still exposes the direct sign transition.

imio/dms/mail/dmsmail.py:921-932 shows can_be_signed() only checks file presence and whether approvals are all done. Because these branches only add set_to_print and never remove propose_to_be_signed from to_approve, the last approver can still skip the to_print step in both apply orders.

🛠️ Suggested fix
-        transitions = ["propose_to_be_signed", "back_to_creation"]
+        transitions = ["back_to_creation"]
         # what is already applied ?
         already_applied = ""
         applied_wfa = [dic["adaptation"] for dic in get_applied_adaptations()]
         if u"imio.dms.mail.wfadaptations.OMServiceValidation" in applied_wfa:
             already_applied = "n_plus"
             transitions.append("back_to_n_plus_1")
             transitions.append("back_to_validated")
         if u"imio.dms.mail.wfadaptations.OMToPrintAdaptation" in applied_wfa:
             already_applied = "to_print"
             transitions.append("set_to_print")
+        else:
+            transitions.insert(0, "propose_to_be_signed")
         if u"imio.dms.mail.wfadaptations.OMToApproveAdaptation" in applied_wfa:
             transitions.append("back_to_approve")
             # Add "set_to_print" from to_approve
             ta_state = wf.states["to_approve"]
-            ta_transitions = list(ta_state.transitions)
+            ta_transitions = [tr for tr in ta_state.transitions if tr != "propose_to_be_signed"]
             if to_tr_id not in ta_transitions:
                 ta_transitions.append(to_tr_id)
             ta_state.transitions = tuple(ta_transitions)

Also applies to: 1332-1339

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/wfadaptations.py` around lines 1015 - 1025, The transitions
list currently always contains "propose_to_be_signed" so the final approver can
skip the to_print flow; update the logic in the block that builds transitions
(using get_applied_adaptations(), applied_wfa and the transitions variable) so
that when u"imio.dms.mail.wfadaptations.OMToPrintAdaptation" is present you
remove or do not include "propose_to_be_signed" (and likewise ensure
OMServiceValidation handling doesn't leave the direct sign transition if the
to_print path is required); modify the two places mentioned (the block around
transitions = ["propose_to_be_signed", "back_to_creation"] and the similar block
at 1332-1339) to conditionally omit "propose_to_be_signed" when
OMToPrintAdaptation is applied and keep adding only the appropriate back_to_*
and set_to_print transitions.
♻️ Duplicate comments (1)
imio/dms/mail/utils.py (1)

1533-1535: ⚠️ Potential issue | 🟠 Major

Pick the post-approval branch under Reviewer role.

portal_workflow.getTransitionsFor() is permission-filtered. From a caller that does not currently have Reviewer rights, this helper can miss set_to_print and fall back to propose_to_be_signed, so the next state depends on the caller instead of the workflow.

🔧 Suggested fix
 def get_post_approval_transition(context):
     """Return the transition to trigger when all approvals are done.
 
     When OMToPrintAdaptation is applied (handsigned flow), the mail goes to
     to_print after approval instead of directly to to_be_signed.
     """
     pw = api.portal.get().portal_workflow
-    trs = [tr['id'] for tr in pw.getTransitionsFor(context)]
+    with api.env.adopt_roles(["Reviewer"]):
+        trs = [tr["id"] for tr in pw.getTransitionsFor(context)]
     if "set_to_print" in trs:
         return "set_to_print"
     return "propose_to_be_signed"

Run this to confirm the helper is still resolved outside the elevated role path and inspect the surrounding approval flow:

#!/bin/bash
set -e
rg -n -C4 --type py '\bget_post_approval_transition\s*\(' imio/dms/mail
rg -n -C12 --type py 'def approve_file|adopt_roles|get_post_approval_transition|api\.content\.transition|doActionFor' imio/dms/mail/adapters.py

Expected result: get_post_approval_transition() is evaluated before the code path that elevates to Reviewer for the actual workflow transition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/utils.py` around lines 1533 - 1535, The code uses
portal_workflow.getTransitionsFor(context) (pw variable) which is
permission-filtered and may not show "set_to_print" for callers without Reviewer
rights, causing get_post_approval_transition behavior to depend on caller
permissions; change the logic to resolve the post-approval transition outside
the limited-caller path by invoking the existing helper
get_post_approval_transition (or evaluate transitions under an elevated Reviewer
role) before any role elevation or state-changing branch, ensuring you check for
"set_to_print" vs "propose_to_be_signed" using an unfiltered transition list so
the decision does not depend on the current caller's permissions.
🧹 Nitpick comments (4)
imio/dms/mail/tests/test_wfadaptations.py (4)

416-428: Consider using direct attribute access and next() for cleaner code.

Static analysis flagged minor style improvements:

  • Line 417: Use fti.localroles instead of getattr(fti, "localroles").
  • Line 427: Use next(...) instead of list comprehension with [0] slice.
♻️ Proposed refactor
-        fti = getUtility(IDexterityFTI, name="dmsoutgoingmail")
-        lr = getattr(fti, "localroles")
+        fti = getUtility(IDexterityFTI, name="dmsoutgoingmail")
+        lr = fti.localroles
-        self.assertIn(
-            "to_approve",
-            [dic["v"] for dic in folder["om_treating"].query if dic["i"] == "review_state"][0],
-        )
+        review_state_values = next(dic["v"] for dic in folder["om_treating"].query if dic["i"] == "review_state")
+        self.assertIn("to_approve", review_state_values)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/tests/test_wfadaptations.py` around lines 416 - 428, Replace
the getattr(fti, "localroles") call with direct attribute access fti.localroles
and simplify the final assertion by using next(...) to find the first dict in
folder["om_treating"].query where dic["i"] == "review_state" and then assert
"to_approve" is in that dict's "v" list (use next((dic for dic in
folder["om_treating"].query if dic["i"] == "review_state"), None) to get the
dict and handle it directly).

341-359: Duplicate tests: test_OMToPrintAdaptationBeforeNp1 and test_OMToPrintAdaptationAfterNp1 are identical.

Both tests apply the same imiodmsmail-om_to_print_wfadaptation profile twice and call self.common_tests(). The names suggest different scenarios (before/after N+1) but the implementations are identical. Consider removing one or clarifying their distinct purposes if they should test different scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/tests/test_wfadaptations.py` around lines 341 - 359, The two
test methods test_OMToPrintAdaptationBeforeNp1 and
test_OMToPrintAdaptationAfterNp1 are identical; remove or differentiate them so
they test distinct scenarios: either delete one of the duplicated methods
(keeping the other calling self.common_tests()) or modify one to apply the
appropriate different profile or setup for the "before"/"after N+1" scenario
(e.g., change the second runImportStepFromProfile call or preceding setup in
test_OMToPrintAdaptationAfterNp1) so the methods exercise different states while
still invoking self.common_tests(); ensure the unique profile id
"imiodmsmail-om_to_print_wfadaptation" remains correct for the intended
scenario.

493-511: Duplicate tests: test_OMToApproveAdaptationBeforeNp1 and test_OMToApproveAdaptationAfterNp1 are identical.

Same issue as in TestOMToPrintAdaptation - both methods apply imiodmsmail-om_to_approve_wfadaptation twice and call self.common_tests(). Consider consolidating into a single idempotency test or differentiating the scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/tests/test_wfadaptations.py` around lines 493 - 511, These two
tests are duplicates: test_OMToApproveAdaptationBeforeNp1 and
test_OMToApproveAdaptationAfterNp1 both run the same import step twice and call
self.common_tests(); consolidate by keeping a single idempotency test (e.g.,
test_OMToApproveAdaptation_idempotent) that runs
imiodmsmail-om_to_approve_wfadaptation twice and calls self.common_tests(), or
if you intended to test a different scenario for "AfterNp1" change the second
test to run the distinct adaptation or setup that reflects the "AfterNp1"
condition and/or assert the differing expected state instead of duplicating the
same steps.

485-486: Consider using next() instead of slice.

Same pattern as line 427 - static analysis suggests using next() over single-element slice.

♻️ Proposed refactor
-        res = [dic["v"] for dic in folder["om_treating"].query if dic["i"] == "review_state"][0]
-        self.assertIn("to_approve", res)
+        res = next(dic["v"] for dic in folder["om_treating"].query if dic["i"] == "review_state")
+        self.assertIn("to_approve", res)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@imio/dms/mail/tests/test_wfadaptations.py` around lines 485 - 486, Replace
the single-element slice pattern that retrieves the first matching dict from
folder["om_treating"].query with a next() call: iterate/filter
folder["om_treating"].query for dic["i"] == "review_state" and use next(...) to
get the first matching element (with either a default or letting it raise if
absent) before accessing ["v"]; update the line that assigns res (and the
similar occurrence near line 427) to use next() instead of [0] to avoid creating
a temporary list and to be more idiomatic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@imio/dms/mail/migrations/migrate_to_3_1_2.py`:
- Around line 27-29: The fast-path mistakenly treats rules with only a
valid_until as already migrated because it only checks for "valid_from"; update
the condition that uses existing_rules (the line with not any("valid_from" in
rule for rule in existing_rules)) to require neither "valid_from" nor
"valid_until" is present (e.g., not any(("valid_from" in r) or ("valid_until" in
r) for r in existing_rules)) so rules that have only an end date will not skip
the migration, and keep the logger.info("omail_signer_rules already migrated,
skipping") behavior for true fully-migrated cases.
- Around line 34-58: The migration loop over existing_rules can append a
substitute with absent_signer=None when no undated candidate is found; update
the logic in the block that builds the substitute (inside the for idx, rule in
enumerate(existing_rules) loop) so you only create/append a substitute dict and
add to substitutes/existing_pairs if absent_signer is not None (i.e., after the
inner candidate scan check that a matching undated signer was actually found);
reference variables: existing_rules, candidate, absent_signer, substitutes,
existing_pairs, and the pair tuple to gate the append.

In `@imio/dms/mail/wfadaptations.py`:
- Around line 1015-1025: The transitions list currently always contains
"propose_to_be_signed" so the final approver can skip the to_print flow; update
the logic in the block that builds transitions (using get_applied_adaptations(),
applied_wfa and the transitions variable) so that when
u"imio.dms.mail.wfadaptations.OMToPrintAdaptation" is present you remove or do
not include "propose_to_be_signed" (and likewise ensure OMServiceValidation
handling doesn't leave the direct sign transition if the to_print path is
required); modify the two places mentioned (the block around transitions =
["propose_to_be_signed", "back_to_creation"] and the similar block at 1332-1339)
to conditionally omit "propose_to_be_signed" when OMToPrintAdaptation is applied
and keep adding only the appropriate back_to_* and set_to_print transitions.

---

Duplicate comments:
In `@imio/dms/mail/utils.py`:
- Around line 1533-1535: The code uses
portal_workflow.getTransitionsFor(context) (pw variable) which is
permission-filtered and may not show "set_to_print" for callers without Reviewer
rights, causing get_post_approval_transition behavior to depend on caller
permissions; change the logic to resolve the post-approval transition outside
the limited-caller path by invoking the existing helper
get_post_approval_transition (or evaluate transitions under an elevated Reviewer
role) before any role elevation or state-changing branch, ensuring you check for
"set_to_print" vs "propose_to_be_signed" using an unfiltered transition list so
the decision does not depend on the current caller's permissions.

---

Nitpick comments:
In `@imio/dms/mail/tests/test_wfadaptations.py`:
- Around line 416-428: Replace the getattr(fti, "localroles") call with direct
attribute access fti.localroles and simplify the final assertion by using
next(...) to find the first dict in folder["om_treating"].query where dic["i"]
== "review_state" and then assert "to_approve" is in that dict's "v" list (use
next((dic for dic in folder["om_treating"].query if dic["i"] == "review_state"),
None) to get the dict and handle it directly).
- Around line 341-359: The two test methods test_OMToPrintAdaptationBeforeNp1
and test_OMToPrintAdaptationAfterNp1 are identical; remove or differentiate them
so they test distinct scenarios: either delete one of the duplicated methods
(keeping the other calling self.common_tests()) or modify one to apply the
appropriate different profile or setup for the "before"/"after N+1" scenario
(e.g., change the second runImportStepFromProfile call or preceding setup in
test_OMToPrintAdaptationAfterNp1) so the methods exercise different states while
still invoking self.common_tests(); ensure the unique profile id
"imiodmsmail-om_to_print_wfadaptation" remains correct for the intended
scenario.
- Around line 493-511: These two tests are duplicates:
test_OMToApproveAdaptationBeforeNp1 and test_OMToApproveAdaptationAfterNp1 both
run the same import step twice and call self.common_tests(); consolidate by
keeping a single idempotency test (e.g., test_OMToApproveAdaptation_idempotent)
that runs imiodmsmail-om_to_approve_wfadaptation twice and calls
self.common_tests(), or if you intended to test a different scenario for
"AfterNp1" change the second test to run the distinct adaptation or setup that
reflects the "AfterNp1" condition and/or assert the differing expected state
instead of duplicating the same steps.
- Around line 485-486: Replace the single-element slice pattern that retrieves
the first matching dict from folder["om_treating"].query with a next() call:
iterate/filter folder["om_treating"].query for dic["i"] == "review_state" and
use next(...) to get the first matching element (with either a default or
letting it raise if absent) before accessing ["v"]; update the line that assigns
res (and the similar occurrence near line 427) to use next() instead of [0] to
avoid creating a temporary list and to be more idiomatic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fdb00368-2d65-424b-97dc-7c839b0b18ef

📥 Commits

Reviewing files that changed from the base of the PR and between 47452a2 and a5c0ff1.

📒 Files selected for processing (11)
  • imio/dms/mail/adapters.py
  • imio/dms/mail/browser/iconified_category.py
  • imio/dms/mail/browser/views.py
  • imio/dms/mail/dmsmail.py
  • imio/dms/mail/migrations/migrate_to_3_1_2.py
  • imio/dms/mail/profiles/default/workflows/outgoingmail_workflow/definition.xml
  • imio/dms/mail/steps.py
  • imio/dms/mail/tests/test_dmsmail.py
  • imio/dms/mail/tests/test_wfadaptations.py
  • imio/dms/mail/utils.py
  • imio/dms/mail/wfadaptations.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • imio/dms/mail/browser/iconified_category.py
  • imio/dms/mail/tests/test_dmsmail.py
  • imio/dms/mail/adapters.py
  • imio/dms/mail/dmsmail.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant