Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
32d2a4f
Fix:Expose general collection prefs for picklist scoping and attachme…
Gitesh307 Oct 1, 2025
008fe07
implement Collection Preferences UI with dedicated definitions
Gitesh307 Oct 2, 2025
a9fd48e
adding missing files for the Collection preferences
Gitesh307 Oct 2, 2025
45b4734
resolve typecheck errors in Collection Preferences definitions
Gitesh307 Oct 3, 2025
9411b4a
add Collection Preferences UI and editor with doc links
Gitesh307 Oct 4, 2025
d185810
implify CollectionPreferences page by removing internal save
Gitesh307 Oct 4, 2025
3f19937
Corrected specifyNetwork keys to publishingOrg and datasetKey
Gitesh307 Oct 4, 2025
ce2b1c6
fix TypeScript errors in collection preference definitions
Gitesh307 Oct 4, 2025
45cbc31
align CollectionDefinitions with UserDefinitions typing
Gitesh307 Oct 4, 2025
d70cdd5
Fix localization scanner issues in preferences
Gitesh307 Oct 4, 2025
eb00132
adding path for role gated access
Gitesh307 Oct 5, 2025
fb3d616
Lint code with ESLint and Prettier
Gitesh307 Oct 4, 2025
b59b33e
Refactor collection preferences renderer and documentation link layout
Gitesh307 Oct 6, 2025
b75ae77
Lint code with ESLint and Prettier
Gitesh307 Oct 6, 2025
2beb319
Localization cleanup for collection preferences
Gitesh307 Oct 6, 2025
7e905d0
Lint code with ESLint and Prettier
Gitesh307 Oct 6, 2025
c37ae85
removed collection pref refrence from User Tools
Gitesh307 Oct 6, 2025
d84ba94
remove extra language lines and keep only 'en-us'
Gitesh307 Oct 7, 2025
70780ae
extract shared logic into createPreferencesEditor and wire User/Colle…
Gitesh307 Oct 7, 2025
ccd5d0c
implemented IR<T> for preferences typing
Gitesh307 Oct 7, 2025
80a9467
Lint code with ESLint and Prettier
Gitesh307 Oct 7, 2025
d1bdada
resolve BasePreferences context type incompatibility in index.tsx
Gitesh307 Oct 7, 2025
f534d6c
simplify resolveCollectionDocumentHref logic using unified NAME_DOCS_MAP
Gitesh307 Oct 7, 2025
baaebac
Lint code with ESLint and Prettier
Gitesh307 Oct 7, 2025
5369e91
unify visibilityContext to remove duplication
Gitesh307 Oct 7, 2025
53490e7
Lint code with ESLint and Prettier
Gitesh307 Oct 7, 2025
7bb534f
optimize preferences wrappers by introducing FetchGate component.
Gitesh307 Oct 7, 2025
b7fb880
simplify className logic for documentation paragraph
Gitesh307 Oct 7, 2025
f61be53
Localization cleanup for collection preferences
Gitesh307 Oct 8, 2025
0cd11cb
removed `as unknown` casts
Gitesh307 Oct 8, 2025
665e6c7
consolidate doc links and merge catalog-number card
Gitesh307 Oct 10, 2025
a315d41
removed unused imports
Gitesh307 Oct 10, 2025
85413e4
function call
Gitesh307 Oct 10, 2025
81e4daa
render editor inline for User tools menu
Gitesh307 Oct 11, 2025
88a6280
Lint code with ESLint and Prettier
Gitesh307 Oct 10, 2025
5bec699
Lint code with ESLint and Prettier
Gitesh307 Oct 11, 2025
18ae49a
Modularize preferences localization into smaller files
Gitesh307 Oct 16, 2025
0baff4d
createDictionary export for modularize prefrences
Gitesh307 Oct 17, 2025
c7fc0db
Refactor preference localization dictionaries to expose raw maps
Gitesh307 Oct 17, 2025
694864a
Modularize preferences localization dictionary
Gitesh307 Oct 17, 2025
a22c3cd
fix: links to docs
grantfitzsimmons Oct 17, 2025
67c9b78
Lint code with ESLint and Prettier
Gitesh307 Oct 17, 2025
3c7088b
refactor(preferences): improve tree management
grantfitzsimmons Oct 17, 2025
43a5c90
fix: improve public attachment description
grantfitzsimmons Oct 17, 2025
527c1cb
Update attachments.ts
grantfitzsimmons Oct 17, 2025
b9b57a6
feat: add descriptions for specify network fields
grantfitzsimmons Oct 17, 2025
322e760
feat: improve descriptions for stats fields
grantfitzsimmons Oct 17, 2025
02e7a26
feat: improve title for synonym behavior section
grantfitzsimmons Oct 17, 2025
6a2306c
feat: improve cat number inheritance
grantfitzsimmons Oct 17, 2025
ade9f24
feat: improve descriptions for catalog number inheritance
grantfitzsimmons Oct 17, 2025
88b775b
feat: add better icon, distinguish preferences
grantfitzsimmons Oct 17, 2025
8fcfeea
fix: reorder trees in order of relevance
grantfitzsimmons Oct 17, 2025
9f315e4
feat(collection preferences): add sidebar
grantfitzsimmons Oct 17, 2025
1eec258
feat(preferences): match user preferences visual
grantfitzsimmons Oct 17, 2025
0d16c07
Sync localization strings with Weblate
maksim2005UKR Oct 14, 2025
51bd2bd
Sync localization strings with Weblate
Oct 17, 2025
9f180ac
fix: failing test
grantfitzsimmons Oct 18, 2025
ec5c1f7
fix: remove unused localization strings
grantfitzsimmons Oct 18, 2025
8545502
refactor: simplify code
grantfitzsimmons Oct 18, 2025
536711f
Lint code with ESLint and Prettier
grantfitzsimmons Oct 18, 2025
151e642
fix(prefs): use collection pref for tree settings
grantfitzsimmons Oct 27, 2025
7583ea6
fix: collection pref
grantfitzsimmons Oct 28, 2025
799707d
Lint code with ESLint and Prettier
grantfitzsimmons Oct 28, 2025
d5e96b9
removed obsolete logic for statsThreshold
Gitesh307 Oct 28, 2025
6cec0e1
stats threshold preference definitions
Gitesh307 Oct 28, 2025
0a586ae
Use collection-scoped remote prefs for synonym and attachment defaults
Gitesh307 Oct 30, 2025
ef5a8f7
Use collection-scoped remote prefs for synonym and attachment defaults
Gitesh307 Oct 30, 2025
cd711e3
Use collection-scoped remote prefs for synonym and attachment defaults
Gitesh307 Oct 30, 2025
6e0ba1e
Refine collection-pref lookup for synonym and attachment defaults
Gitesh307 Oct 30, 2025
27160c8
remove always-false condition in
Gitesh307 Oct 30, 2025
bba8088
removed unused imports
Gitesh307 Oct 30, 2025
e9b8f26
Update remotePrefs snapshot for collection pref keys
Gitesh307 Oct 30, 2025
58bb93b
Lint code with ESLint and Prettier
Gitesh307 Oct 30, 2025
f1dc60e
Allow collections to enable synonym actions via preference.
Gitesh307 Oct 31, 2025
8457181
Fix picklist scoping to respect “Scope Entire Table” preference
Gitesh307 Nov 1, 2025
51e1d5c
Fix Entire Table picklist scoping toggle to respect collection prefs
Gitesh307 Nov 1, 2025
d99d8c2
Fix Entire Table picklist scoping toggle to respect collection prefs
Gitesh307 Nov 1, 2025
a6a1065
Added ajax overrides for the unscoped requests
Gitesh307 Nov 1, 2025
9b814bb
fixed failing tests
Gitesh307 Nov 1, 2025
579a30b
Enable collection-level catalog-number inheritance toggles
Gitesh307 Nov 2, 2025
e8b6976
Lint code with ESLint and Prettier
Gitesh307 Nov 2, 2025
0964161
Collection Preferences menu visibility based on roles and App Resourc…
Gitesh307 Nov 14, 2025
da5271a
Lint code with ESLint and Prettier
Gitesh307 Nov 14, 2025
8b8f0c2
Fix catalog inheritance sidebar entry and show Component heading
Gitesh307 Nov 14, 2025
63b2cc7
Merge branch 'issue-7445' into issue-7440
Gitesh307 Nov 14, 2025
ec1864b
Merge remote-tracking branch 'origin/issue-7445' into issue-7440
Gitesh307 Nov 16, 2025
044b1df
changed default value to name for Ordering the trees
Gitesh307 Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 102 additions & 14 deletions specifyweb/backend/trees/extras.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import re
from contextlib import contextmanager
import logging
from typing import Iterable

