diff --git a/src/mavedb/lib/mave/utils.py b/src/mavedb/lib/mave/utils.py index f59a3fca0..dd6b75916 100644 --- a/src/mavedb/lib/mave/utils.py +++ b/src/mavedb/lib/mave/utils.py @@ -1,5 +1,7 @@ import re +import pandas as pd + NA_VALUE = "NA" NULL_VALUES = ("", "na", "nan", "nil", "none", "null", "n/a", "undefined", NA_VALUE) @@ -22,6 +24,9 @@ def is_csv_null(value): """Return True if a string from a CSV file represents a NULL value.""" + # Avoid any boolean miscasts from comparisons by handling NA types up front. + if pd.isna(value): + return True # Number 0 is treated as False so that all 0 will be converted to NA value. if value == 0: return value diff --git a/src/mavedb/lib/permissions.py b/src/mavedb/lib/permissions.py index 972b91be1..1b2f966c8 100644 --- a/src/mavedb/lib/permissions.py +++ b/src/mavedb/lib/permissions.py @@ -95,8 +95,10 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) -> elif private: # Do not acknowledge the existence of a private entity. return PermissionResponse(False, 404, f"experiment set with URN '{item.urn}' not found") + elif user_data is None or user_data.user is None: + return PermissionResponse(False, 401, f"insufficient permissions for URN '{item.urn}'") else: - return PermissionResponse(False) + return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'") elif action == Action.UPDATE: if user_may_edit: return PermissionResponse(True) @@ -106,8 +108,10 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) -> elif private: # Do not acknowledge the existence of a private entity. return PermissionResponse(False, 404, f"experiment set with URN '{item.urn}' not found") + elif user_data is None or user_data.user is None: + return PermissionResponse(False, 401, f"insufficient permissions for URN '{item.urn}'") else: - return PermissionResponse(False) + return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'") elif action == Action.DELETE: # Owner may only delete an experiment set if it has not already been published. if user_may_edit: @@ -143,8 +147,10 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) -> elif private: # Do not acknowledge the existence of a private entity. return PermissionResponse(False, 404, f"experiment with URN '{item.urn}' not found") + elif user_data is None or user_data.user is None: + return PermissionResponse(False, 401, f"insufficient permissions for URN '{item.urn}'") else: - return PermissionResponse(False) + return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'") elif action == Action.UPDATE: if user_may_edit: return PermissionResponse(True) @@ -154,8 +160,10 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) -> elif private: # Do not acknowledge the existence of a private entity. return PermissionResponse(False, 404, f"experiment with URN '{item.urn}' not found") + elif user_data is None or user_data.user is None: + return PermissionResponse(False, 401, f"insufficient permissions for URN '{item.urn}'") else: - return PermissionResponse(False) + return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'") elif action == Action.DELETE: # Owner may only delete an experiment if it has not already been published. if user_may_edit: @@ -191,8 +199,10 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) -> elif private: # Do not acknowledge the existence of a private entity. return PermissionResponse(False, 404, f"score set with URN '{item.urn}' not found") + elif user_data is None or user_data.user is None: + return PermissionResponse(False, 401, f"insufficient permissions for URN '{item.urn}'") else: - return PermissionResponse(False) + return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'") elif action == Action.UPDATE: if user_may_edit: return PermissionResponse(True) @@ -202,8 +212,10 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) -> elif private: # Do not acknowledge the existence of a private entity. return PermissionResponse(False, 404, f"score set with URN '{item.urn}' not found") + elif user_data is None or user_data.user is None: + return PermissionResponse(False, 401, f"insufficient permissions for URN '{item.urn}'") else: - return PermissionResponse(False) + return PermissionResponse(False, 403, f"insufficient permissions for URN '{item.urn}'") elif action == Action.DELETE: # Owner may only delete a score set if it has not already been published. if user_may_edit: @@ -247,7 +259,7 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) -> elif roles_permitted(active_roles, [UserRole.admin]): return PermissionResponse(True) else: - return PermissionResponse(False) + return PermissionResponse(False, 403, "Insufficient permissions for user update.") elif action == Action.UPDATE: if user_is_self: return PermissionResponse(True) diff --git a/src/mavedb/routers/score_sets.py b/src/mavedb/routers/score_sets.py index 24c602ca6..44fd2a307 100644 --- a/src/mavedb/routers/score_sets.py +++ b/src/mavedb/routers/score_sets.py @@ -854,9 +854,13 @@ async def update_score_set( scores_data = pd.DataFrame( variants_to_csv_rows(item.variants, columns=score_columns, dtype="score_data") ).replace("NA", pd.NA) - count_data = pd.DataFrame( - variants_to_csv_rows(item.variants, columns=count_columns, dtype="count_data") - ).replace("NA", pd.NA) + + if item.dataset_columns["count_columns"]: + count_data = pd.DataFrame( + variants_to_csv_rows(item.variants, columns=count_columns, dtype="count_data") + ).replace("NA", pd.NA) + else: + count_data = None # Although this is also updated within the variant creation job, update it here # as well so that we can display the proper UI components (queue invocation delay diff --git a/src/mavedb/routers/statistics.py b/src/mavedb/routers/statistics.py index 53a3e8fc0..29445fe2d 100644 --- a/src/mavedb/routers/statistics.py +++ b/src/mavedb/routers/statistics.py @@ -332,6 +332,11 @@ def record_object_statistics( Model names and fields should be members of the Enum classes defined above. Providing an invalid model name or model field will yield a 422 Unprocessable Entity error with details about valid enum values. """ + # Validation to ensure 'keywords' is only used with 'experiment'. + if model == RecordNames.scoreSet and field == RecordFields.keywords: + raise HTTPException(status_code=422, + detail="The 'keywords' field can only be used with the 'experiment' model.") + count_data = _record_from_field_and_model(db, model, field) return {field_val: count for field_val, count in count_data if field_val is not None}