Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 37 additions & 5 deletions specifyweb/backend/trees/extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,13 @@ def definition_joins(table, depth):
for j in range(depth)
])

def set_fullnames(treedef, null_only=False, node_number_range=None):
def set_fullnames(
treedef,
null_only: bool = False,
node_number_range=None,
include_synonyms: bool = False,
synonyms_only: bool = False,
):
table = treedef.treeentries.model._meta.db_table
depth = treedef.treedefitems.count()
reverse = treedef.fullnamedirection == -1
Expand All @@ -569,29 +575,55 @@ def set_fullnames(treedef, null_only=False, node_number_range=None):
if depth < 1:
return
cursor = connection.cursor()

if synonyms_only:
accepted_filter = "and t0.acceptedid is not null"
elif include_synonyms:
accepted_filter = ""
else:
accepted_filter = "and t0.acceptedid is null"

fullname_sql_expr = fullname_expr(depth, reverse)

diff_condition = (
"and (t0.fullname is null OR t0.fullname <> {expr})".format(
expr=fullname_sql_expr
)
if not null_only
else ""
)

sql = (
"update {table} t0\n"
"{parent_joins}\n"
"{definition_joins}\n"
"set {set_expr}\n"
"where t{root}.parentid is null\n"
"and t0.{table}treedefid = {treedefid}\n"
"and t0.acceptedid is null\n"
"{accepted_filter}\n"
"{null_only}\n"
"{node_number_range}\n"
"{diff_condition}\n"
).format(
root=depth-1,
table=table,
treedefid=treedefid,
set_expr=f"t0.fullname = {fullname_expr(depth, reverse)}",
set_expr=f"t0.fullname = {fullname_sql_expr}",
parent_joins=parent_joins(table, depth),
definition_joins=definition_joins(table, depth),
accepted_filter=accepted_filter,
null_only="and t0.fullname is null" if null_only else "",
node_number_range=f"and t0.nodenumber between {node_number_range[0]} and {node_number_range[1]}" if not (node_number_range is None) else ''
node_number_range=(
f"and t0.nodenumber between {node_number_range[0]} and {node_number_range[1]}"
if node_number_range is not None
else ''
),
diff_condition=diff_condition,
)

logger.debug('fullname update sql:\n%s', sql)
return cursor.execute(sql)
cursor.execute(sql)
return cursor.rowcount

def predict_fullname(table, depth, parentid, defitemid, name, reverse=False):
cursor = connection.cursor()
Expand Down
1 change: 1 addition & 0 deletions specifyweb/backend/trees/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
path('<int:id>/bulk_move/', views.bulk_move),
path('<int:id>/synonymize/', views.synonymize),
path('<int:id>/desynonymize/', views.desynonymize),
path('<int:id>/rebuild_fullname/', views.rebuild_fullname),
path('<int:rankid>/tree_rank_item_count/', views.tree_rank_item_count),
path('<int:parentid>/predict_fullname/', views.predict_fullname),
re_path(r'^(?P<treedef>\d+)/(?P<parentid>\w+)/stats/$', views.tree_stats),
Expand Down
54 changes: 54 additions & 0 deletions specifyweb/backend/trees/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,54 @@ def repair_tree(request, tree: TREE_TABLE):
extras.renumber_tree(table)
extras.validate_tree_numbering(table)

@login_maybe_required
@require_POST
@transaction.atomic
def rebuild_fullname(request, tree: TREE_TABLE, id: int):
"""Rebuild fullname values for the specified tree definition.

By default only accepted (preferred) nodes are processed. Pass
?rebuild_synonyms=true to also rebuild fullname values for synonym nodes.
"""
check_permission_targets(
request.specify_collection.id,
request.specify_user.id,
[perm_target(tree).rebuild_fullname],
)

rebuild_synonyms = request.GET.get('rebuild_synonyms', 'false').lower() == 'true'

tree_name = tree.title()
treedef = get_object_or_404(f"{tree_name}treedef", id=id)

accepted_changed = extras.set_fullnames(
treedef,
null_only=False,
include_synonyms=False,
synonyms_only=False,
)

synonyms_changed = 0
if rebuild_synonyms:
synonyms_changed = extras.set_fullnames(
treedef,
null_only=False,
include_synonyms=True,
synonyms_only=True,
)

payload = {
"success": True,
"rebuild_synonyms": rebuild_synonyms,
"changed": {
"accepted": accepted_changed,
"synonyms": synonyms_changed,
"total": accepted_changed + synonyms_changed,
},
}

return HttpResponse(toJson(payload), content_type="application/json")

@tree_mutation
def add_root(request, tree, treeid):
"Creates a root node in a specific tree."
Expand Down Expand Up @@ -550,6 +598,7 @@ class TaxonMutationPT(PermissionTarget):
synonymize = PermissionTargetAction()
desynonymize = PermissionTargetAction()
repair = PermissionTargetAction()
rebuild_fullname = PermissionTargetAction()


class GeographyMutationPT(PermissionTarget):
Expand All @@ -559,6 +608,7 @@ class GeographyMutationPT(PermissionTarget):
synonymize = PermissionTargetAction()
desynonymize = PermissionTargetAction()
repair = PermissionTargetAction()
rebuild_fullname = PermissionTargetAction()


class StorageMutationPT(PermissionTarget):
Expand All @@ -569,6 +619,7 @@ class StorageMutationPT(PermissionTarget):
synonymize = PermissionTargetAction()
desynonymize = PermissionTargetAction()
repair = PermissionTargetAction()
rebuild_fullname = PermissionTargetAction()