from specifyweb.backend.trees.ranks import RankOperation, post_tree_rank_save, pre_tree_rank_deletion, \
verify_rank_parent_chain_integrity, pre_tree_rank_init, post_tree_rank_deletion
Expand All @@ -16,7 +18,97 @@
from specifyweb.backend.businessrules.exceptions import TreeBusinessRuleException
import specifyweb.specify.models as spmodels

from specifyweb.backend.workbench.upload.auditcodes import TREE_BULK_MOVE, TREE_MERGE, TREE_SYNONYMIZE, TREE_DESYNONYMIZE
from specifyweb.backend.workbench.upload.auditcodes import TREE_BULK_MOVE, TREE_MERGE, TREE_SYNONYMIZE, TREE_DESYNONYMIZE

_SYNONYM_PREF_KEYS_BY_TABLE: dict[str, tuple[str, ...]] = {
'GeologicTimePeriod': (
'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod',
'sp7.allow_adding_child_to_synonymized_parent.ChronosStrat',
'sp7.allow_adding_child_to_synonymized_parent.ChronoStrat',
),
'ChronosStrat': (
Copy link
Contributor

Choose a reason for hiding this comment

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

Why 2? See line 34

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Older deployments saved the Chronostrat synonym preference under two different keys, ChronosStrat (the legacy table/view name) and ChronoStrat (the shortened label used in global properties). If we drop one of them, any collection that previously saved the preference under that spelling would lose the ability to add children to synonymized Chronostrat nodes. Keeping both entries ensures _synonym_pref_keys continues to read whatever was persisted historically.

'sp7.allow_adding_child_to_synonymized_parent.ChronosStrat',
'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod',
'sp7.allow_adding_child_to_synonymized_parent.ChronoStrat',
),
'ChronoStrat': (
'sp7.allow_adding_child_to_synonymized_parent.ChronoStrat',
'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod',
'sp7.allow_adding_child_to_synonymized_parent.ChronosStrat',
),
}


