Skip to content

Commit ae1690e

Browse files
committed
Use tree mapping for rank count
Accept mapping directly instead of just mappingUrl
1 parent 5329412 commit ae1690e

File tree

2 files changed

+82
-21
lines changed

2 files changed

+82
-21
lines changed

specifyweb/backend/trees/utils.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def initialize_default_tree(tree_type: str, discipline, tree_name: str, rank_nam
126126
tree_rank_model(
127127
treedef=tree_def,
128128
name=rank_name, # TODO: allow rank name configuration
129+
title=rank_name.capitalize(),
129130
rankid=int(rank_id),
130131
)
131132
)
@@ -176,9 +177,12 @@ def add_default_tree_record(tree_type: str, discipline, row: dict, tree_name: st
176177
defaults[model_field] = v
177178

178179
treedef_item, _ = tree_rank_model.objects.get_or_create(
179-
name=rank.capitalize(),
180+
name=rank,
180181
treedef=tree_def,
181-
rankid=rank_id
182+
rankid=rank_id,
183+
defaults={
184+
'title': rank.capitalize()
185+
}
182186
)
183187

184188
obj = tree_node_model.objects.filter(
@@ -210,8 +214,8 @@ def add_default_tree_record(tree_type: str, discipline, row: dict, tree_name: st
210214
rank_id += 10
211215

212216
@app.task(base=LogErrorsTask, bind=True)
213-
def create_default_tree_task(self, url: str, discipline_id: int, tree_discipline_name: str, rank_count: int, specify_collection_id: int,
214-
specify_user_id: int, mapping_url: str, row_count: Optional[int], tree_name: str):
217+
def create_default_tree_task(self, url: str, discipline_id: int, tree_discipline_name: str, specify_collection_id: int,
218+
specify_user_id: int, tree_cfg: dict, row_count: Optional[int], tree_name: str):
215219
logger.info(f'starting task {str(self.request.id)}')
216220

217221
specify_user = spmodels.Specifyuser.objects.get(id=specify_user_id)
@@ -248,12 +252,7 @@ def set_tree(name: str) -> None:
248252
# non-taxon tree
249253
tree_type = tree_discipline_name
250254

251-
try:
252-
resp = requests.get(mapping_url)
253-
resp.raise_for_status()
254-
tree_cfg = resp.json()
255-
except Exception:
256-
raise
255+
rank_count = len(tree_cfg['ranks'])
257256

258257
total_rows = 0
259258
if row_count:
@@ -321,11 +320,11 @@ def lines_iter() -> Iterator[str]:
321320
if new_line_index == -1: break
322321
line = buffer[:new_line_index + 1] # extract line
323322
buffer = buffer[new_line_index + 1 :] # clear read buffer
324-
yield line.decode('utf-8', errors='replace')
323+
yield line.decode('utf-8-sig', errors='replace')
325324

326325
if buffer:
327326
# yield last line
328-
yield buffer.decode('utf-8', errors='replace')
327+
yield buffer.decode('utf-8-sig', errors='replace')
329328
return
330329
except (ChunkedEncodingError, ConnectionError) as e:
331330
# Trigger retry
@@ -341,6 +340,8 @@ def lines_iter() -> Iterator[str]:
341340

342341
rank_names_lst = reader.fieldnames[:rank_count]
343342
rank_names_lst.insert(0, "Root") # Add Root rank
343+
logger.debug(f"################ CREATING TREE WITH THE FOLLOWING {rank_count} RANKS ###############")
344+
logger.debug(rank_names_lst)
344345
tree_name = initialize_default_tree(tree_type, discipline, initial_tree_name, rank_names_lst)
345346
set_tree(tree_name)
346347

specifyweb/backend/trees/views.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from specifyweb.specify.api.serializers import obj_to_data, toJson
1717
from sqlalchemy import select, func, distinct, literal
1818
from sqlalchemy.orm import aliased
19+
from jsonschema import validate # type: ignore
20+
from jsonschema.exceptions import ValidationError # type: ignore
1921

2022
from specifyweb.middleware.general import require_GET
2123
from specifyweb.backend.businessrules.exceptions import BusinessRuleException
@@ -556,6 +558,39 @@ def has_tree_read_permission(tree: TREE_TABLE) -> bool:
556558
# update = PermissionTargetAction()
557559
# delete = PermissionTargetAction()
558560

561+
# Schema definition for the mapping file that is used in default tree creation.
562+
DEFAULT_TREE_MAPPING_SCHEMA = {
563+
"title": "Tree column mapping for default trees",
564+
"description": "The mapping of the CSV columns for default tree creation.",
565+
"$schema": "http://json-schema.org/schema#",
566+
"type": "object",
567+
"properties": {
568+
"all_columns": {
569+
"description": "A list of all the column header names contained in the CSV. The first columns names should correspond the number of ranks defined in this schema.",
570+
"type": "array",
571+
"items": {
572+
"type": "string"
573+
}
574+
},
575+
"ranks": {
576+
"description": "An ordered list containing all the ranks to be created.",
577+
"type": "array",
578+
"minItems": 1,
579+
"items": {
580+
"description": "A rank's mapping definition.",
581+
"type": "object",
582+
"additionalProperties": {
583+
"description": "Mapping of CSV column names to the rank's field names.",
584+
"type": "object",
585+
"additionalProperties": {
586+
"type": "string"
587+
}
588+
}
589+
}
590+
}
591+
},
592+
"required": ["ranks"]
593+
}
559594
@openapi(schema={
560595
"post": {
561596
"requestBody": {
@@ -594,10 +629,6 @@ def has_tree_read_permission(tree: TREE_TABLE) -> bool:
594629
"type": "string",
595630
"description": "The URL of the tree CSV file."
596631
},
597-
"mappingUrl": {
598-
"type": "string",
599-
"description": "The URL of a JSON file describing the column mapping of the CSV data."
600-
},
601632
"treeName": {
602633
"type": "string",
603634
"description": "The name to be used by the new tree.",
@@ -615,7 +646,23 @@ def has_tree_read_permission(tree: TREE_TABLE) -> bool:
615646
"description": "The total number of rows contained in the CSV file. Only used for progress tracking."
616647
},
617648
},
618-
"required": ["url", "mappingUrl", "disciplineName"],
649+
"required": ["url", "disciplineName"],
650+
"oneOf": [
651+
{"required": ["mapping"],
652+
"properties": {
653+
"mapping": {
654+
"type": "object",
655+
"description": "An object describing the column mapping of the CSV data."
656+
},
657+
}},
658+
{"required": ["mappingUrl"],
659+
"properties": {
660+
"mappingUrl": {
661+
"type": "string",
662+
"description": "The URL of a JSON file describing the column mapping of the CSV data."
663+
},
664+
}}
665+
]
619666
},
620667
"SuccessBackground": {
621668
"type": "object",
@@ -666,26 +713,39 @@ def create_default_tree_view(request):
666713
discipline = spmodels.Discipline.objects.all().first()
667714

668715
url = data.get('url', None)
669-
mapping_url = data.get('mappingUrl', None)
670716

671717
tree_rank_model_name = tree_discipline_name.capitalize()
672-
rank_count = int(tree_rank_count(tree_rank_model_name, 8))
673718
tree_name = data.get('treeName', tree_rank_model_name)
674719

675720
row_count = data.get('rowCount', None)
676721

677722
if not url:
678723
return http.JsonResponse({'error': 'Tree not found.'}, status=404)
724+
725+
# CSV mapping. Accept the mapping directly or a url to a JSON file containing the mapping.
726+
tree_cfg = data.get('mapping', None)
727+
mapping_url = data.get('mappingUrl', None)
728+
if mapping_url:
729+
try:
730+
resp = requests.get(mapping_url)
731+
resp.raise_for_status()
732+
tree_cfg = resp.json()
733+
except Exception:
734+
return http.JsonResponse({'error': f'Could not retrieve default tree mapping from {mapping_url}.'}, status=404)
735+
try:
736+
validate(tree_cfg, DEFAULT_TREE_MAPPING_SCHEMA)
737+
except ValidationError as e:
738+
return http.JsonResponse({'error': f'Default tree mapping is invalid: {e}'}, status=400)
679739

680740
Message.objects.create(user=request.specify_user, content=json.dumps({
681741
'type': 'create-default-tree-starting',
682-
'name': tree_rank_model_name,
742+
'name': tree_name,
683743
'collection_id': request.specify_collection.id,
684744
}))
685745

686746
task_id = str(uuid4())
687747
async_result = create_default_tree_task.apply_async(
688-
args=[url, discipline.id, tree_discipline_name, rank_count, request.specify_collection.id, request.specify_user.id, mapping_url, row_count, tree_name],
748+
args=[url, discipline.id, tree_discipline_name, request.specify_collection.id, request.specify_user.id, tree_cfg, row_count, tree_name],
689749
task_id=f"create_default_tree_{tree_discipline_name}_{task_id}",
690750
taskid=task_id
691751
)

0 commit comments

Comments
 (0)