feat(#10903): sub contact attachment routing - #10923
Conversation
16849ee to
75a7787
Compare
|
This is ready for review. |
jkuester
left a comment
There was a problem hiding this comment.
@benkags once again, I am so glad you are working through this! ✨ I spent hours this afternoon reading through and debugging around this code. I have not finished reading it all yet, but I ran out of time today and wanted to push the thoughts that I have so for. 👍
|
@jkuester please take another look |
There was a problem hiding this comment.
Okay, I have been deep down the rabbit hole here. 😬 🐇
All of this is so complicated and interlocked that I ended up just creating benkags#10 with my suggested changes instead of trying to put everything in comments here. 😓 The main focus of my changes was getting all the attachments for the main report and sub-reports to be rendered as expected when viewing the report in the "Reports" tab.
I used file_uploads.xlsx to test with.
A couple things I learned in all my testing and debugging:
Current attachment naming strategies (before this PR):
- New-style
filetype attachements (>=4.9.0):- Property value set to attachment name
- Attachment name has
user-file-prefix + property value
- Old-style
filetype attachments (<4.9.0):- Property value set to attachment name
- Attachment name has
user-file/prefex + path/to/property
- All
binarytype attachments:- Property value set to
"" - Attachment name has
user-file/prefex + path/to/property
- Property value set to
#1 and #2 are what you got when using any of the image/audio/file/etc types in your xform and the user selects a file from the device to upload.
#3 should mainly be coming from the external Android app integration where you are getting base64 data loaded as the value for a field in the form with instance:: binary set to true. (Technically can also enter base64 text right in the form or load it as a default value.) This data does not go through the Enekto file manager.
Editing docs with attachments
Currently, (before this PR) you can edit docs with file attachments. The attachments with type=file are rendered properly in the form, but the type=binary attachments are not rendered. Depending on how we plan to load profile pictures into a contact form, this may or may not be something we need to worry about....
The rendering of the type=binary attachments is not fixed (even by my changes in the linked PR). I was up way too late last night trying to sort out a solution (see the first commit in my PR for what I came up with). I think there is something viable here that we could move forwards with in the future, but ultimately I felt it was out of scope for your PR here since the existing functionality remains unchanged.
The changes in my PR come without any updates to the tests 😓 or even any manual validation of the contact-attachment flow. I have been purely testing with reports. I am out of time for the week, though, and I figured I would post what I have and get your thoughts. 👍
|
Thanks @jkuester for going down the rabbit hole. I have been down there myself a couple of times with this PR 😃 . Thanks for PR benkags#10 - I agree, it a much better PR review feedback approach. This is what I picked from PR as net change. Let me know if I missed something
I think it really comes down to what approach to adopt between Approach A (the one I have here) and Approach B (in your PR) from a design perpective
|
|
I think you have pretty clearly understood my PR. 👍 Just a couple comments:
I 100% was hoping this would work out as you have described. 😓 Unfortunately, it did not really seem to save us any complexity.
The On top of that, changing to store the attachment name as the property value for
It ended up being similar complexity to just properly calculate the field path (even for the sub-docs). That was the point where I started trying to figure out what benefits I was actually getting from On the other hand, with @benkags if you have a strong preference for Also happy to jump on a call to discuss further if that would be helpful! 👍 |
39ccc3c to
69fe840
Compare
e26b095 to
e850f29
Compare
|
Ready for review @jkuester |
19e33c9 to
d3f9c15
Compare
|
@dianabarsan @benkags you have continued the discussion/efforts here in my absence last week. 🙏 Thank you! I know there are some plans in motion regarding a Feature Release, etc. I do not intent to change our direction there at all, but I did want to start a new conversation about how to move forwards in the future with getting these changes (or an future iteration of them) into Coming back this week and taking a fresh look at the code, I have put together a sort of design doc where I try to clearly outline the details of the existing |
e3ec0be to
0c4ecea
Compare
…rt forms (medic#10922) Co-authored-by: Bernard K. <kagondubernard81@gmail.com>
…rt forms When a report form contains [db-doc="true"] sub-documents with binary/file fields, attachments are now routed to the owning sub-document instead of always attaching to the main report doc. - Add resolveOwnerDoc() to walk up XML tree to nearest db-doc ancestor - Route FileManager file uploads to correct owner doc - Route inline binary blobs to correct owner doc - Fall back to main report doc when element is not inside a sub-doc
processAllAttachments now walks the parsed XML to determine which prepared doc owns each [type=binary] element (main / sibling / repeat child) and attaches files accordingly, instead of dumping every upload on preparedDocs[0]. Field-value sanitization and orphan cleanup also run per-doc. Adds two private helpers: - resolveContactOwnerDoc: DOM-walk from any element to its section root, then to the owning prepared doc (with mainDoc fallback). - findContactOwnerForFilename: locates the [type=binary] node whose text matches a FileManager filename and resolves its owner. No public API changes; no new service dependencies.
saveContact now uses validateAttachments(preparedDocs.preparedDocs)
New 'attachment routing to sub-contacts' describe block exercising: - file uploaded inside a sibling section -> sibling doc - file uploaded inside a repeat child -> i-th repeat doc - mixed uploads across main / sibling / repeat -> each owner - FileManager file with no matching binary node -> main doc fallback - inline binary (draw widget) inside sibling -> sibling doc - per-doc field-value sanitization (sibling field rewritten, main untouched) - main-doc orphan cleanup on edit path with per-doc loop
- main-doc & sub-doc oversize attachment fails saveContact - normal-sized attachment passes validation
Enketo's setVal rewrites uploaded binary nodes to type="file" the moment a value is set.
Port the sub-doc attachment-routing feature (medic#10903/medic#10904) onto master's restructured Enketo form-save code (medic#11256). - Re-home report + contact attachment routing onto EnketoService.saveReport / saveContact via AttachmentRoutingService strategies that resolve each file / inline-binary owner by nearest owning DOM element (EnketoReportFormData / EnketoContactFormData), replacing processFormAttachments. - Move the data-attachment-ref sidecar + node-name findCurrentElement into EnketoPrepopulationDataService.bindJsonToXml (ex-EnketoTranslationService home). - Add validateAttachments on the contact save path in FormService. - Restore AttachmentService (the routing engine's doc-mutation API). - Add EnketoContactFormData.getMainData(). - Route on minified contact docs to avoid the parent/contact reference cycle; drop empty _attachments maps. - Delete the obsolete ContactSaveService / EnketoTranslationService and their specs; update enketo.service.spec attachment assertions to owner-relative user-file-<ref> naming. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hi @benkags — thank you for the contribution! Before this PR can be reviewed, a few things need to be addressed:
While these checks fail, the PR carries the |
jkuester
left a comment
There was a problem hiding this comment.
Okay, @dianabarsan, this should be ready for your final review! 🙏
As was foretold, the merge here with the latest changes in master was spicy. The main changes that now exist here are that I moved all the attachment handling for the docs out of the enketo.service and down into the form-data classes. That just provided cleaner re-usability (and got a bit more logic out of the EnektoService...).
Bernard had a bunch of e2e tests and I combined a few and stacked some more in their too, so we really have good e2e coverage of all this functionality now!
A couple other things that changed were:
binaryattachment names for reports now include the top-levelfieldsin the path. This is just to unify the naming standard across all docs (reports + contacts).- Updated the
format-data-record.serviceto properly account for this naming change when loading the binary images for the Reports view. - Also updated the
format-data-record.serviceto properly handle repeat groups in thehidden_fieldslist (fixes #10791). - To support all of this, I ended up refactoring/simplifying the xpath code to allow for injecting the form's
repeatPathscontext.
@benkags can you support here by testing the latest code in this branch against some of the edge cases you and Imran were working through? 🙏 I am pretty sure everything you had automated tests for is covered, but as you know there are just an unending amount of nasty edge cases and I want to be sure we do not regress on anything that you had working before... 😬
| .reduce((binaryAttachments, { filename, attachment }) => ({ ...binaryAttachments, [filename]: attachment }), {}); | ||
| const newFileAttachments = FileManager | ||
| .getCurrentFiles() | ||
| .filter(({ name }) => this.findNodeWithTextContent(name)) |
There was a problem hiding this comment.
Will two files with the same name, but different content, get mapped correctly in different docs here?
There was a problem hiding this comment.
two files with the same name, but different content
This is technically impossible. Enketo appends a unique(ish) value to the end of a filename when you upload it into the form (hh-mm-ss). If the user manages to upload different files with the same name into two different form questions within the same second, the collision will happen in the Enekto code (and I think the second file will win). If the form questions are routed to different docs, the winning file will be attached to both docs (since both docs have a property that references the file-name). This seemed like the most acceptable behavior without patching Enketo to have more unique file names.
|
Thank you @jkuester for the hard work merging master. I'll manually test this and update here. |
|
D-Tree's household form works as expected. |



Description
Every file uploaded in an Enketo form was attached to the main doc, regardless of which sub-document it was uploaded in. A photo added inside a CHW (sibling) section of a CHW Area creation form landed on the CHW Area doc; a file inside a
db-doc="true"group orrepeat landed on the report doc instead of the sub-report.
This PR routes each upload and inline binary to the document that owns it, in both save pipelines:
Contact forms (#10903)
[type=file]/[type=binary]node by its form section: main section → primary doc,<contact>/<parent>section → the matching sibling doc, i-th<repeat><child>→ the i-th repeat doc. Unmatched uploads fall back to the main doc.Report forms (#10904)
db-doc="true"sub-document, falling back to the main report.doc.fields(repeat-index aware for the main doc), so the field value always resolves its attachment asuser-file-+ value.Shared pipeline
Both services now delegate to a new
AttachmentRoutingService(upload routing → inline-binary routing → per-doc finalize), parameterized by a small per-pipeline strategy (owner resolution, reference container, field path). Pure DOM primitives live inattachment-routing.provider.ts.Attachment naming & backwards compatibility
user-file-<owner-relative-xpath>(e.g.user-file-photo,user-file-group/photo,user-file-my_repeat[2]/photo) instead of the legacyuser-file/<form-id>/<xpath>, and the field value now stores the barereference. File-widget uploads keep the
user-file-<filename>scheme.user-file--prefixed names it can account for.data-attachment-refsidecar attribute stashed at load time; a fresh upload always wins over a stale sidecar. A dedicated spec pins the enketo-core merge behavior this relies on.db-docsub-report routing (including repeats and multiple sub-docs), untouched inline-binary preservation on report edit.Code review checklist
draft docs PR chore: document per-doc attachment routing and inline-binary naming (cht-core#10903) cht-docs#2226
Used claude code for code walkthrough, reasoning about the problem, design and implementing code stubs
License
The software is provided under AGPL-3.0. Contributions to this project are accepted under the same license.