def _synonym_pref_keys(node) -> tuple[str, ...]:
table_name = node.specify_model.name
base_key = f'sp7.allow_adding_child_to_synonymized_parent.{table_name}'
keys = _SYNONYM_PREF_KEYS_BY_TABLE.get(table_name)
if keys is None:
return (base_key,)

if keys and keys[0] == base_key:
return keys
return (base_key, *keys)


def _collection_synonym_pref_enabled(keys: Iterable[str]) -> bool:
from specifyweb.specify.models import Spappresourcedata

qs = Spappresourcedata.objects.filter(
spappresource__name='CollectionPreferences'
).values_list('data', flat=True)

for raw_data in qs:
if not raw_data:
continue
if isinstance(raw_data, memoryview):
raw_data = raw_data.tobytes()
if isinstance(raw_data, (bytes, bytearray)):
try:
raw_data = raw_data.decode('utf-8')
except UnicodeDecodeError:
continue
try:
prefs = json.loads(raw_data)
except (TypeError, ValueError):
continue

if not isinstance(prefs, dict):
continue

tree_management = prefs.get('treeManagement')
if not isinstance(tree_management, dict):
continue

synonymized = tree_management.get('synonymized')
if not isinstance(synonymized, dict):
continue

for key in keys:
value = synonymized.get(key)
if value is True:
return True

