Skip to content
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 an Add and Delete tree ranks option #4257

Merged
merged 110 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
6df778e
init api for adding and deleting tree ranks
acwhite211 Dec 5, 2023
d17e266
init unit test for adding/deleting ranks
acwhite211 Dec 7, 2023
fc664ad
unit test fixing and api parameter addition
acwhite211 Dec 8, 2023
5125989
fix more unit tests
acwhite211 Dec 9, 2023
d40fb84
fix delete rank unit test
acwhite211 Dec 12, 2023
c08cbb8
fix call to set_fullnames
acwhite211 Dec 12, 2023
62e902e
add edit_ranks permission
acwhite211 Dec 12, 2023
e4b9dbb
fix openapi schema
acwhite211 Dec 13, 2023
024efa0
reconfigure HttpResponseBadRequest responses
acwhite211 Dec 13, 2023
d031ff5
change wrapper for rank edit functions
acwhite211 Dec 15, 2023
2dc4ba8
add fullname tests
acwhite211 Jan 2, 2024
0c6b825
Merge remote-tracking branch 'origin/production' into add_delete_tree…
CarolineDenis Mar 15, 2024
1ff3385
get tree by name or id
acwhite211 Mar 18, 2024
19bc954
Add a button to add new ranks in trees
CarolineDenis Mar 18, 2024
77223ec
add test of parents of child nodes
acwhite211 Mar 18, 2024
2625e45
Remove code
CarolineDenis Mar 18, 2024
0424b56
Merge branch 'add_delete_tree_rank_api' of https://github.com/specify…
acwhite211 Mar 18, 2024
67c61f6
add treeName to api schema
acwhite211 Mar 18, 2024
051af5c
retract required fields
acwhite211 Mar 18, 2024
b171bc8
Change variable name
CarolineDenis Mar 19, 2024
2b73d71
Lint code with ESLint and Prettier
CarolineDenis Mar 19, 2024
b70bbe0
add extra params to add_tree_rank api
acwhite211 Mar 19, 2024
24cb1db
Merge branch 'add_delete_tree_rank_api' of https://github.com/specify…
acwhite211 Mar 19, 2024
79fe45f
add business rules for adding/deleting trees
acwhite211 Mar 19, 2024
46cd29c
Refactor add rank feature
CarolineDenis Mar 20, 2024
a6c98e3
Revert unecessary cahnges
CarolineDenis Mar 20, 2024
7ec54c6
Use tree id
CarolineDenis Mar 20, 2024
1d6a189
Remove tree name
CarolineDenis Mar 20, 2024
f82eaaa
add tree def dependent fields to help deletion blocker
acwhite211 Mar 20, 2024
b2940ad
Merge branch 'add_delete_tree_rank_api' of https://github.com/specify…
acwhite211 Mar 20, 2024
f5173a6
treedefitems typo fix
acwhite211 Mar 20, 2024
77e53ae
Code improvement, deletion of previous logic for treeDefItems
CarolineDenis Mar 21, 2024
a045b1d
Lint code with ESLint and Prettier
CarolineDenis Mar 21, 2024
947d9d2
Remove addRank function
CarolineDenis Mar 21, 2024
62fbee2
fix tree dependent fields
acwhite211 Mar 21, 2024
0258dab
filter_rank_deletion_exception
acwhite211 Mar 21, 2024
29cbb77
fix delete_blockers filter
acwhite211 Mar 21, 2024
6693ab9
tree ranks bussiness rules init
acwhite211 Mar 21, 2024
e696d4e
raise exception for parent id not given
acwhite211 Mar 21, 2024
bd35e0f
test assign parent to treeResource
CarolineDenis Mar 21, 2024
cf67f55
Lint code with ESLint and Prettier
CarolineDenis Mar 21, 2024
907da38
Set parent on continue
CarolineDenis Mar 21, 2024
97139a2
Lint code with ESLint and Prettier
CarolineDenis Mar 21, 2024
5fcf3b4
param and exception fixes
acwhite211 Mar 21, 2024
173eeae
Assign parent to new rank with parent id
CarolineDenis Mar 22, 2024
f8c45a4
Remove import
CarolineDenis Mar 22, 2024
6d2c8a1
Lint code with ESLint and Prettier
CarolineDenis Mar 22, 2024
60977fa
Erase console log
CarolineDenis Mar 22, 2024
542b3fd
fix rankid error
acwhite211 Mar 22, 2024
1191f92
working tree rank tests using resource api
acwhite211 Mar 22, 2024
8eb677f
Simplify code and check for permissions
CarolineDenis Mar 25, 2024
ef98956
Lint code with ESLint and Prettier
CarolineDenis Mar 25, 2024
6a658f7
code cleanup, remove old view funciton
acwhite211 Mar 25, 2024
9538ec5
remove old urls
acwhite211 Mar 25, 2024
d64c7d0
revert change to the simple api unit test
acwhite211 Mar 25, 2024
d5b5f6c
fix delete with avant_delete()
acwhite211 Mar 25, 2024
d189e7f
cleanup old tree_views code
acwhite211 Mar 25, 2024
1ed635c
use is_instance_of_tree_def_item in tree_rules
acwhite211 Mar 25, 2024
691e798
fix treedef error
acwhite211 Mar 25, 2024
d549285
add_tree_rank description update
acwhite211 Mar 25, 2024
21a2361
naming change
CarolineDenis Mar 26, 2024
817b5bf
Lint code with ESLint and Prettier
CarolineDenis Mar 26, 2024
7ba0ac2
add orm_signal_handler_with_kwargs
acwhite211 Mar 26, 2024
4dbf9b6
Reload after rank deletion
CarolineDenis Mar 26, 2024
45eb9a8
Add root rank as default to add new rank
CarolineDenis Mar 26, 2024
a25fbd3
set_rank_id code cleanup
acwhite211 Mar 26, 2024
82d801f
fix workbench unit test exception
acwhite211 Mar 26, 2024
c41707c
simplify set_rank_id function params
acwhite211 Mar 26, 2024
c0559c0
pass treeDef to rank resource api call
acwhite211 Mar 27, 2024
05a8592
require treedef before adding rank
acwhite211 Mar 27, 2024
171562f
Add form to add rank componenent
CarolineDenis Mar 29, 2024
3a548c3
Remove prop
CarolineDenis Mar 29, 2024
a822430
abstract orm_signal_handler
acwhite211 Apr 1, 2024
ac04d0a
unify orm_signal_handler
acwhite211 Apr 1, 2024
3d0cac8
Merge branch 'add_delete_tree_rank_api' of https://github.com/specify…
acwhite211 Apr 1, 2024
816337a
tree_extras models import change
acwhite211 Apr 1, 2024
13a0d48
pre_constraints_delete
acwhite211 Apr 1, 2024
5906c01
verify_rank_parent_chain_integretity
acwhite211 Apr 2, 2024
d529551
typo fix
acwhite211 Apr 2, 2024
0a47ccf
Merge remote-tracking branch 'origin/production' into add_delete_tree…
CarolineDenis May 7, 2024
175144c
check rank item count
acwhite211 May 13, 2024
3d3a7f6
add to url.py file
acwhite211 May 15, 2024
1d76fa9
Display Delete Blocker when delete used rank
CarolineDenis May 15, 2024
83a9dd8
Define ranks in lower case
CarolineDenis May 16, 2024
e5e850b
Simplify parent child rank
CarolineDenis May 16, 2024
15a49ac
Remove unecessary code
CarolineDenis May 16, 2024
36585f4
Fix exclude id
CarolineDenis May 16, 2024
db81246
Remove check fro str
CarolineDenis May 16, 2024
e482c7c
Remove unecessary treedef
CarolineDenis May 16, 2024
c1c56b8
Remove unecessary code
CarolineDenis May 16, 2024
6b751a3
Remove unecessary default since expection raised above
CarolineDenis May 16, 2024
de41d27
Raise warning when treedef none
CarolineDenis May 16, 2024
962be5d
Chnge string
CarolineDenis May 16, 2024
381d30e
Fix if statement
CarolineDenis May 16, 2024
cd8ea33
Remove unecessary check
CarolineDenis May 16, 2024
59b88ea
Remove checks
CarolineDenis May 16, 2024
f8f9734
Remove unecessary warning raise
CarolineDenis May 16, 2024
4a3b8b8
Raise expection when rank ids none
CarolineDenis May 16, 2024
e4f8f2d
Remove warning
CarolineDenis May 16, 2024
c0d86e8
remove unused code
acwhite211 May 17, 2024
05c5e73
simplify tree rank code
acwhite211 May 17, 2024
005b271
remove leftover code
acwhite211 May 20, 2024
605e2b6
Merge branch 'production' into add_delete_tree_rank_api
acwhite211 May 20, 2024
9880e02
Lint code with ESLint and Prettier
acwhite211 May 20, 2024
20e66ac
create abstract TreeRank model for save and delete logic override
acwhite211 May 21, 2024
fb53e79
Merge branch 'production' into add_delete_tree_rank_api
acwhite211 May 21, 2024
87182f0
Merge remote-tracking branch 'origin/production' into add_delete_tree…
CarolineDenis May 21, 2024
cb7b862
Lint code with ESLint and Prettier
CarolineDenis May 21, 2024
86d1729
Merge branch 'production' into add_delete_tree_rank_api
acwhite211 May 22, 2024
3a92011
Merge branch 'production' into add_delete_tree_rank_api
acwhite211 May 22, 2024
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
73 changes: 69 additions & 4 deletions specifyweb/frontend/js_src/lib/components/TreeView/Tree.tsx
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import { userPreferences } from '../Preferences/userPreferences';
import type { Conformations, Row, Stats } from './helpers';
import { fetchStats } from './helpers';
import { TreeRow } from './Row';
import { commonText } from '../../localization/common';
import { Dialog } from '../Molecules/Dialog';
import { Form, Input, Label } from '../Atoms/Form';
import { Submit } from '../Atoms/Submit';
import { ping } from '../../utils/ajax/ping';
import { LoadingContext } from '../Core/Contexts';

