Conversation
melton-jason
left a comment
There was a problem hiding this comment.
I fixed a bug where the backend was completely ignoring the value of the sp7.allow_adding_child_to_synonymized_parent preferences.
The problem is that we were using a regular expression to fetch the value of the Remote Preference (now Collection Preference) prior to 61cc51b (introduced in #7557), and when we transitioned to a JSON dictionary approach to storing the preference, the regular expression was used as the key in the backend to fetch the preference value-- where it should have been using the string literal.
Below is the code from v7.11.3:
specify7/specifyweb/backend/trees/extras.py
Lines 401 to 402 in dcecec6
Below is the code in main:
specify7/specifyweb/backend/trees/extras.py
Lines 420 to 423 in 6683008
We should be using the literal sp7.allow_adding_child_to_synonymized_parent.TREE string rather than the regular expression as the key.
This fix also simplified some of the other changes in this PR, so feel free to give it a once over @acwhite211!
I'm not going to explicitly approve this PR due to the changes I've made (those should probably be reviewed first), but I'll be down to merge once the additional changes are reviewed further!
specifyweb/backend/trees/extras.py
Outdated
| if isinstance(treeManagement_pref, dict) else {} | ||
|
|
||
| add_synonym_enabled = synonymized.get(r'^sp7\.allow_adding_child_to_synonymized_parent\.' + node.specify_model.name + '=(.+)', False) if isinstance(synonymized, dict) else False | ||
| add_synonym_enabled = synonymized.get('sp7.allow_adding_child_to_synonymized_parent.' + node.specify_model.name, False) if isinstance(synonymized, dict) else False |
There was a problem hiding this comment.
(optional, just a question / request)
Is there a particular reason why we kept this preference name for the Collection Preferences (#7557)?
Since we're doing a total migration of this preference from Remote Preferences, I think we could move away from the verbose and confusing Java property names.
Something like this might be more readable for future maintainers and better leverage the new JSON structure of the preference (we could use an array instead of dict if you wanted to 🤷)
# other Collection Preferences ...
"treeManagement": {
"expand_synonymization_actions": {
"taxon": true,
"storage": true
}
}There was a problem hiding this comment.
I'd also be a fan to reconsider the default behavior of these preferences that are being redefined as Collection Preferences.
With this tree management preference in particular, perhaps we can invert its current behavior? That is, the user can opt-in to stricter tree checking rather than opt-in to more relaxed synonym checking behavior.
In other words, we provide the allow adding children to synonyms behavior by default and the managers can set this preference to disallow the adding synonymization of nodes with children and adding a child to a synonym.
@grantfitzsimmons
What do you think of this change to invert the synonymization preference (opt-in to strict checking rather than opt-in to looser checking)?
Form my experiences and understanding of how people interact with this preference thus far, the "looser" behavior is the more common and desired use case here.
There was a problem hiding this comment.
I would prefer this to be opt-in. Users generally favor the added flexibility.
There was a problem hiding this comment.
👍 I pushed a commit that switches both adding_node() and synonymize() to use the literal key sp7.allow_adding_child_to_synonymized_parent. instead of the old regex key, and centralizes CollectionPreferences parsing into a new function _get_collection_prefs_dict() so we handle None/empty/malformed JSON.
There was a problem hiding this comment.
I added a new function that can handle both a new json structure, as well as being backwards compatible with the legacy one. Let me know if that structure looks good.
Ready for re-review.
These will be addressed in #7663
melton-jason
left a comment
There was a problem hiding this comment.
Love the new helper functions for reading preferences! 🥳
| except Exception: | ||
| return {} |
There was a problem hiding this comment.
I'm a little hesitant on using a general Exception catch-all like this 😅.
This could make it pretty hard to debug if there were a genuine bug with parsing the collection preferences as JSON.
Theoretically, the only times the JSON.loads should fail is when the Collection Prefs is either not valid JSON or contains some unparseable character.
In either case, I'm of the opinion we should fail fast (and not catch exceptions) because that would indicate something wrong with the construction or update of the Collection Prefs: this would hopefully be caught with automated tests and general testing.
If some bug were to occur without us catching it, the user would be silently using the default values for the preferences: which could result in data integrity issues.
If we must keep the exception catch here, can we log a warning?
For the best of both worlds, maybe we can include a flag parameter to this function (or otherwise monkey patch it during tests) we can use to indicate whether it should fail silently or raise any execption.
| syn = tm.get("synonymized") | ||
| if isinstance(syn, dict): | ||
| legacy_key = f"sp7.allow_adding_child_to_synonymized_parent.{tree_name}" | ||
| if legacy_key in syn: | ||
| return bool(syn.get(legacy_key)) |
There was a problem hiding this comment.
I love the backwards compatibility sentiment here!
But we don't really need to be backwards compatible with the legacy preference.
This will be part of the new Collection Preferences that was introduced in #7557: which is not yet released!
As far as I know, we're not migrating any of the old legacy preferences or supporting the old Remote Preferences.
While people would need to reconfigure their preferences, this does come with the benefit of not needing to support any legacy customizations--so we can simplify our preference reading behavior and start with a clean slate.
Essentially, unless we decide to migrate people's preferences from Remote Preferences to Collection Preference or decide to implement a precedence in resolving these preferences, the only benefit of supporting the legacy preference is for testing databases that have already configured the preference
| collection_prefs_dict = _get_collection_prefs_dict(collection, user) | ||
|
|
||
| treeManagement_pref = collection_prefs_dict.get('treeManagement', {}) | ||
| treeManagement_pref = treeManagement_pref if isinstance(treeManagement_pref, dict) else {} | ||
|
|
||
| synonymized = treeManagement_pref.get('synonymized', {}) \ | ||
| if isinstance(treeManagement_pref, dict) else {} | ||
| synonymized = treeManagement_pref.get('synonymized', {}) | ||
| synonymized = synonymized if isinstance(synonymized, dict) else {} |
There was a problem hiding this comment.
Can we remove these lines?
They were only needed to read the expanded synonymization preference, but that's already handled as part of the _expand_synonymization_actions_enabled function call below
| collection_prefs_dict = _get_collection_prefs_dict(collection, user) | ||
|
|
||
| treeManagement_pref = collection_prefs_dict.get('treeManagement', {}) | ||
| treeManagement_pref = treeManagement_pref if isinstance(treeManagement_pref, dict) else {} | ||
|
|
||
| synonymized = treeManagement_pref.get('synonymized', {}) \ | ||
| if isinstance(treeManagement_pref, dict) else {} | ||
| synonymized = treeManagement_pref.get('synonymized', {}) | ||
| synonymized = synonymized if isinstance(synonymized, dict) else {} | ||
|
|
||
| add_synonym_enabled = synonymized.get(r'^sp7\.allow_adding_child_to_synonymized_parent\.' + node.specify_model.name + '=(.+)', False) if isinstance(synonymized, dict) else False | ||
| pref_key = f"sp7.allow_adding_child_to_synonymized_parent.{node.specify_model.name}" | ||
| add_synonym_enabled = bool(synonymized.get(pref_key, False)) |
There was a problem hiding this comment.
Can we remove these lines and replace them with a call to the new _expand_synonymization_actions_enabled helper function?
| parsed_locality_fields = [parse_field( | ||
| collection, 'Locality', dict['field'], dict['value'], locality_id, row_number) for dict in locality_values if dict['value'].strip() != ""] | ||
| parsed_locality_fields = [ | ||
| parse_field( | ||
| collection, 'Locality', d['field'], d['value'], locality_id, row_number | ||
| ) | ||
| for d in locality_values | ||
| ] | ||
|
|
||
| parsed_geocoorddetail_fields = [parse_field( | ||
| collection, 'Geocoorddetail', dict["field"], dict['value'], locality_id, row_number) for dict in geocoorddetail_values if dict['value'].strip() != ""] | ||
| parsed_geocoorddetail_fields = [ | ||
| parse_field( | ||
| collection, 'Geocoorddetail', d['field'], d['value'], locality_id, row_number | ||
| ) | ||
| for d in geocoorddetail_values | ||
| ] |
There was a problem hiding this comment.
Which failing tests were these fixing?
The if dict['value'].strip() != "" condition that was removed was put in place to handle the case when a field in the provided Data Set only contained whitespace (in which case the Locality Update Tool was to not parse the field and treat it like no value was provided).
It looks like tests are passing with the old condition as well.
For example a recent commit based on main, fbfcb52, had this function unchanged and backend tests passed: https://github.com/specify/specify7/actions/runs/21755196125/job/62763414702?pr=7643.
Although, I'm unsure if the Locality Update Tool tests even ran during that test suite. I couldn't find any of the tests in the output, and I also found a bunch of broken import paths for the locality update tests that I fixed in b87f4da
Was there some warning or error message in the test output that prompted this change, rather than a test explicitly failing?
| esa = tm.get("expand_synonymization_actions") | ||
| if isinstance(esa, dict) and tree_name in esa: | ||
| return bool(esa.get(tree_name)) |
There was a problem hiding this comment.
Just a reminder before this is merged that we also need to support this new structure on the frontend!
| def _expand_synonymization_actions_enabled(collection, user, tree_name: str) -> bool: | ||
| """ | ||
| New CollectionPreferences shape: | ||
| treeManagement.expand_synonymization_actions.<tree_name> = true/false |
There was a problem hiding this comment.
(A continuation of #7663 (comment) and #7663 (comment))
Do we want to make the more expand synonymization behavior the default and instead make this preference be an opt-in for stricter tree checking?
We would just need to change the name of the preference and flip the conditions when reading the preference.
Or is the decision to tackle that in a different PR or keep the behavior as-is?
Fixes #7662
Fixes the various failing unit tests currently in main. The two groups the unit tests that are fixed in this PR are the tree synonymy and locality update tests.
Checklist
self-explanatory (or properly documented)
Testing instructions