return False


def _remote_synonym_pref_enabled(keys: Iterable[str]) -> bool:
from specifyweb.backend.context.remote_prefs import get_remote_prefs

prefs_text = get_remote_prefs()
for key in keys:
pattern = r'^' + re.escape(key) + r'(?:_\d+)?=(.+)'
override = re.search(pattern, prefs_text, re.MULTILINE)
if override is not None and override.group(1).strip().lower() == "true":
return True
return False


def _synonym_override_enabled(node) -> bool:
"""Return True when collection or remote prefs allow actions on synonymized parents."""

keys = _synonym_pref_keys(node)
return _collection_synonym_pref_enabled(keys) or _remote_synonym_pref_enabled(keys)

@contextmanager
def validate_node_numbers(table, revalidate_after=True):
Expand Down Expand Up @@ -208,11 +300,10 @@ def adding_node(node):
model = type(node)
parent = model.objects.select_for_update().get(id=node.parent.id)
if parent.accepted_id is not None:
from specifyweb.backend.context.remote_prefs import get_remote_prefs
# This business rule can be overriden by a remote pref.
pattern = r'^sp7\.allow_adding_child_to_synonymized_parent\.' + node.specify_model.name + '=(.+)'
override = re.search(pattern, get_remote_prefs(), re.MULTILINE)
if override is None or override.group(1).strip().lower() != "true":
if not _synonym_override_enabled(node):
node_children = [] if node.pk is None else list(node.children.values('id', 'fullname'))
parent_children = list(parent.children.values('id', 'fullname'))
parent_parent_id = parent.parent.id if parent.parent_id else None
raise TreeBusinessRuleException(
f'Adding node "{node.fullname}" to synonymized parent "{parent.fullname}"',
{"tree" : "Taxon",
Expand All @@ -223,14 +314,14 @@ def adding_node(node):
"rankid" : node.rankid,
"fullName" : node.fullname,
"parentid": node.parent.id,
"children": list(node.children.values('id', 'fullname'))
"children": node_children
},
"parent" : {
"id" : parent.id,
"rankid" : parent.rankid,
"fullName" : parent.fullname,
"parentid": parent.parent.id,
"children": list(parent.children.values('id', 'fullname'))
"parentid": parent_parent_id,
"children": parent_children
}})

insertion_point = open_interval(model, parent.nodenumber, 1)
Expand Down Expand Up @@ -396,11 +487,8 @@ def synonymize(node, into, agent):
node.isaccepted = False
node.save()