const treeToPref = {
Geography: 'geography',
Expand Down Expand Up @@ -100,6 +106,25 @@ export function Tree<SCHEMA extends AnyTree>({
[baseUrl, statsThreshold]
);

const loading = React.useContext(LoadingContext);
const rankId = useId('add-tree-rank')('');
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
const [isAddingRank, setIsAddingRank] = React.useState(false);
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
const [newRankName, setNewRankName] = React.useState('');
function addRank(parentRankName: string): void {
const url = `/api/specify_tree/${tableName.toLowerCase()}/add_tree_rank/`;
loading(
ping(url, {
method: 'POST',
body: {
newRankName: newRankName,
parentRankName: parentRankName,
treeName: tableName.toLowerCase(),
},
}).then(() => globalThis.location.reload())
);
setIsAddingRank(false);
}

return (
<div
className={`
Expand Down Expand Up @@ -172,10 +197,50 @@ export function Tree<SCHEMA extends AnyTree>({
: rankName) as LocalizedString
}
</Button.LikeLink>
{isEditingRanks &&
collapsedRanks?.includes(rank.rankId) !== true ? (
<EditTreeRank rank={rank} />
) : undefined}
{isEditingRanks && (
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
<>
{collapsedRanks?.includes(rank.rankId) !== true ? (
<EditTreeRank rank={rank} />
) : undefined}
<Button.Icon
icon="plus"
title={treeText.addNewRank()}
onClick={() => setIsAddingRank(true)}
/>
</>
)}
{isAddingRank && (
<Dialog
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
buttons={
<>
<Button.DialogClose>
{commonText.cancel()}
</Button.DialogClose>
<Submit.Info form={rankId}>
{commonText.create()}
</Submit.Info>
</>
}
onClose={() => setIsAddingRank(false)}
header={treeText.addNewRank()}
>
<Form
id={rankId}
onSubmit={(): void => {
addRank(rankName);
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
}}
>
<Label.Block>
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
{treeText.newRankName()}
<Input.Text
required
value={newRankName}
onValueChange={setNewRankName}
/>
</Label.Block>
</Form>
</Dialog>
)}
</div>
);
})}
Expand Down
6 changes: 6 additions & 0 deletions specifyweb/frontend/js_src/lib/localization/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,4 +459,10 @@ export const treeText = createDictionary({
'uk-ua': 'Синхронізувати',
'ru-ru': 'Это приведет к безвозвратному удалению следующего ресурса',
},
addNewRank: {
'en-us': 'Add New Rank',
},
newRankName: {
'en-us': 'New Rank Name',
},
} as const);
237 changes: 237 additions & 0 deletions specifyweb/specify/test_trees.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import json
from django.test import Client
from specifyweb.specify import models
from specifyweb.specify.api_tests import ApiTests, get_table
from specifyweb.specify.tree_stats import get_tree_stats
from specifyweb.stored_queries.tests import SQLAlchemySetup
from .tree_extras import set_fullnames

class TestTreeSetup(ApiTests):
def setUp(self) -> None:
Expand Down Expand Up @@ -208,4 +211,238 @@ def test_counts_correctness(self):
for parent_id, correct in correct_results.items()
]

class AddDeleteRanksTest(ApiTests):
def setUp(self) -> None:
super().setUp()
acwhite211 marked this conversation as resolved.
Show resolved Hide resolved

def test_add_ranks_without_defaults(self):
c = Client()
c.force_login(self.specifyuser)

treedef_geo = models.Geographytreedef.objects.create(name='GeographyTest')

# Test adding non-default rank on empty heirarchy
response = c.post(
'/api/specify_tree/geography/add_tree_rank/',
data=json.dumps({
'newRankName': 'Universe',
'parentRankName': 'root',
'treeID': treedef_geo.id
}),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(100, models.Geographytreedefitem.objects.get(name='Universe').rankid)

# Test adding non-default rank to the end of the heirarchy
response = c.post(
'/api/specify_tree/geography/add_tree_rank/',
data=json.dumps({
'newRankName': 'Galaxy',
'parentRankName': 'Universe',
'treeID': treedef_geo.id
}),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(200, models.Geographytreedefitem.objects.get(name='Galaxy').rankid)

# Test adding non-default rank to the front of the heirarchy
response = c.post(
'/api/specify_tree/geography/add_tree_rank/',
data=json.dumps({
'newRankName': 'Multiverse',
'parentRankName': 'root',
'treeID': treedef_geo.id
}),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(50, models.Geographytreedefitem.objects.get(name='Multiverse').rankid)

# Test adding non-default rank in the middle of the heirarchy
response = c.post(
'/api/specify_tree/geography/add_tree_rank/',
data=json.dumps({
'newRankName': 'Dimension',
'parentRankName': 'Universe',
'treeID': treedef_geo.id
}),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(150, models.Geographytreedefitem.objects.get(name='Dimension').rankid)

# Test foreign keys
self.assertEqual(4, models.Geographytreedefitem.objects.filter(treedef=treedef_geo).count())

# Create test nodes
cfc = models.Geography.objects.create(name='Central Finite Curve', rankid=50, definition=treedef_geo,
definitionitem=models.Geographytreedefitem.objects.get(name='Multiverse'))
c137 = models.Geography.objects.create(name='C137', rankid=100, parent=cfc, definition=treedef_geo,
definitionitem=models.Geographytreedefitem.objects.get(name='Universe'))
d3 = models.Geography.objects.create(name='3D', rankid=150, parent=c137, definition=treedef_geo,
definitionitem=models.Geographytreedefitem.objects.get(name='Dimension'))
milky_way = models.Geography.objects.create(name='Milky Way', parent=d3, rankid=200, definition=treedef_geo,
definitionitem=models.Geographytreedefitem.objects.get(
name='Galaxy'))

# Test full name reconstruction
set_fullnames(treedef_geo, null_only=False, node_number_range=None)
if cfc.fullname is not None:
self.assertEqual('Central Finite Curve', cfc.fullname)
if c137.fullname is not None:
self.assertEqual('C137', c137.fullname)
if d3.fullname is not None:
self.assertEqual('3D', d3.fullname)
if milky_way.fullname is not None:
self.assertEqual('Milky Way', milky_way.fullname)

# Test parents of child nodes
self.assertEqual(cfc.id, c137.parent.id)
self.assertEqual(c137.id, d3.parent.id)
self.assertEqual(d3.id, milky_way.parent.id)


def test_add_ranks_with_defaults(self):
c = Client()
c.force_login(self.specifyuser)

treedef_taxon = models.Taxontreedef.objects.create(name='TaxonTest')

# Test adding default rank on empty heirarchy
response = c.post(
'/api/specify_tree/taxon/add_tree_rank/',
data=json.dumps({
'newRankName': 'Taxonomy Root',
'parentRankName': 'root',
'treeID': treedef_taxon.id
}),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(0, models.Taxontreedefitem.objects.get(name='Taxonomy Root').rankid)

# Test adding non-default rank in front of rank 0
response = c.post(
'/api/specify_tree/taxon/add_tree_rank/',
data=json.dumps({
'newRankName': 'Invalid',
'parentRankName': 'root',
'treeID': treedef_taxon.id
}),
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
self.assertEqual(0, models.Taxontreedefitem.objects.filter(name='Invalid').count())

# Test adding default rank to the end of the heirarchy
response = c.post(
'/api/specify_tree/taxon/add_tree_rank/',
data=json.dumps({
'newRankName': 'Division',
'parentRankName': 'Taxonomy Root',
'treeID': treedef_taxon.id
}),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(30, models.Taxontreedefitem.objects.get(name='Division').rankid)

# Test adding default rank to the middle of the heirarchy
response = c.post(
'/api/specify_tree/taxon/add_tree_rank/',
data=json.dumps({
'newRankName': 'Kingdom',
'parentRankName': 'Taxonomy Root',
'treeID': treedef_taxon.id
}),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(10, models.Taxontreedefitem.objects.get(name='Kingdom').rankid)
self.assertEqual(models.Taxontreedefitem.objects.get(name='Division').parent.id,
models.Taxontreedefitem.objects.get(name='Kingdom').id)
self.assertEqual(models.Taxontreedefitem.objects.get(name='Kingdom').parent.id,
models.Taxontreedefitem.objects.get(name='Taxonomy Root').id)

# Test foreign keys
for rank in models.Taxontreedefitem.objects.all():
self.assertEqual(treedef_taxon.id, rank.treedef.id)

# Create test nodes
pokemon = models.Taxon.objects.create(name='Pokemon', rankid=50, definition=treedef_taxon,
definitionitem=models.Taxontreedefitem.objects.get(name='Taxonomy Root'))
water = models.Taxon.objects.create(name='Water', rankid=100, parent=pokemon, definition=treedef_taxon,
definitionitem=models.Taxontreedefitem.objects.get(name='Kingdom'))
squirtle = models.Taxon.objects.create(name='Squirtle', rankid=150, parent=water, definition=treedef_taxon,
definitionitem=models.Taxontreedefitem.objects.get(name='Division'))
blastoise = models.Taxon.objects.create(name='Blastoise', parent=water, rankid=200, definition=treedef_taxon,
definitionitem=models.Taxontreedefitem.objects.get(name='Division'))

# Test full name reconstruction
set_fullnames(treedef_taxon, null_only=False, node_number_range=None)
if pokemon.fullname is not None:
self.assertEqual('Pokemon', pokemon.fullname)
if water.fullname is not None:
self.assertEqual('Water', water.fullname)
if squirtle.fullname is not None:
self.assertEqual('Squirtle', squirtle.fullname)
if blastoise.fullname is not None:
self.assertEqual('Blastoise', blastoise.fullname)

def test_delete_ranks(self):
c = Client()
c.force_login(self.specifyuser)

treedef_geotimeperiod = models.Geologictimeperiodtreedef.objects.create(name='GeographyTimePeriodTest')
era_ranks = models.Geologictimeperiodtreedefitem.objects.create(
name='Era',
rankid=100,
treedef=treedef_geotimeperiod
)
period_rank = models.Geologictimeperiodtreedefitem.objects.create(
name='Period',
rankid=200,
treedef=treedef_geotimeperiod,
parent=era_ranks
)
epoch_rank = models.Geologictimeperiodtreedefitem.objects.create(
name='Epoch',
rankid=300,
treedef=treedef_geotimeperiod,
parent=period_rank
)
age_rank = models.Geologictimeperiodtreedefitem.objects.create(
name='Age',
rankid=400,
treedef=treedef_geotimeperiod,
parent=epoch_rank
)

# Test deleting a rank in the middle of the heirarchy
response = c.post(
'/api/specify_tree/geologictimeperiod/delete_tree_rank/',
data= json.dumps({'rankName': 'Epoch', 'treeID': treedef_geotimeperiod.id}),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(None, models.Geologictimeperiodtreedefitem.objects.filter(name='Epoch').first())
self.assertEqual(period_rank.id, models.Geologictimeperiodtreedefitem.objects.get(name='Age').parent.id)

# Test deleting a rank at the end of the heirarchy
response = c.post(
'/api/specify_tree/geologictimeperiod/delete_tree_rank/',
data= json.dumps({'rankName': 'Age', 'treeID': treedef_geotimeperiod.id}),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(None, models.Geologictimeperiodtreedefitem.objects.filter(name='Age').first())

# Test deleting a rank at the head of the heirarchy
response = c.post(
'/api/specify_tree/geologictimeperiod/delete_tree_rank/',
data= json.dumps({'rankName': 'Era', 'treeID': treedef_geotimeperiod.id}),
content_type='application/json'
)
self.assertEqual(response.status_code, 500)
Loading
Loading