-
Notifications
You must be signed in to change notification settings - Fork 137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for choice from previous repeat answers #472
Conversation
Codecov Report
@@ Coverage Diff @@
## master #472 +/- ##
==========================================
+ Coverage 83.53% 83.59% +0.05%
==========================================
Files 25 25
Lines 3608 3627 +19
Branches 838 842 +4
==========================================
+ Hits 3014 3032 +18
Misses 452 452
- Partials 142 143 +1
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for picking this back up and for explaining the string-length
predicate! I described a few more tests to add.
pyxform/tests_v1/test_repeat.py
Outdated
| | text | name | Enter name | | | ||
| | end repeat | | | | | ||
| | select one fruits | fruit | Choose a fruit | | | ||
| | select one ${name} | choice | Choose name | starts-with(./name, "b") | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize users wouldn't write the choice_filter
that way, they'd write starts-with(${name}, "b")
. Another even more useful predicate to write would be, if there were also an age question in the repeat, ${age} > 10
. In both of those cases, the references should expand to ./name
and ./age
respectively because the predicate's context is the repeat. I'm not sure the best way to do that. Maybe expand references to an absolute reference and then substring the repeat path? So here you'd expand ${age}
to /data/repeat/age
and then remove /data/repeat
. This should work even if there's a subgroup in the repeat (e.g. /data/repeat/demographics/age
).
I think you should also have a test for one of these in a nested repeat. This seems likely to happen. For example, imagine an outer loop that collects information about households. Then a first repeat gets a roster of all household members. A second repeat after that asks questions about everyone over 18 (see the dynamic filter case above). I think the nodeset reference would have to be relative and look like ../repeat1
. Any relative references in the predicate would also have to be relative and start with current()/..
, I believe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree @lognaturel. Do leave some tests without ${}
syntax though. There are users that have started to leverage external data with complex queries and those will be using XPath for e.g. choice_filters (because the ${}
syntax won't work). They may naturally use XPath for all choice_filters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this may not be the case (or maybe you have another example in mind):
I think the nodeset reference would have to be relative and look like ../repeat1. Any relative references in the predicate would also have to be relative and start with current()/.., I believe.
We're not switching to another (instance) context here. So for:
<data>
<household> <!-- repeat -->
<member> <!-- repeat -->
<name/>
<age/>
<adult> <!-- repeat -->
<person/>
<education/>
</adult>
</member>
</household>
</data>
If person is a select one {name}
question, the nodeset would be "../../member[./age > 18]"
"../../../member[./age > 18]"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the nodeset would be "../../member[./age > 18]"
For that case the context would be /data/household[X]/member[Y]/adult[Z]/person
, right? Wouldn't the nodeset be ../../../member[./age > 18]
?
I was thinking of:
<data>
<household> <!-- repeat -->
<person> <!-- repeat -->
<name/>
<age/>
</person>
<adult> <!-- repeat -->
<adult_name/>
</adult>
</household>
</data>
First you get a roster of all the people in the household. Then you have a separate repeat that applies only to adults. Why would you do this instead of having a section with a relevant
in the person
repeat? I'm not sure. But I've seen forms like this.
If adult_name
has type select one ${name}
with choice filter ${age} > 18
then the context is /data/household[X]/adult[Y]/adult_name
and the nodeset would be ../../person[./age > 18]
. You're right, in that scenario, no current()
is needed.
I think a scenario where current()
would be needed is when a sibling node of a select in a repeat is used in the choice filter:
<data>
<household> <!-- repeat -->
<eligible/>
<person> <!-- repeat -->
<name/>
<age/>
</person>
<selected> <!-- repeat -->
<target_min_age/>
<selected_name/>
</selected>
</household>
</data>
Let's say selected_name
has type select one ${name}
with choice filter ${age} > ${target_min_age}
. The context is /data/household[X]/selected[Y]/selected_name
. The nodeset would be ../../person[./age > current()/../target_min_age]
, I think. This doesn't seem like a particularly practical example and it might be ok not to support it with ${}
syntax. I'd just like for us to be explicit about what we do and don't support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a test case for this scenario:
<data> <household> <!-- repeat --> <eligible/> <person> <!-- repeat --> <name/> <age/> </person> <selected> <!-- repeat --> <target_min_age/> <selected_name/> </selected> </household> </data>
With the current changes present in this branch, this seems to be supported with the ${}
syntax (Though we're currently using an absolute path to the node... Not entirely sure how & if that'll affect anything... Looking into generating relative paths at the moment).
Kindly have another look at the PR when possible @lognaturel @MartijnR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lognaturel, your explanation seems flawless (and my example indeed should have stepped down once more).
This also means that my understanding of current() and our spec needs work: https://getodk.github.io/xforms-spec/#fn:current getodk/xforms-spec#276
@DavisRayM, sorry for forgetting. I'll try to look at the tests today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem like a particularly practical example and it might be ok not to support it with ${} syntax. I'd just like for us to be explicit about what we do and don't support.
Would be great if we did. We'd likely save more work in analyzing difficult support requests in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<data> <household> <!-- repeat --> <member> <!-- repeat --> <name/> <age/> <adult> <!-- repeat --> <person/> <education/> </adult> </member> </household> </data>
Hey @MartijnR @lognaturel, For the example above should the nodeset be ../.. [ ./age > 18]
or ../../../member [ ./age > 18
? Is there a difference between the two ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd missed this and answered in Slack but for completeness: "That would not be the same because it would be going up to a specific instance of the member repeat. What we want is to get the member nodeset and then filter its nodes based on age."
e01adae
to
67b1b4c
Compare
7aeb1fb
to
4ca43f3
Compare
73f0b2d
to
7736a48
Compare
7736a48
to
41c36ad
Compare
d3af776
to
5d96e21
Compare
49a6bf6
to
a635a5a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 Great progress. One test case left to address. The rest all looks good and I've just given a couple ideas for you to consider. @MartijnR the test coverage looks sufficient to me.
pyxform/tests_v1/test_repeat.py
Outdated
| | end group | demographics | | | | ||
| | end repeat | | | | | ||
| | select one fruits | fruit | Choose a fruit | | | ||
| | select one ${name} | choice | Choose name | starts-with(./name, "b") | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make this more realistic, use ${name}
. Then you'll just need to add a space before ./name
in the expected output.
pyxform/tests_v1/test_repeat.py
Outdated
xml__contains=[ | ||
"<itemset nodeset=\"/pyxform_autotestname/rep[./name != '']\">" | ||
], | ||
run_odk_validate=True, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great that you tried all with Validate. We typically turn this flag off for the test suite because it takes time.
pyxform/question.py
Outdated
@@ -238,9 +251,21 @@ def build_xml(self): | |||
node("label", ref=itemset_label_ref), | |||
] | |||
result.appendChild(node("itemset", *itemset_children, nodeset=nodeset)) | |||
elif not self["children"] and self["list_name"]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially thought it would be clearer to have this whole "select from repeat" case separated from the existing cases but I see now that with having itemset
vs. list_name
attributes it gets messy. So I think the way you have it is fine but it'd be helpful to have a comment indicating which case this is -- a select from a previous repeat with no choice filter specified.
Or see another possible option at #472 (comment)
My goal would be to unify the "select from repeat" case so it's easier to understand and detangle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the option described in #472 (comment) is a bit more cleaner as you've stated. I've implemented the option in the latest commits.
pyxform/xls2json.py
Outdated
@@ -1187,6 +1188,8 @@ def replace_prefix(d, prefix): | |||
json_dict["choices"] = choices | |||
elif file_extension in [".csv", ".xml"]: | |||
new_json_dict["itemset"] = list_name | |||
elif re.match(r"\$\{(.*?)\}", list_name): | |||
new_json_dict["list_name"] = list_name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if you populate itemset
here, instead? Could you then collapse the cases in question.py
and have a case in the is_previous_question
branch for if the choice_filter
is not set? That would feel more "correct" to me in that when you define a select from a repeat it's inherently an itemset
, not a simple choice list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is possible. I've made these changes in the latest commits
name="data", | ||
id_string="some-id", | ||
md=xlsform_md, | ||
xml__contains=['<itemset nodeset="../../../member[ ./age > 18]">',], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is tricky indeed. The rule is that if you want to refer to an ancestor repeat, you need to parent above it and include its name. I'm not sure where the best place for that is and let's talk if you get stuck.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the changes at https://github.com/XLSForm/pyxform/pull/472/files#r511057165 to address this case? I think basically you'll need to address #453. Does that sound right, @MartijnR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this was kicking around my brain and I believe https://github.com/XLSForm/pyxform/pull/472/files#discussion_r511920303 addresses the case in #453. The case that's now missing is referencing a parent repeat. That is, a reference with a relative terminal is being generated (../../
) when the parent repeat name needs to be included (../../../member
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a few changes in the latest commits; Now checking if the context_parent
is a child of the xpath_path
and only referencing the parent repeat when this is the case
c21aaee
to
8eb48d0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, @DavisRayM! I think using "itemset" for the case with and without a choice filter turned out really well. And great that you've addressed all tricky cases!!
Going to give @MartijnR a chance to comment if he wants but this is ready to merge as far as I'm concerned.
Documentation Update issue: XLSForm/xlsform.github.io#210 |
Sorry, I've not been much online in the past month (network issues) and not
really able to do any work because of it. Relative path values for
`nodeset` and `ref` attributes won't work in Enketo. I think this may have
been first introduced with the setvalue feature in pyxform. I don't think
we can get this to work in Enketo without severe performance degradation
during the transformation step (and that's why it was actually removed
about 7 years ago and because pyxform never generated such values).
…On Thu, Oct 29, 2020 at 11:07 AM Hélène Martin ***@***.***> wrote:
***@***.**** approved this pull request.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#472 (review)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAEZFFUIV4G5M3FDHWXHUDTSNGOOZANCNFSM4RV3WC2Q>
.
--
*Pushing data since 2012.*
Enketo <https://enketo.org> | LinkedIn
<http://www.linkedin.com/company/enketo-llc> | GitHub
<https://github.com/enketo> | Twitter <https://twitter.com/enketo>
| Blog <http://blog.enketo.org>
--
--
*Revolutionizing data collection since 2012.*
Enketo
<https://enketo.org/> | LinkedIn
<http://www.linkedin.com/company/enketo-llc> | GitHub
<https://github.com/enketo> | Twitter <https://twitter.com/enketo>
| Blog <http://blog.enketo.org/>
|
Oh no! ⛰️
Sorry to hear that. I can see how performance could be a challenge for contextualization. What do you suggest as a way forward? Maybe we could add an Enketo-specific warning if such attribute values are generated? My (admittedly limited) experience has been that forms with nested repeats tend to be enumerator-mediated and lean more on Collect so it may not be a huge limitation. There are a lot of very useful things that can be done with this functionality for a single top-level repeat. I haven't seen as many nested repeats in Enketo but maybe you have more insights or see it differently, @MartijnR. |
I think there would be two solutions.
|
In a nested repeat context, wouldn't that mean that the first instance of the outer repeat is always used?
If you want I think the same would be the case for |
Ah, no, I think the difference is when relative paths are using in expressions (that should stay). We've always generated absolute paths for ref and nodeset attribute values for questions inside repeats (until about 2 months ago) and I think we still do for most questions. I think the relative path introduction was just an unnecessary slip (though syntax-wise not incorrect). |
Interesting, I see! I think we had identified a while back that according to W3C XForms I think it might be a messy special case to introduce but should be possible. How about merging this if it seems complete to you and then addressing the nodeset/ref case separately since it doesn't only touch on this work? |
OMG, I am so sorry. I saw the (an absolute value would return all the nodes from all repeats combined, I think) Sorry for wasting your time with this! Ready to merge I would think. Thanks to you both for this cool feature @DavisRayM and @lognaturel! |
Ok, now I'm confused. 😄 In this case it's some arbitrary XPath expression that gets contextualized against the current context and that may or may not be in the primary instance. That makes sense to me and it makes sense why it would be relative for the reasons I stated above. The
|
No, I believe it's a In the case of an itemset Enketo just evaluates the |
Great, looking forward to the explanation if there is one. 😄 Merging is blocked on your change request, @MartijnR! If you're happy can you please approve and merge? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, forgot I had changes to review. Looks good!
Thanks all, and especially @DavisRayM! 🎉 |
Based on #381
Closes #38
Why is this the best possible solution? Were any other approaches considered?
Easiest solution with the least amount of changes.
What are the regression risks?
None
Does this change require updates to documentation? If so, please file an issue here and include the link below.
Yes, would need an additional entry that states that this is possible.
Before submitting this PR, please make sure you have:
tests_v1
nosetests
and verified all tests passblack pyxform
to format code