# This check can be disabled by a remote pref
from specifyweb.backend.context.remote_prefs import get_remote_prefs
pattern = r'^sp7\.allow_adding_child_to_synonymized_parent\.' + node.specify_model.name + '=(.+)'
override = re.search(pattern, get_remote_prefs(), re.MULTILINE)
if node.children.count() > 0 and (override is None or override.group(1).strip().lower() != "true"):
# This check can be disabled by a remote or collection preference override
if node.children.count() > 0 and not _synonym_override_enabled(node):
raise TreeBusinessRuleException(
f'Synonymizing node "{node.fullname}" which has children',
{"tree" : "Taxon",
Expand Down
83 changes: 81 additions & 2 deletions specifyweb/backend/trees/tests/test_tree_extras/test_synonymize.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,77 @@
import json

from specifyweb.backend.businessrules.exceptions import TreeBusinessRuleException
from specifyweb.specify.models import Determination, Taxon, Taxontreedef
from specifyweb.specify.models import (
Determination,
Spappresource,
Spappresourcedata,
Spappresourcedir,
Taxon,
Taxontreedef,
)
from specifyweb.backend.trees.tests.test_trees import GeographyTree
from specifyweb.backend.trees.extras import synonymize


class TestSynonymize(GeographyTree):

def _set_synonym_pref(self, key: str, value: bool = True) -> None:
app_dir = Spappresourcedir.objects.filter(
ispersonal=False,
collection=self.collection,
discipline=self.discipline,
specifyuser=self.specifyuser,
).first()
if app_dir is None:
app_dir = Spappresourcedir.objects.create(
ispersonal=False,
collection=self.collection,
discipline=self.discipline,
specifyuser=self.specifyuser,
)

app_resource = Spappresource.objects.filter(
spappresourcedir=app_dir,
name="CollectionPreferences",
specifyuser=self.specifyuser,
).first()
if app_resource is None:
app_resource = Spappresource.objects.create(
spappresourcedir=app_dir,
name="CollectionPreferences",
level=0,
metadata="",
mimetype="application/json",
specifyuser=self.specifyuser,
)

app_data = Spappresourcedata.objects.filter(
spappresource=app_resource
).first()
if app_data is None:
app_data = Spappresourcedata.objects.create(
spappresource=app_resource,
data=json.dumps({}),
)

raw_data = app_data.data
if isinstance(raw_data, memoryview):
raw_data = raw_data.tobytes()
if isinstance(raw_data, (bytes, bytearray)):
raw_data = raw_data.decode('utf-8')

try:
prefs = json.loads(raw_data if raw_data else '{}')
except (TypeError, ValueError):
prefs = {}

tree_management = prefs.setdefault('treeManagement', {})
synonymized = tree_management.setdefault('synonymized', {})
synonymized[key] = value

app_data.data = json.dumps(prefs)
app_data.save()

def test_different_type(self):
with self.assertRaises(AssertionError) as context:
synonymize(self.na, self.collectionobjects[0], self.agent)
Expand Down Expand Up @@ -69,6 +135,19 @@ def test_synonymize_geography_target_children(self):

self.assertEqual(context.exception.args[1]['localizationKey'], "nodeSynonimizeWithChildren")

def test_synonymize_geography_target_children_with_collection_pref(self):
self._set_synonym_pref(
'sp7.allow_adding_child_to_synonymized_parent.Geography',
True,
)

try:
synonymize(self.kansas, self.mo, self.agent)
except TreeBusinessRuleException:
self.fail(
'synonymize raised TreeBusinessRuleException despite collection preference override'
)

def test_synonymize_taxon_no_target_children(self):

life = Taxon.objects.create(
Expand Down Expand Up @@ -141,4 +220,4 @@ def test_synonymize_taxon_no_target_children(self):

self.assertEqual(det_plantae_1.preferredtaxon_id, plantae.id)
self.assertEqual(det_plantae_2.preferredtaxon_id, plantae.id)


17 changes: 12 additions & 5 deletions specifyweb/frontend/js_src/lib/components/AppResources/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { formatUrl } from '../Router/queryString';
import type { AppResourcesTree } from './hooks';
import { useResourcesTree } from './hooks';
import type { AppResourcesOutlet } from './index';
import { shouldShowCollectionPreferenceSubType } from './permissions';
import type { AppResourceType, ScopedAppResourceDir } from './types';
import { appResourceSubTypes, appResourceTypes } from './types';

Expand Down Expand Up @@ -60,6 +61,7 @@ export function CreateAppResource(): JSX.Element {
const [templateFile, setTemplateFile] = React.useState<
string | false | undefined
>(undefined);
const canSeeCollectionPreferences = shouldShowCollectionPreferenceSubType();
return directory === undefined ? (
<NotFoundView container={false} />
) : type === undefined ? (
Expand Down Expand Up @@ -98,11 +100,16 @@ export function CreateAppResource(): JSX.Element {
</tr>
</thead>
<tbody>
{Object.entries(appResourceSubTypes).map(
([
key,
{ icon, mimeType, name = '', documentationUrl, label, ...rest },
]) =>
{Object.entries(appResourceSubTypes)
.filter(
([key]) =>
key !== 'collectionPreferences' || canSeeCollectionPreferences
)
.map(
([
key,
{ icon, mimeType, name = '', documentationUrl, label, ...rest },
]) =>
'scope' in rest &&
!f.includes(rest.scope, directory.scope) ? undefined : (
<tr key={key}>
Expand Down
Loading