class GeologictimeperiodMutationPT(PermissionTarget):
Expand All @@ -578,6 +629,7 @@ class GeologictimeperiodMutationPT(PermissionTarget):
synonymize = PermissionTargetAction()
desynonymize = PermissionTargetAction()
repair = PermissionTargetAction()
rebuild_fullname = PermissionTargetAction()


class LithostratMutationPT(PermissionTarget):
Expand All @@ -587,6 +639,7 @@ class LithostratMutationPT(PermissionTarget):
synonymize = PermissionTargetAction()
desynonymize = PermissionTargetAction()
repair = PermissionTargetAction()
rebuild_fullname = PermissionTargetAction()

class TectonicunitMutationPT(PermissionTarget):
resource = "/tree/edit/tectonicunit"
Expand All @@ -595,6 +648,7 @@ class TectonicunitMutationPT(PermissionTarget):
synonymize = PermissionTargetAction()
desynonymize = PermissionTargetAction()
repair = PermissionTargetAction()
rebuild_fullname = PermissionTargetAction()

def perm_target(tree):
return {
Expand Down
1 change: 1 addition & 0 deletions specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const icons = {
userCircle: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z" fillRule="evenodd" /></svg>,
variable: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M4.649 3.084A1 1 0 015.163 4.4 13.95 13.95 0 004 10c0 1.993.416 3.886 1.164 5.6a1 1 0 01-1.832.8A15.95 15.95 0 012 10c0-2.274.475-4.44 1.332-6.4a1 1 0 011.317-.516zM12.96 7a3 3 0 00-2.342 1.126l-.328.41-.111-.279A2 2 0 008.323 7H8a1 1 0 000 2h.323l.532 1.33-1.035 1.295a1 1 0 01-.781.375H7a1 1 0 100 2h.039a3 3 0 002.342-1.126l.328-.41.111.279A2 2 0 0011.677 14H12a1 1 0 100-2h-.323l-.532-1.33 1.035-1.295A1 1 0 0112.961 9H13a1 1 0 100-2h-.039zm1.874-2.6a1 1 0 011.833-.8A15.95 15.95 0 0118 10c0 2.274-.475 4.44-1.332 6.4a1 1 0 11-1.832-.8A13.949 13.949 0 0016 10c0-1.993-.416-3.886-1.165-5.6z" fillRule="evenodd" /></svg>,
viewList: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" fillRule="evenodd" /></svg>,
wrench: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M19 5.5a4.5 4.5 0 0 1-4.791 4.49c-.873-.055-1.808.128-2.368.8l-6.024 7.23a2.724 2.724 0 1 1-3.837-3.837L9.21 8.16c.672-.56.855-1.495.8-2.368a4.5 4.5 0 0 1 5.873-4.575c.324.105.39.51.15.752L13.34 4.66a.455.455 0 0 0-.11.494 3.01 3.01 0 0 0 1.617 1.617c.17.07.363.02.493-.111l2.692-2.692c.241-.241.647-.174.752.15.14.435.216.9.216 1.382ZM4 17a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" fillRule="evenodd" /></svg>,
x: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" fillRule="evenodd" /></svg>,
zoomIn: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M5 8a1 1 0 011-1h1V6a1 1 0 012 0v1h1a1 1 0 110 2H9v1a1 1 0 11-2 0V9H6a1 1 0 01-1-1z" /><path clipRule="evenodd" d="M2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8zm6-4a4 4 0 100 8 4 4 0 000-8z" fillRule="evenodd" /></svg>,
zoomOut: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" fillRule="evenodd" /><path clipRule="evenodd" d="M5 8a1 1 0 011-1h4a1 1 0 110 2H6a1 1 0 01-1-1z" fillRule="evenodd" /></svg>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const rawUserTools = ensure<IR<IR<Omit<MenuItem, 'name'>>>>()({
repairTree: {
title: headerText.repairTree(),
url: '/specify/overlay/tree-repair/',
icon: icons.checkCircle,
icon: icons.wrench,
enabled: () =>
getDisciplineTrees().some((treeName) =>
hasPermission(`/tree/edit/${toLowerCase(treeName)}`, 'repair')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,21 @@ export const operationPolicies = {
'copy_from_library',
],
'/permissions/library/roles': ['read', 'create', 'update', 'delete'],
'/tree/edit/taxon': ['merge', 'move', 'synonymize', 'desynonymize', 'repair'],
'/tree/edit/taxon': [
'merge',
'move',
'synonymize',
'desynonymize',
'repair',
'rebuild_fullname',
],
'/tree/edit/geography': [
'merge',
'move',
'synonymize',
'desynonymize',
'repair',
'rebuild_fullname',
],
'/tree/edit/storage': [
'merge',
Expand All @@ -39,27 +47,31 @@ export const operationPolicies = {
'desynonymize',
'repair',
'bulk_move',
'rebuild_fullname',
],
'/tree/edit/geologictimeperiod': [
'merge',
'move',
'synonymize',
'desynonymize',
'repair',
'rebuild_fullname',
],
'/tree/edit/lithostrat': [
'merge',
'move',
'synonymize',
'desynonymize',
'repair',
'rebuild_fullname',
],
'/tree/edit/tectonicunit': [
'merge',
'move',
'synonymize',
'desynonymize',
'repair',
'rebuild_fullname',
],
'/querybuilder/query': [
'execute',
Expand Down
Loading