diff --git a/.secrets b/.secrets new file mode 100644 index 00000000..e69de29b diff --git a/biocompute/apis.py b/biocompute/apis.py index 94b01f22..ea5bd1a7 100644 --- a/biocompute/apis.py +++ b/biocompute/apis.py @@ -4,19 +4,34 @@ """BioCompute Object APIs """ +from biocompute.services import ( + BcoDraftSerializer, + BcoValidator, + ModifyBcoDraftSerializer, + publish_draft, + bco_counter_increment +) +from biocompute.selectors import ( + retrieve_bco, + user_can_modify_bco, + user_can_publish_bco, + object_id_deconstructor, +) +from config.services import ( + legacy_api_converter, + response_constructor, + response_status, +) from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from django.conf import settings from django.db import utils -from rest_framework.views import APIView +from prefix.selectors import user_can_draft_prefix from rest_framework import status -from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from tests.fixtures.example_bco import BCO_000001 -from config.services import legacy_api_converter, response_constructor -from biocompute.services import BcoDraftSerializer, bco_counter_increment, ModifyBcoDraftSerializer -from biocompute.selectors import retrieve_bco, user_can_modify_bco -from prefix.selectors import user_can_draft hostname = settings.PUBLIC_HOSTNAME @@ -93,7 +108,7 @@ def post(self, request) -> Response: for index, object in enumerate(data): response_id = object.get("object_id", index) bco_prefix = object.get("prefix", index) - prefix_permitted = user_can_draft(owner, bco_prefix) + prefix_permitted = user_can_draft_prefix(owner, bco_prefix) if prefix_permitted is None: response_data.append(response_constructor( @@ -147,23 +162,131 @@ def post(self, request) -> Response: )) rejected_requests = True - if accepted_requests is False and rejected_requests == True: - return Response( - status=status.HTTP_400_BAD_REQUEST, - data=response_data - ) + status_code = response_status(accepted_requests, rejected_requests) + return Response(status=status_code, data=response_data) + +class DraftsModifyApi(APIView): + """Modify BCO Draft [Bulk Enabled] + + API endpoint for modifying BioCompute Object (BCO) drafts, with support + for bulk operations. + + This endpoint allows authenticated users to modify existing BCO drafts + individually or in bulk by submitting a list of BCO drafts. The operation + can be performed for one or more drafts in a single request. Each draft is + validated and processed independently, allowing for mixed response + statuses (HTTP_207_MULTI_STATUS) in the case of bulk submissions. + + NOTE: If a list of `authorized_users` is provided, this method replaces + the current list of authorized users with the new list, allowing for + dynamic access control to the BCO. Users not included in the new list will + lose their access unless they are the owner or have other permissions. + """ + + permission_classes = [IsAuthenticated,] + + @swagger_auto_schema( + operation_id="api_objects_drafts_modify", + request_body=openapi.Schema( + type=openapi.TYPE_ARRAY, + title="Modify BCO Draft Schema", + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=[], + properties={ + "authorized_users": openapi.Schema( + type=openapi.TYPE_ARRAY, + description="Users which can access the BCO draft.", + items=openapi.Schema(type=openapi.TYPE_STRING, + example="tester") + ), + "contents": openapi.Schema( + type=openapi.TYPE_OBJECT, + description="Contents of the BCO.", + example=BCO_000001 + ), + }, + ), + description="Modify BCO draft [Bulk Enabled].", + ), + responses={ + 200: "All requests were accepted.", + 207: "Some requests failed and some succeeded. Each object submitted" + " will have it's own response object with it's own status" + " code and message.\n", + 400: "All requests were rejected.", + 403: "Invalid token.", + }, + tags=["BCO Management"], + ) + + def post(self, request) -> Response: + response_data = [] + requester = request.user + data = request.data + rejected_requests = False + accepted_requests = False + if 'POST_api_objects_drafts_modify' in request.data: + data = legacy_api_converter(request.data) - if accepted_requests is True and rejected_requests is True: - return Response( - status=status.HTTP_207_MULTI_STATUS, - data=response_data - ) + for index, object in enumerate(data): + response_id = object.get("object_id", index) + modify_permitted = user_can_modify_bco(response_id, requester) + + if modify_permitted is None: + response_data.append(response_constructor( + identifier=response_id, + status = "NOT FOUND", + code= 404, + message= f"Invalid BCO: {response_id}.", + )) + rejected_requests = True + continue - if accepted_requests is True and rejected_requests is False: - return Response( - status=status.HTTP_200_OK, - data=response_data - ) + if modify_permitted is False: + response_data.append(response_constructor( + identifier=response_id, + status = "FORBIDDEN", + code= 400, + message= f"User, {requester}, does not have draft permissions"\ + + f" for BCO {response_id}.", + )) + rejected_requests = True + continue + + bco = ModifyBcoDraftSerializer(data=object) + + if bco.is_valid(): + try: + bco.update(bco.validated_data) + response_data.append(response_constructor( + identifier=response_id, + status = "SUCCESS", + code= 200, + message= f"BCO {response_id} updated", + )) + accepted_requests = True + + except Exception as err: + response_data.append(response_constructor( + identifier=response_id, + status = "SERVER ERROR", + code= 500, + message= f"BCO {response_id} failed", + )) + + else: + response_data.append(response_constructor( + identifier=response_id, + status = "REJECTED", + code= 400, + message= f"BCO {response_id} rejected", + data=bco.errors + )) + rejected_requests = True + + status_code = response_status(accepted_requests, rejected_requests) + return Response(status=status_code, data=response_data) class DraftsModifyApi(APIView): """Modify BCO Draft [Bulk Enabled] @@ -355,6 +478,156 @@ def get(self, request, bco_accession): bco_counter_increment(bco_instance) return Response(status=status.HTTP_200_OK, data=bco_instance.contents) +class DraftsPublishApi(APIView): + """Publish Draft BCO [Bulk Enabled] + + API endpoint for publishing BioCompute Object (BCO) drafts, with support + for bulk operations. + + This endpoint allows authenticated users to publish existing BCO drafts + individually or in bulk by submitting a list of BCO drafts. The operation + can be performed for one or more drafts in a single request. Each draft is + validated and processed independently, allowing for mixed response + statuses (HTTP_207_MULTI_STATUS) in the case of bulk submissions. + """ + + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_id="api_objects_drafts_publish", + request_body=openapi.Schema( + type=openapi.TYPE_ARRAY, + title="Publish BCO Draft Schema", + description="Publish draft BCO [Bulk Enabled]", + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=["object_id"], + properties={ + "published_object_id": openapi.Schema( + type=openapi.TYPE_STRING, + description="BCO Object Draft ID.", + example="http://127.0.0.1:8000/TEST_000001/1.0" + ), + "object_id": openapi.Schema( + type=openapi.TYPE_STRING, + description="BCO Object ID to use for published object.", + example="http://127.0.0.1:8000/TEST_000001/DRAFT" + ), + "delete_draft": openapi.Schema( + type=openapi.TYPE_BOOLEAN, + description="Whether or not to delete the draft."\ + +" False by default.", + example=False + ), + } + ) + ), + responses={ + 200: "All requests were accepted.", + 207: "Some requests failed and some succeeded. Each object submitted" + " will have it's own response object with it's own status" + " code and message.\n", + 400: "All requests were rejected.", + 403: "Invalid token.", + }, + tags=["BCO Management"], + ) + + def post(self, request) -> Response: + validator = BcoValidator() + response_data = [] + requester = request.user + data = request.data + rejected_requests = False + accepted_requests = False + if 'POST_api_objects_drafts_publish' in request.data: + data = legacy_api_converter(request.data) + + for index, object in enumerate(data): + response_id = object.get("object_id", index) + bco_instance = user_can_publish_bco(object, requester) + + if bco_instance is None: + response_data.append(response_constructor( + identifier=response_id, + status = "NOT FOUND", + code= 404, + message= f"Invalid BCO: {response_id} does not exist.", + )) + rejected_requests = True + continue + + if bco_instance is False: + response_data.append(response_constructor( + identifier=response_id, + status = "FORBIDDEN", + code= 403, + message= f"User, {requester}, does not have draft permissions"\ + + f" for BCO {response_id}.", + )) + rejected_requests = True + continue + + if type(bco_instance) is str: + response_data.append(response_constructor( + identifier=response_id, + status = "BAD REQUEST", + code= 400, + message= bco_instance + )) + rejected_requests = True + continue + + if type(bco_instance) is tuple: + response_data.append(response_constructor( + identifier=response_id, + status = "BAD REQUEST", + code= 400, + message= f"Invalid `published_object_id`."\ + + f"{bco_instance[0]} and {bco_instance[1]}"\ + + " do not match.", + )) + rejected_requests = True + continue + + if bco_instance.state == 'PUBLISHED': + object_id = bco_instance.object_id + response_data.append(response_constructor( + identifier=response_id, + status = "CONFLICT", + code= 409, + message= f"Invalid `object_id`: {object_id} already"\ + + " exists.", + )) + rejected_requests = True + continue + + bco_results = validator.parse_and_validate(bco_instance.contents) + for identifier, result in bco_results.items(): + if result["number_of_errors"] > 0: + response_data.append(response_constructor( + identifier=response_id, + status = "REJECTED", + code= 400, + message= f"Publishing BCO {response_id} rejected", + data=bco_results + )) + rejected_requests = True + + else: + published_bco = publish_draft(bco_instance, requester, object) + identifier=published_bco.object_id + response_data.append(response_constructor( + identifier=identifier, + status = "SUCCESS", + code= 201, + message= f"BCO {identifier} has been published.", + )) + accepted_requests = True + + status_code = response_status(accepted_requests, rejected_requests) + return Response(status=status_code, data=response_data) + class PublishedRetrieveApi(APIView): """Get Published BCO @@ -374,7 +647,10 @@ class PublishedRetrieveApi(APIView): - `bco_version`: Specifies the version of the BCO to be retrieved. """ - + + authentication_classes = [] + permission_classes = [AllowAny] + @swagger_auto_schema( operation_id="api_get_published", manual_parameters=[ diff --git a/biocompute/selectors.py b/biocompute/selectors.py index 95147f5e..3a0fe3f8 100644 --- a/biocompute/selectors.py +++ b/biocompute/selectors.py @@ -5,28 +5,190 @@ Functions to query the database related to BioCompute Objects """ +import pytz +from biocompute.models import Bco +from datetime import datetime from django.conf import settings from django.contrib.auth. models import User -from biocompute.models import Bco -from prefix.selectors import user_can_view, user_can_modify +from prefix.selectors import ( + user_can_view_prefix, + user_can_modify_prefix, + user_can_publish_prefix +) + +def datetime_converter(input_date): + """Datetime converter + + Convert between a datetime object and an ISO 8601 formatted string. If the + input is a datetime object, it converts it to an ISO 8601 formatted string + with 'Z' as timezone (UTC). If the input is a string in ISO 8601 format, + it converts it to a datetime object with UTC timezone. + + Parameters: + - input_date (datetime or str): + The date to be converted, either as a datetime object or an ISO 8601 + string. + + Returns: + - datetime or str: + The converted date, either as an ISO 8601 string or a datetime object + with UTC timezone. + """ + + if isinstance(input_date, datetime): + return input_date.isoformat( + timespec='milliseconds').replace('+00:00', 'Z') + elif isinstance(input_date, str): + return datetime.fromisoformat( + input_date.rstrip('Z')).replace(tzinfo=pytz.UTC) + else: + raise ValueError("Input must be either a datetime object or a string"\ + + " in ISO 8601 format.") + +def prefix_from_object_id(object_id: str) -> str: + """Prefix From Object ID + + Parses a BCO object ID to extract the prefix part of the ID. + + Parameters: + - object_id (str): + The object ID from which the prefix needs to be extracted. + + Returns: + - str: + The extracted prefix name from the provided object ID. + + Raises: + - ValueError: + If the prefix cannot be extracted. + """ + + try: + prefix_name = object_id_deconstructor(object_id)[-2].split("_")[0] + return prefix_name + + except IndexError: + raise ValueError( + f"The object ID '{object_id}' does not conform to the expected"\ + + "format and the prefix cannot be extracted." + ) + +def user_can_publish_bco(object: dict, user:User) -> Bco: + """Publish BCO + + Determines if a user has permission to publish a specific BioCompute + Object (BCO). + + Checks if a given user is authorized to publish a BCO identified by its + `object_id` based on the following conditiions: + 1. The BCO exists. + 2. The user has general 'publish' permissions for the prefix associated + with the BCO, providing broader modification rights across BCOs with + the same prefix. + + Parameters: + - object_id (str): + The unique identifier of the BCO + - user (User): + The user whose modification permissions are being verified. + + Returns: + - Bco: + if the user is authorized to publish the specified BCO, `False` + otherwise. Returns `None` if the specified BCO does not exist. + """ + + draft_deconstructed = object_id_deconstructor(object["object_id"]) + published_deconstructed = [] + if "published_object_id" in object: + published_deconstructed = object_id_deconstructor( + object["published_object_id"] + ) + if published_deconstructed[-2] != draft_deconstructed[-2]: + return published_deconstructed[-2], draft_deconstructed[-2] + + try: + published_object = Bco.objects.get( + object_id=object["published_object_id"] + ) + return published_object + except Bco.DoesNotExist: + pass + + try: + bco_instance = Bco.objects.get(object_id=object["object_id"]) + version = bco_instance.contents['provenance_domain']['version'] + if len(published_deconstructed) == 6: + version = \ + bco_instance.contents['provenance_domain']['version'] + if version != published_deconstructed[-1]: + message = f"BCO version, {version}, does not match "\ + + f"`published_object_id`, {published_deconstructed[0]}" + return message + else: + draft_deconstructed[-1] = version + published_object_id = '/'.join(draft_deconstructed[1:]) + try: + published_object = Bco.objects.get( + object_id=published_object_id + ) + return published_object + except Bco.DoesNotExist: + pass + + if bco_instance.owner == user: + return bco_instance + + except Bco.DoesNotExist: + return None + + publish_permission = user_can_publish_prefix( + user, prefix_from_object_id(object["object_id"]) + ) + if publish_permission is False: + return publish_permission + + return bco_instance def user_can_modify_bco(object_id: str, user:User) -> bool: """Modify BCO + + Determines if a user has permission to modify a specific BioCompute + Object (BCO). + + Checks if a given user is authorized to modify a BCO identified by its + `object_id` based on the following conditiions: + 1. The user is listed in the `authorized_users` of the BCO instance, + allowing direct modification rights. + 2. The user has general 'modify' permissions for the prefix associated + with the BCO, providing broader modification rights across BCOs with + the same prefix. + + Parameters: + - object_id (str): + The unique identifier of the BCO + - user (User): + The user whose modification permissions are being verified. + + Returns: + - bool: + `True` if the user is authorized to modify the specified BCO, + `False` otherwise. Returns `None` if the specified BCO does not exist. """ try: bco_instance = Bco.objects.get(object_id=object_id) except Bco.DoesNotExist: return None + if user in bco_instance.authorized_users.all(): return True + + view_permission = user_can_modify_prefix( + user, prefix_from_object_id(object_id) + ) - prefix_name = object_id.split("/")[-2].split("_")[0] - view_permission = user_can_modify(prefix_name, user) - if view_permission is False: - return False - - return True + return view_permission def retrieve_bco(bco_accession:str, user:User, bco_version:str=None) -> bool: """Retrieve BCO @@ -57,8 +219,31 @@ def retrieve_bco(bco_accession:str, user:User, bco_version:str=None) -> bool: return bco_instance prefix_name = bco_accession.split("_")[0] - view_permission = user_can_view(prefix_name, user) + view_permission = user_can_view_prefix(prefix_name, user) if view_permission is False: return False - return bco_instance \ No newline at end of file + return bco_instance + +def object_id_deconstructor(object_id=str) -> list: + """ + Deconstructs a BioCompute Object (BCO) identifier into its constituent + parts (protocol, hostname, BCO accession, and BCO version). + + Parameters: + - object_id (str): + The unique identifier of a BCO. This identifier should follow the + recommended format which includes the protocol, hostname, BCO + accession (prefix and identifier), and version. + + Returns: + - list: + A list where the first element is the original `object_id` followed + by its deconstructed parts: + [original object_id, protocol, hostname, BCO accession, version] + """ + + deconstructed_object_id = object_id.split("/") + deconstructed_object_id.insert(0, object_id) + return deconstructed_object_id + diff --git a/biocompute/services.py b/biocompute/services.py index 4692417e..d3582106 100644 --- a/biocompute/services.py +++ b/biocompute/services.py @@ -1,9 +1,14 @@ #!/usr/bin/env python3 # biocopmute/services.py +import copy import json +import jsonref +import jsonschema +import re from hashlib import sha256 from biocompute.models import Bco +from biocompute.selectors import object_id_deconstructor, datetime_converter from copy import deepcopy from django.conf import settings from django.contrib.auth.models import User @@ -13,6 +18,8 @@ from prefix.models import Prefix from prefix.services import prefix_counter_increment from rest_framework import serializers +from simplejson.errors import JSONDecodeError +from requests.exceptions import ConnectionError as RequestsConnectionError """BioCompute Services @@ -20,6 +27,199 @@ """ HOSTNAME = settings.PUBLIC_HOSTNAME +BASE_DIR = settings.BASE_DIR + +class BcoValidator: + """BCO Validator + + Handles validation of BioCompute Objects (BCOs) against JSON Schemas. + """ + + def __init__(self): + """Initializes the BCOValidator with common attributes, if any.""" + self.base_path = f"{BASE_DIR}/config/IEEE/2791object.json" + + @staticmethod + def load_schema(schema_uri): + """ + Loads a JSON Schema from a given URI. + + Parameters: + - schema_uri (str): The URI or path to the JSON schema. + + Returns: + - dict: The loaded JSON schema. + """ + + if schema_uri == \ + "https://w3id.org/ieee/ieee-2791-schema/2791object.json": + return jsonref.load_uri( + f"file://{BASE_DIR}/config/IEEE/2791object.json" + ) + try: + return jsonref.load_uri(schema_uri) + except (JSONDecodeError, TypeError, RequestsConnectionError) as e: + error_msg = "Failed to load schema. " + if isinstance(e, JSONDecodeError): + return {schema_uri: [error_msg + "JSON Decode Error."]} + elif isinstance(e, TypeError): + return {schema_uri: [error_msg + "Invalid format."]} + elif isinstance(e, RequestsConnectionError): + return {schema_uri: [error_msg + "Connection Error."]} + + def validate_json(self, schema, json_object): + """ + Validates a JSON object against a specified schema. + + Parameters: + - schema (dict): The JSON schema to validate against. + - json_object (dict): The JSON object to be validated. + + Returns: + - list: A list of error messages, empty if valid. + """ + errors = [] + validator = jsonschema.Draft7Validator(schema) + for error in validator.iter_errors(json_object): + path = "".join(f"[{v}]" for v in error.path) + errors.append(f"{path}: {error.message}" if path else error.message) + return errors + + def parse_and_validate(self, bco): + """ + Parses and validates a BCO against both the base and extension schemas. + + Parameters: + - bco (dict): The BioCompute Object to validate. + + Returns: + - dict: A dictionary containing the validation results. + """ + + identifier = bco.get("object_id", "Unknown") + results = {identifier: {'number_of_errors': 0, 'error_detail': []}} + + # Validate against the base schema + base_schema = self.load_schema(bco['spec_version']) + base_errors = self.validate_json(base_schema, bco) + results[identifier]['error_detail'].extend(base_errors) + results[identifier]['number_of_errors'] += len(base_errors) + + # Validate against extension schemas, if any + for extension in bco.get("extension_domain", []): + extension_schema_uri = extension.get("extension_schema") + extension_schema = self.load_schema(extension_schema_uri) + if not isinstance(extension_schema, dict): # Validation passed + extension_errors = self.validate_json(extension_schema, extension) + results[identifier]['error_detail'].extend(extension_errors) + results[identifier]['number_of_errors'] += len(extension_errors) + + return results + +class ModifyBcoDraftSerializer(serializers.Serializer): + """Serializer for modifying draft BioCompute Objects (BCO). + + This serializer is used to validate and serialize data related to the + update of BCO drafts. + + Attributes: + - contents (JSONField): + The contents of the BCO in JSON format. + - authorized_users (ListField): + A list of usernames authorized to access the BCO, besides the owner. + + Methods: + - validate: Validates the incoming data for updating a BCO draft. + - update: Updates a BCO instance based on the validated data. + """ + contents = serializers.JSONField() + authorized_users = serializers.ListField(child=serializers.CharField(), required=False) + + def validate(self, attrs): + """BCO Modify Draft Validator + + Parameters: + - attrs (dict): + The incoming data to be validated. + + Returns: + - dict: + The validated data. + + Raises: + - serializers.ValidationError: If any validation checks fail. + """ + + errors = {} + + if 'authorized_users' in attrs: + for user in attrs['authorized_users']: + try: + User.objects.get(username=user) + except Exception as err: + errors['authorized_users'] =f"Invalid user: {user}" + + if errors: + raise serializers.ValidationError(errors) + + return attrs + + @transaction.atomic + def update(self, validated_data): + """Update BCO + + Updates an existing BioCompute Object (BCO) draft instance with + validated data. + + This method applies the validated changes to a BCO draft, including + updating its contents and the list of authorized users. It also + recalculates the `etag` of the BCO to reflect the new contents + and ensures that the `last_update` timestamp is current. If a list of + `authorized_users` is provided, this method replaces the current list + of authorized users with the new list, allowing for dynamic access + control to the BCO. Users not included in the new list will lose + their access unless they are the owner or have other permissions. + + This method employs Django's atomic transactions to ensure database + integrity during the update process. + + Parameters: + - instance (Bco): + The BCO instance to be updated. This parameter is automatically + supplied by the Django Rest Framework and not explicitly passed + in the serializer's call. + - validated_data (dict): + The data that has passed validation checks and is to be used to + update the BCO instance. It includes updated `contents` and + potentially a new list of `authorized_users`. + + Returns: + - Bco: The updated BCO instance + + Raises: + - Bco.DoesNotExist: + If the BCO instance with the specified `object_id` does not exist. + - User.DoesNotExist: + If one or more of the usernames in the `authorized_users` list do not correspond to valid User instances. + """ + + authorized_usernames = validated_data.pop('authorized_users', []) + bco_instance = Bco.objects.get( + object_id = validated_data['contents']['object_id'] + ) + bco_instance.contents = validated_data['contents'] + bco_instance.last_update=timezone.now() + bco_contents = deepcopy(bco_instance.contents) + etag = generate_etag(bco_contents) + bco_instance.contents['etag'] = etag + bco_instance.save() + if authorized_usernames: + authorized_users = User.objects.filter( + username__in=authorized_usernames + ) + bco_instance.authorized_users.set(authorized_users) + + return bco_instance class ModifyBcoDraftSerializer(serializers.Serializer): """Serializer for modifying draft BioCompute Objects (BCO). @@ -305,6 +505,80 @@ def generate_etag(bco_contents: dict) -> str: A SHA-256 hash string acting as the etag for the BCO. """ - del bco_contents['object_id'], bco_contents['spec_version'], bco_contents['etag'] + bco_contents_copy = copy.deepcopy(bco_contents) + + for key in ['object_id', 'spec_version', 'etag']: + bco_contents_copy.pop(key, None) + bco_etag = sha256(json.dumps(bco_contents).encode('utf-8')).hexdigest() return bco_etag + +def check_etag_validity(bco_contents: dict) -> bool: + """ + Check the validity of an ETag for a BioCompute Object (BCO). + + This function regenerates the ETag based on the current state of the BCO's contents, + excluding the 'object_id', 'spec_version', and 'etag' fields, and compares it to the + provided ETag. If both ETags match, it indicates that the BCO has not been altered in + a way that affects its ETag, thus confirming its validity. + + Parameters: + - bco_contents (dict): + The current contents of the BCO. + + Returns: + - bool: + True if the provided ETag matches the regenerated one, False otherwise. + """ + + provided_etag = bco_contents.get("etag", "") + bco_contents_copy = copy.deepcopy(bco_contents) + + for key in ['object_id', 'spec_version', 'etag']: + bco_contents_copy.pop(key, None) + + regenerated_etag = sha256(json.dumps(bco_contents_copy).encode('utf-8')).hexdigest() + print(provided_etag, regenerated_etag) + return provided_etag == regenerated_etag + +@transaction.atomic +def publish_draft(bco_instance: Bco, user: User, object: dict): + """Create Published BCO + """ + + new_bco_instance = deepcopy(bco_instance) + new_bco_instance.id = None + new_bco_instance.state = "PUBLISHED" + if "published_object_id" in object: + new_bco_instance.object_id = object["published_object_id"] + else: + contents= new_bco_instance.contents + version = contents['provenance_domain']['version'] + draft_deconstructed = object_id_deconstructor(object["object_id"]) + draft_deconstructed[-1] = version + new_bco_instance.object_id = '/'.join(draft_deconstructed[1:]) + contents["object_id"] = new_bco_instance.object_id + new_bco_instance.last_update = timezone.now() + contents["provenance_domain"]["modified"] = datetime_converter( + timezone.now() + ) + contents["etag"] = generate_etag(contents) + new_bco_instance.save() + + if object["delete_draft"] is True: + deleted = delete_draft(bco_instance=bco_instance, user=user) + + return new_bco_instance + +def delete_draft(bco_instance:Bco, user:User,): + """Delete Draft + + Delete draft bco + """ + + if bco_instance.owner == user: + bco_instance.state = "DELETE" + bco_instance.save() + + return "deleted" + diff --git a/biocompute/urls.py b/biocompute/urls.py index aa2fc7cc..f3a0b6f4 100644 --- a/biocompute/urls.py +++ b/biocompute/urls.py @@ -6,9 +6,11 @@ from biocompute.apis import ( DraftsCreateApi, DraftsModifyApi, + DraftsPublishApi, ) urlpatterns = [ path("objects/drafts/create/", DraftsCreateApi.as_view()), path("objects/drafts/modify/", DraftsModifyApi.as_view()), + path("objects/drafts/publish/", DraftsPublishApi.as_view()), ] \ No newline at end of file diff --git a/config/IEEE/2791object.json b/config/IEEE/2791object.json new file mode 100755 index 00000000..7c0c25b0 --- /dev/null +++ b/config/IEEE/2791object.json @@ -0,0 +1,178 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://w3id.org/ieee/ieee-2791-schema/2791object.json", + "type": "object", + "title": "Base type for all IEEE-2791 Objects", + "description": "All IEEE-2791 object types must adhear to this type in order to be compliant with IEEE-2791 standard", + "required": [ + "object_id", + "spec_version", + "etag", + "provenance_domain", + "usability_domain", + "description_domain", + "execution_domain", + "io_domain" + ], + "definitions": { + "object_id": { + "type": "string", + "description": "A unique identifier that should be applied to each IEEE-2791 Object instance, generated and assigned by a IEEE-2791 database engine. IDs should never be reused" + }, + "uri": { + "type": "object", + "description": "Any of the four Resource Identifers defined at https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7.3.5", + "additionalProperties": false, + "required": [ + "uri" + ], + "properties": { + "filename": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "uri" + }, + "access_time": { + "type": "string", + "description": "Time stamp of when the request for this data was submitted", + "format": "date-time" + }, + "sha1_checksum": { + "type": "string", + "description": "output of hash function that produces a message digest", + "pattern": "[A-Za-z0-9]+" + } + } + }, + "contributor": { + "type": "object", + "description": "Contributor identifier and type of contribution (determined according to PAV ontology) is required", + "required": [ + "contribution", + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Name of contributor", + "examples": [ + "Charles Darwin" + ] + }, + "affiliation": { + "type": "string", + "description": "Organization the particular contributor is affiliated with", + "examples": [ + "HMS Beagle" + ] + }, + "email": { + "type": "string", + "description": "electronic means for identification and communication purposes", + "examples": [ + "name@example.edu" + ], + "format": "email" + }, + "contribution": { + "type": "array", + "description": "type of contribution determined according to PAV ontology", + "reference": "https://doi.org/10.1186/2041-1480-4-37", + "items": { + "type": "string", + "enum": [ + "authoredBy", + "contributedBy", + "createdAt", + "createdBy", + "createdWith", + "curatedBy", + "derivedFrom", + "importedBy", + "importedFrom", + "providedBy", + "retrievedBy", + "retrievedFrom", + "sourceAccessedBy" + ] + } + }, + "orcid": { + "type": "string", + "description": "Field to record author information. ORCID identifiers allow for the author to curate their information after submission. ORCID identifiers must be valid and must have the prefix ‘https://orcid.org/’", + "examples": [ + "http://orcid.org/0000-0002-1825-0097" + ], + "format": "uri" + } + } + } + }, + "additionalProperties": false, + "properties": { + "object_id": { + "$ref": "#/definitions/object_id", + "readOnly": true + }, + "spec_version": { + "type": "string", + "description": "Version of the IEEE-2791 specification used to define this document", + "examples": [ + "https://w3id.org/ieee/ieee-2791-schema/" + ], + "readOnly": true, + "format": "uri" + }, + "etag": { + "type": "string", + "description": "See https://tools.ietf.org/html/rfc7232#section-2.1 for full description. It is recommended that the ETag be deleted or updated if the object file is changed (except in cases using weak ETags in which the entirety of the change comprises a simple re-writing of the JSON).", + "examples": [ + "5986B05969341343E77A95B4023600FC8FEF48B7E79F355E58B0B404A4F50995" + ], + "readOnly": true, + "pattern": "^([A-Za-z0-9]+)$" + }, + "provenance_domain": { + "$ref": "provenance_domain.json" + }, + "usability_domain": { + "$ref": "usability_domain.json" + }, + "extension_domain": { + "type": "array", + "description": "An optional domain that contains user-defined fields.", + "items":{ + "required":[ + "extension_schema" + ], + "additionalProperties": true, + "properties": { + "extension_schema":{ + "title": "Extension Schema", + "description": "resolving this URI should provide this extension's JSON Schema", + "type": "string", + "format": "uri" + } + } + } + }, + "description_domain": { + "$ref": "description_domain.json" + }, + "execution_domain": { + "$ref": "execution_domain.json" + }, + "parametric_domain": { + "$ref": "parametric_domain.json" + }, + "io_domain": { + "$ref": "io_domain.json" + }, + "error_domain": { + "$ref": "error_domain.json" + } + } +} \ No newline at end of file diff --git a/config/IEEE/description_domain.json b/config/IEEE/description_domain.json new file mode 100755 index 00000000..f22610e8 --- /dev/null +++ b/config/IEEE/description_domain.json @@ -0,0 +1,165 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://w3id.org/ieee/ieee-2791-schema/description_domain.json", + "type": "object", + "title": "Description Domain", + "description": "Structured field for description of external references, the pipeline steps, and the relationship of I/O objects.", + "required": [ + "keywords", + "pipeline_steps" + ], + "properties": { + "keywords": { + "type": "array", + "description": "Keywords to aid in search-ability and description of the object.", + "items": { + "type": "string", + "description": "This field should take free text value using common biological research terminology.", + "examples": [ + "HCV1a", + "Ledipasvir", + "antiviral resistance", + "SNP", + "amino acid substitutions" + ] + } + }, + "xref": { + "type": "array", + "description": "List of the databases or ontology IDs that are cross-referenced in the IEEE-2791 Object.", + "items": { + "type": "object", + "description": "External references are stored in the form of prefixed identifiers (CURIEs). These CURIEs map directly to the URIs maintained by Identifiers.org.", + "reference": "https://identifiers.org/", + "required": [ + "namespace", + "name", + "ids", + "access_time" + ], + "properties": { + "namespace": { + "type": "string", + "description": "External resource vendor prefix", + "examples": [ + "pubchem.compound" + ] + }, + "name": { + "type": "string", + "description": "Name of external reference", + "examples": [ + "PubChem-compound" + ] + }, + "ids": { + "type": "array", + "description": "List of reference identifiers", + "items": { + "type": "string", + "description": "Reference identifier", + "examples": [ + "67505836" + ] + } + }, + "access_time": { + "type": "string", + "description": "Date and time the external reference was accessed", + "format": "date-time" + } + } + } + }, + "platform": { + "type": "array", + "description": "reference to a particular deployment of an existing platform where this IEEE-2791 Object can be reproduced.", + "items": { + "type": "string", + "examples": [ + "hive" + ] + } + }, + "pipeline_steps": { + "type": "array", + "description": "Each individual tool (or a well defined and reusable script) is represented as a step. Parallel processes are given the same step number.", + "items": { + "additionalProperties": false, + "type": "object", + "required": [ + "step_number", + "name", + "description", + "input_list", + "output_list" + ], + "properties": { + "step_number": { + "type": "integer", + "description": "Non-negative integer value representing the position of the tool in a one-dimensional representation of the pipeline." + }, + "name": { + "type": "string", + "description": "This is a recognized name of the software tool", + "examples": [ + "HIVE-hexagon" + ] + }, + "description": { + "type": "string", + "description": "Specific purpose of the tool.", + "examples": [ + "Alignment of reads to a set of references" + ] + }, + "version": { + "type": "string", + "description": "Version assigned to the instance of the tool used corresponding to the upstream release.", + "examples": [ + "1.3" + ] + }, + "prerequisite": { + "type": "array", + "description": "Reference or required prereqs", + "items": { + "type": "object", + "description": "Text value to indicate a package or prerequisite for running the tool used.", + "required": [ + "name", + "uri" + ], + "properties": { + "name": { + "type": "string", + "description": "Public searchable name for reference or prereq.", + "examples": [ + "Hepatitis C virus genotype 1" + ] + }, + "uri": { + "$ref": "2791object.json#/definitions/uri" + } + } + } + }, + "input_list": { + "type": "array", + "description": "URIs (expressed as a URN or URL) of the input files for each tool.", + "items": { + "$ref": "2791object.json#/definitions/uri" + } + }, + "output_list": { + "type": "array", + "description": "URIs (expressed as a URN or URL) of the output files for each tool.", + "items": { + "$ref": "2791object.json#/definitions/uri" + } + } + } + } + } + } +} diff --git a/config/IEEE/error_domain.json b/config/IEEE/error_domain.json new file mode 100755 index 00000000..c0be62b0 --- /dev/null +++ b/config/IEEE/error_domain.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://w3id.org/ieee/ieee-2791-schema/error_domain.json", + "type": "object", + "title": "Error Domain", + "description": "Fields in the Error Domain are open-ended and not restricted nor defined by the IEEE-2791 standard. It is RECOMMENDED that the keys directly under empirical_error and algorithmic_error use a full URI. Resolving the URI SHOULD give a JSON Schema or textual definition of the field. Other keys are not allowed error_domain", + "additionalProperties": false, + "required": [ + "empirical_error", + "algorithmic_error" + ], + "properties": { + "empirical_error": { + "type": "object", + "title": "Empirical Error", + "description": "empirically determined values such as limits of detectability, false positives, false negatives, statistical confidence of outcomes, etc. This can be measured by running the algorithm on multiple data samples of the usability domain or through the use of carefully designed in-silico data." + }, + "algorithmic_error": { + "type": "object", + "title": "Algorithmic Error", + "description": "descriptive of errors that originate by fuzziness of the algorithms, driven by stochastic processes, in dynamically parallelized multi-threaded executions, or in machine learning methodologies where the state of the machine can affect the outcome." + } + } +} diff --git a/config/IEEE/execution_domain.json b/config/IEEE/execution_domain.json new file mode 100755 index 00000000..858cad2d --- /dev/null +++ b/config/IEEE/execution_domain.json @@ -0,0 +1,111 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://w3id.org/ieee/ieee-2791-schema/execution_domain.json", + "type": "object", + "title": "Execution Domain", + "description": "The fields required for execution of the IEEE-2791 Object are herein encapsulated together in order to clearly separate information needed for deployment, software configuration, and running applications in a dependent environment", + "required": [ + "script", + "script_driver", + "software_prerequisites", + "external_data_endpoints", + "environment_variables" + ], + "additionalProperties": false, + "properties": { + "script": { + "type": "array", + "description": "points to a script object or objects that was used to perform computations for this IEEE-2791 Object instance.", + "items": { + "additionalProperties": false, + "properties": { + "uri": { + "$ref": "2791object.json#/definitions/uri" + } + } + } + }, + "script_driver": { + "type": "string", + "description": "Indication of the kind of executable that can be launched in order to perform a sequence of commands described in the script in order to run the pipelin", + "examples": [ + "hive", + "cwl-runner", + "shell" + ] + }, + "software_prerequisites": { + "type": "array", + "description": "Minimal necessary prerequisites, library, tool versions needed to successfully run the script to produce this IEEE-2791 Object.", + "items": { + "type": "object", + "description": "A necessary prerequisite, library, or tool version.", + "required": [ + "name", + "version", + "uri" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Names of software prerequisites", + "examples": [ + "HIVE-hexagon" + ] + }, + "version": { + "type": "string", + "description": "Versions of the software prerequisites", + "examples": [ + "babajanian.1" + ] + }, + "uri": { + "$ref": "2791object.json#/definitions/uri" + } + } + } + }, + "external_data_endpoints": { + "type": "array", + "description": "Minimal necessary domain-specific external data source access in order to successfully run the script to produce this IEEE-2791 Object.", + "items": { + "type": "object", + "description": "Requirement for network protocol endpoints used by a pipeline’s scripts, or other software.", + "required": [ + "name", + "url" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Description of the service that is accessed", + "examples": [ + "HIVE", + "access to e-utils" + ] + }, + "url": { + "type": "string", + "description": "The endpoint to be accessed.", + "examples": [ + "https://hive.biochemistry.gwu.edu/dna.cgi?cmd=login" + ] + } + } + } + }, + "environment_variables": { + "type": "object", + "description": "Environmental parameters that are useful to configure the execution environment on the target platform.", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z_]+[a-zA-Z0-9_]*$": { + "type": "string" + } + } + } + } +} diff --git a/config/IEEE/io_domain.json b/config/IEEE/io_domain.json new file mode 100755 index 00000000..c460e576 --- /dev/null +++ b/config/IEEE/io_domain.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://w3id.org/ieee/ieee-2791-schema/io_domain.json", + "type": "object", + "title": "Input and Output Domain", + "description": "The list of global input and output files created by the computational workflow, excluding the intermediate files. Custom to every specific IEEE-2791 Object implementation, these fields are pointers to objects that can reside in the system performing the computation or any other accessible system.", + "required": [ + "input_subdomain", + "output_subdomain" + ], + "properties": { + "input_subdomain": { + "type": "array", + "title": "input_domain", + "description": "A record of the references and input files for the entire pipeline. Each type of input file is listed under a key for that type.", + "items": { + "additionalProperties": false, + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "$ref": "2791object.json#/definitions/uri" + } + } + } + }, + "output_subdomain": { + "type": "array", + "title": "output_subdomain", + "description": "A record of the outputs for the entire pipeline.", + "items": { + "type": "object", + "title": "The Items Schema", + "required": [ + "mediatype", + "uri" + ], + "properties": { + "mediatype": { + "type": "string", + "title": "mediatype", + "description": "https://www.iana.org/assignments/media-types/", + "default": "application/octet-stream", + "examples": [ + "text/csv" + ], + "pattern": "^(.*)$" + }, + "uri": { + "$ref": "2791object.json#/definitions/uri" + } + } + } + } + } +} diff --git a/config/IEEE/parametric_domain.json b/config/IEEE/parametric_domain.json new file mode 100755 index 00000000..cde0644b --- /dev/null +++ b/config/IEEE/parametric_domain.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://w3id.org/ieee/ieee-2791-schema/parametric_domain.json", + "type": "array", + "title": "Parametric Domain", + "description": "This represents the list of NON-default parameters customizing the computational flow which can affect the output of the calculations. These fields can be custom to each kind of analysis and are tied to a particular pipeline implementation", + "items":{ + "required": [ + "param", + "value", + "step" + ], + "additionalProperties": false, + "properties": { + "param": { + "type": "string", + "title": "param", + "description": "Specific variables for the computational workflow", + "examples": [ + "seed" + ] + }, + "value": { + "type": "string", + "description": "Specific (non-default) parameter values for the computational workflow", + "title": "value", + "examples": [ + "14" + ] + }, + "step": { + "type": "string", + "title": "step", + "description": "Refers to the specific step of the workflow relevant to the parameters specified in 'param' and 'value'", + "examples": [ + "1" + ], + "pattern": "^(.*)$" + } + } + } +} diff --git a/config/IEEE/provenance_domain.json b/config/IEEE/provenance_domain.json new file mode 100755 index 00000000..0c1aa5ac --- /dev/null +++ b/config/IEEE/provenance_domain.json @@ -0,0 +1,126 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://w3id.org/ieee/ieee-2791-schema/provenance_domain.json", + "type": "object", + "title": "Provenance Domain", + "description": "Structured field for tracking data through transformations, including contributors, reviewers, and versioning.", + "required": [ + "name", + "version", + "created", + "modified", + "contributors", + "license" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Public searchable name for IEEE-2791 Object. This public field should take free text value using common biological research terminology supporting the terminology used in the usability_domain, external references (xref), and keywords sections.", + "examples": [ + "HCV1a ledipasvir resistance SNP detection" + ] + }, + "version": { + "type": "string", + "description": "Records the versioning of this IEEE-2791 Object instance. IEEE-2791 Object Version should adhere to semantic versioning as recommended by Semantic Versioning 2.0.0.", + "reference": "https://semver.org/spec/v2.0.0.html", + "examples": [ + "2.9" + ] + }, + "review": { + "type": "array", + "description": "Description of the current verification status of an object in the review process. The unreviewed flag indicates that the object has been submitted, but no further evaluation or verification has occurred. The in-review flag indicates that verification is underway. The approved flag indicates that the IEEE-2791 Object has been verified and reviewed. The suspended flag indicates an object that was once valid is no longer considered valid. The rejected flag indicates that an error or inconsistency was detected in the IEEE-2791 Object, and it has been removed or rejected. The fields from the contributor object (described in section 2.1.10) is inherited to populate the reviewer section.", + "items": { + "type": "object", + "required": [ + "status", + "reviewer" + ], + "additionalProperties": false, + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "reviewer": { + "$ref": "2791object.json#/definitions/contributor", + "description": "Contributer that assigns IEEE-2791 review status." + }, + "reviewer_comment": { + "type": "string", + "description": "Optional free text comment by reviewer", + "examples": [ + "Approved by research institution staff. Waiting for approval from regulator" + ] + }, + "status": { + "type": "string", + "enum": [ + "unreviewed", + "in-review", + "approved", + "rejected", + "suspended" + ], + "description": "Current verification status of the IEEE-2791 Object", + "default": "unreviewed" + } + } + } + }, + "derived_from": { + "description": "value of `ieee2791_id` field of another IEEE-2791 that this object is partially or fully derived from", + "$ref": "2791object.json#/definitions/object_id" + }, + "obsolete_after": { + "type": "string", + "description": "If the object has an expiration date, this optional field will specify that using the ‘datetime’ type described in ISO-8601 format, as clarified by W3C https://www.w3.org/TR/NOTE-datetime.", + "format": "date-time" + }, + "embargo": { + "type": "object", + "description": "If the object has a period of time during which it shall not be made public, that range can be specified using these optional fields. Using the datetime type, a start and end time are specified for the embargo.", + "additionalProperties": false, + "properties": { + "start_time": { + "type": "string", + "description": "Beginning date of embargo period.", + "format": "date-time" + }, + "end_time": { + "type": "string", + "description": "End date of embargo period.", + "format": "date-time" + } + } + }, + "created": { + "type": "string", + "description": "Date and time of the IEEE-2791 Object creation", + "readOnly": true, + "format": "date-time" + }, + "modified": { + "type": "string", + "description": "Date and time the IEEE-2791 Object was last modified", + "readOnly": true, + "format": "date-time" + }, + "contributors": { + "type": "array", + "description": "This is a list to hold contributor identifiers and a description of their type of contribution, including a field for ORCIDs to record author information, as they allow for the author to curate their information after submission. The contribution type is a choice taken from PAV ontology: provenance, authoring and versioning, which also maps to the PROV-O.", + "items": { + "$ref": "2791object.json#/definitions/contributor" + } + }, + "license": { + "type": "string", + "description": "Creative Commons license or other license information (text) space. The default or recommended license can be Attribution 4.0 International as shown in example", + "examples": [ + "https://spdx.org/licenses/CC-BY-4.0.html" + ] + } + } +} diff --git a/config/IEEE/usability_domain.json b/config/IEEE/usability_domain.json new file mode 100755 index 00000000..54e936e4 --- /dev/null +++ b/config/IEEE/usability_domain.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://w3id.org/ieee/ieee-2791-schema/usability_domain.json", + "type": "array", + "title": "Usability Domain", + "description": "Author-defined usability domain of the IEEE-2791 Object. This field is to aid in search-ability and provide a specific description of the function of the object.", + "items": { + "type": "string", + "description": "Free text values that can be used to provide scientific reasoning and purpose for the experiment", + "examples": [ + "Identify baseline single nucleotide polymorphisms SNPs [SO:0000694], insertions [so:SO:0000667], and deletions [so:SO:0000045] that correlate with reduced ledipasvir [pubchem.compound:67505836] antiviral drug efficacy in Hepatitis C virus subtype 1 [taxonomy:31646]", + "Identify treatment emergent amino acid substitutions [so:SO:0000048] that correlate with antiviral drug treatment failure", + "Determine whether the treatment emergent amino acid substitutions [so:SO:0000048] identified correlate with treatment failure involving other drugs against the same virus" + ] + } +} diff --git a/config/services.py b/config/services.py index b3255953..1cc0dbbd 100644 --- a/config/services.py +++ b/config/services.py @@ -1,19 +1,64 @@ #!/usr/bin/env python3 # config/services.py +from rest_framework import status + """DB Level Services - Service functiontions for the entire DB + This module contains service functions that apply to the entire BioCompute + Object Database (BCODB). It includes utility functions for handling + response status determination, legacy API data conversion, and + constructing standardized response objects. """ +def response_status(accepted_requests: bool, rejected_requests: bool)-> status: + """Determine Response Status + + Determines the appropriate HTTP response status code based on the + acceptance or rejection of requests. + + Parameters: + - accepted_requests (bool): + Flag indicating whether any requests have been accepted. + - rejected_requests (bool): + Flag indicating whether any requests have been rejected. + + Returns: + - int: The HTTP status code representing the outcome. Possible values are: + - status.HTTP_400_BAD_REQUEST (400) if all requests are rejected. + - status.HTTP_207_MULTI_STATUS (207) if there is a mix of accepted and rejected requests. + - status.HTTP_200_OK (200) if all requests are accepted. + """ + + if accepted_requests is False and rejected_requests == True: + status_code = status.HTTP_400_BAD_REQUEST + + if accepted_requests is True and rejected_requests is True: + status_code = status.HTTP_207_MULTI_STATUS + + if accepted_requests is True and rejected_requests is False: + status_code = status.HTTP_200_OK + + return status_code + def legacy_api_converter(data:dict) ->dict: """Legacy API converter Used to remove the `POST_` object from requests. - Prefix APIs require a little more cleaning. + Prefix APIs and "draft_publish" APIs require a little more cleaning. """ _, new_data = data.popitem() + if "draft_id" in new_data[0]: + return_data =[] + for object in new_data: + return_data.append({ + "object_id": object["draft_id"], + "published_object_id": object["object_id"], + "delete_draft": object["delete_draft"] + }) + return return_data + if "prefixes" in new_data[0]: return_data =[] for object in new_data: diff --git a/docs/refactor.md b/docs/refactor.md index e5454202..3560859f 100644 --- a/docs/refactor.md +++ b/docs/refactor.md @@ -45,6 +45,7 @@ - unwanted swagger endpoints - need tests for token - prefix api documentation and portal docs for prefix +- Remove ETag from Portal Prefix Perms: add -> create new DRAFT @@ -56,7 +57,10 @@ Prefix Perms: If prefix is public anyone can view, but only auth users can modify. - Things to look for when reviewing code: +## Things to look for when reviewing code: +### For Swaggar: + - Each swaggar endpoint has a "one click" working example. + ### For functions: - variable names are consistant and make sense - all functions have documentation. This shoudl include: - descriptions diff --git a/prefix/selectors.py b/prefix/selectors.py index dc27a76f..b13b87b9 100644 --- a/prefix/selectors.py +++ b/prefix/selectors.py @@ -10,7 +10,23 @@ from django.db import utils from prefix.models import Prefix -def user_can_modify(user: User, prefix_name:str) -> bool: +def user_can_publish_prefix(user: User, prefix_name:str) -> bool: + """User Can Publish + + Takes a prefix name and user. Returns a bool if the user can publish a BCO + with the prefix if it exists. If the prefix does not exist `None` is + returned. + """ + + try: + Prefix.objects.get(prefix=prefix_name) + except Prefix.DoesNotExist: + return None + codename = f"publish_{prefix_name}" + user_prefixes = get_user_prefixes(user) + return codename in user_prefixes + +def user_can_modify_prefix(user: User, prefix_name:str) -> bool: """User Can Modify Takes a prefix name and user. Returns a bool if the user can modify a BCO @@ -27,7 +43,7 @@ def user_can_modify(user: User, prefix_name:str) -> bool: return codename in user_prefixes -def user_can_draft(user: User, prefix_name:str) -> bool: +def user_can_draft_prefix(user: User, prefix_name:str) -> bool: """User Can Draft Takes a prefix name and user. Returns a bool if the user can draft a BCO @@ -44,7 +60,7 @@ def user_can_draft(user: User, prefix_name:str) -> bool: return codename in user_prefixes -def user_can_view(prefix_name:str, user: User) -> bool: +def user_can_view_prefix(prefix_name:str, user: User) -> bool: """User Can View Takes a prefix name and user. Returns a bool if the user can view a BCO diff --git a/tests/test_apis/test_biocompute/test_objects_drafts_create.py b/tests/test_apis/test_biocompute/test_objects_drafts_create.py index b69f9e95..1fecba30 100644 --- a/tests/test_apis/test_biocompute/test_objects_drafts_create.py +++ b/tests/test_apis/test_biocompute/test_objects_drafts_create.py @@ -83,7 +83,11 @@ def test_partial_failure(self): 'prefix': 'BCO', 'owner_group': 'bco_drafter', 'schema': 'IEEE', - 'contents': {} + 'contents': { + "object_id": "https://biocomputeobject.org/BCO_000005", + "spec_version": "https://w3id.org/ieee/ieee-2791-schema/2791object.json", + "etag": "11ee4c3b8a04ad16dcca19a6f478c0870d3fe668ed6454096ab7165deb1ab8ea" + } }, { 'prefix': 'Reeyaa', @@ -132,4 +136,4 @@ def test_invalid_token(self): } self.client.credentials(HTTP_AUTHORIZATION='Token InvalidToken') response = self.client.post('/api/objects/drafts/create/', data=data, format='json') - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 403) \ No newline at end of file diff --git a/tests/test_apis/test_biocompute/test_objects_drafts_publish.py b/tests/test_apis/test_biocompute/test_objects_drafts_publish.py new file mode 100644 index 00000000..b38790b0 --- /dev/null +++ b/tests/test_apis/test_biocompute/test_objects_drafts_publish.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +"""Tests for DraftsPublishApi [Bulk Enabled] + +DraftsPublishApi: +- checks for legacy submission +- for each object: + - `user_can_publish_bco`: + - checks for published_object_id and makes sure it does not exist + - checks that DRAFT exists + - if published_object_id in request, then checks that published_object_id version matches BCO version + - else checks that draft object_id + version does not exist + - checks if user can publish with prefix of BCO + : `returns DRAFT object` if every check is passed + - `parse_and_validate`: validates BCO. If errors then rejected. + - `publish_draft`: + - copies draft, assignes new ID and status to the copy + - updates the "last_update" field in Django and the BCOs "modified" field + - generates ETag + - saves published object + - if "delete_draft" is true then deletes draft +""" \ No newline at end of file