diff --git a/ifcbdb/common/constants.py b/ifcbdb/common/constants.py index 4ddbea93..91970b15 100644 --- a/ifcbdb/common/constants.py +++ b/ifcbdb/common/constants.py @@ -17,4 +17,6 @@ class BinManagementActions(Enum): UNASSIGN_DATASET = "unassign-dataset" # Metadata column names -BIN_ID_COLUMNS = ['id','pid','lid','bin','bin_id','sample','sample_id','filename'] \ No newline at end of file +BIN_ID_COLUMNS = ['id','pid','lid','bin','bin_id','sample','sample_id','filename'] +ADD_DATASET_COLUMNS = ['add_dataset', 'adddataset'] +REMOVE_DATASET_COLUMNS = ['remove_dataset', 'removedataset', 'delete_dataset', 'deletedataset'] \ No newline at end of file diff --git a/ifcbdb/dashboard/accession.py b/ifcbdb/dashboard/accession.py index 444f218e..f680cbc1 100644 --- a/ifcbdb/dashboard/accession.py +++ b/ifcbdb/dashboard/accession.py @@ -13,7 +13,8 @@ import pandas as pd import numpy as np -from .models import Bin, DataDirectory, Instrument, Timeline, Dataset, normalize_tag_name, Team, TeamDataset +from .models import Bin, DataDirectory, Instrument, Timeline, Dataset, Tag, TagEvent, \ + normalize_tag_name, Team, TeamDataset from .qaqc import check_bad, check_no_rois import ifcb @@ -312,6 +313,10 @@ def import_metadata(metadata_dataframe, progress_callback=do_nothing): CAST_COLUMNS = ['cast'] NISKIN_COLUMNS = ['niskin','bottle'] SAMPLE_TYPE_COLUMNS = ['sampletype','sample_type'] + ADD_DATASET_COLUMNS = ['add_dataset', 'adddataset'] + REMOVE_DATASET_COLUMNS = ['remove_dataset', 'removedataset', 'delete_dataset', 'deletedataset'] + ADD_TAG_COLUMNS = ['add_tag', 'addtag'] + REMOVE_TAG_COLUMNS = ['remove_tag', 'removetag', 'delete_tag', 'deletetag'] SKIP_POSITIVE_VALUES = ['skip','yes','y','true','t','1'] SKIP_NEGATIVE_VALUES = ['noskip','no','n','false','f','0'] @@ -361,6 +366,11 @@ def get_cell(named_tup, key): sample_type_col = get_column(df, SAMPLE_TYPE_COLUMNS) + add_dataset_col = get_column(df, ADD_DATASET_COLUMNS) + remove_dataset_col = get_column(df, REMOVE_DATASET_COLUMNS) + add_tag_col = get_column(df, ADD_TAG_COLUMNS) + remove_tag_col = get_column(df, REMOVE_TAG_COLUMNS) + tag_cols = [] for c in df.columns: if c.startswith('tag'): @@ -467,6 +477,43 @@ def get_cell(named_tup, key): if body is not None: b.add_comment(body, skip_duplicates=True) + # command columns + + if add_dataset_col is not None: + dataset_name = get_cell(row, add_dataset_col) + if str(dataset_name or "").strip(): + try: + dataset = Dataset.objects.get(name__iexact=dataset_name) + b.datasets.add(dataset) + except Dataset.DoesNotExist: + raise ValueError(f"Dataset '{dataset_name}' not found for {add_dataset_col}") + + if remove_dataset_col is not None: + dataset_name = get_cell(row, remove_dataset_col) + if str(dataset_name or "").strip(): + try: + dataset = Dataset.objects.get(name__iexact=dataset_name) + b.datasets.remove(dataset) + except Dataset.DoesNotExist: + pass + + if add_tag_col is not None: + tag = get_cell(row, add_tag_col) + if str(tag or "").strip(): + if re.match(r"^[0-9\.]+$", str(tag)): + raise ValueError(f"tag '{tag}' consists of only digits") + normalized = normalize_tag_name(str(tag)) + b.add_tag(normalized) + + if remove_tag_col is not None: + tag = get_cell(row, remove_tag_col) + if str(tag or "").strip(): + normalized = normalize_tag_name(str(tag)) + try: + b.delete_tag(normalized) + except (Tag.DoesNotExist, TagEvent.DoesNotExist): + pass + # skip flag if skip_col is not None: @@ -597,6 +644,10 @@ def add(field, rename=None): r['comment_summary'].append(comment_summary_by_id.get(item['id'], '')) r['trigger_selection'].append(trigger_selection_by_id.get(item['id'], '')) r['skip'].append(1 if item['skip'] else 0) + r['add_dataset'].append('') + r['remove_dataset'].append('') + r['add_tag'].append('') + r['remove_tag'].append('') df = pd.DataFrame(r) diff --git a/ifcbdb/secure/views.py b/ifcbdb/secure/views.py index 11c6c668..66d4fe8a 100644 --- a/ifcbdb/secure/views.py +++ b/ifcbdb/secure/views.py @@ -20,7 +20,7 @@ from dashboard.accession import export_metadata from common import auth -from common.constants import Features, TeamRoles, BinManagementActions, BIN_ID_COLUMNS +from common.constants import Features, TeamRoles, BinManagementActions, BIN_ID_COLUMNS, ADD_DATASET_COLUMNS, REMOVE_DATASET_COLUMNS from django.core.cache import cache @@ -869,6 +869,27 @@ def get_column(df, possible_names): bins_ids = list(bins_ids) df = df[df[pid_col].isin(bins_ids)] + + # Blank out any dataset names that are not associated with the user uploading the data to + # ensure they cannot assign or unassign bins to datasets they are not privy to + add_dataset_col = get_column(df, ADD_DATASET_COLUMNS) + remove_dataset_col = get_column(df, REMOVE_DATASET_COLUMNS) + + if add_dataset_col is not None or remove_dataset_col is not None: + valid_dataset_names = auth \ + .get_associated_datasets(request.user) \ + .values_list("name", flat=True) + + if add_dataset_col is not None: + df[add_dataset_col] = df[add_dataset_col].apply( + lambda x: x if pd.isna(x) or str(x).strip() in valid_dataset_names else "" + ) + + if remove_dataset_col is not None: + df[remove_dataset_col] = df[remove_dataset_col].apply( + lambda x: x if pd.isna(x) or str(x).strip() in valid_dataset_names else "" + ) + json_df = df.to_json() added = cache.add(METADATA_UPLOAD_LOCK_KEY, True, timeout=None) # this is atomic diff --git a/ifcbdb/templates/secure/upload-metadata.html b/ifcbdb/templates/secure/upload-metadata.html index 691eceb5..8d8d2597 100644 --- a/ifcbdb/templates/secure/upload-metadata.html +++ b/ifcbdb/templates/secure/upload-metadata.html @@ -49,6 +49,22 @@