Skip to content

Commit 8ee1039

Browse files
authored
Publish draft (#308)
* Add DraftsModifyApi Changes to be committed: modified: biocompute/apis.py modified: biocompute/selectors.py modified: biocompute/services.py modified: biocompute/urls.py modified: prefix/selectors.py deleted: tests/test_apis/test_biocompute/objects_drafts_create.py deleted: tests/test_apis/test_biocompute/test_objects_drafts_create.py * Add IEEE schema Changes to be committed: new file: .secrets new file: config/IEEE/2791object.json new file: config/IEEE/description_domain.json new file: config/IEEE/error_domain.json new file: config/IEEE/execution_domain.json new file: config/IEEE/io_domain.json new file: config/IEEE/parametric_domain.json new file: config/IEEE/provenance_domain.json new file: config/IEEE/usability_domain.json * test_objects_drafts_create Changes to be committed: new file: tests/test_apis/test_biocompute/test_objects_drafts_create.py * Fix test Changes to be committed: modified: tests/test_apis/test_biocompute/test_objects_drafts_create.py * Enable publish bco endpoint This included a great many supporting functions and permission checks to accomplish. Also added the validate functions to the codebase Changes to be committed: modified: biocompute/apis.py modified: biocompute/selectors.py modified: biocompute/services.py modified: biocompute/urls.py modified: config/services.py modified: docs/refactor.md modified: prefix/selectors.py new file: tests/test_apis/test_biocompute/test_objects_drafts_publish.py
1 parent 407b18c commit 8ee1039

18 files changed

+1590
-42
lines changed

.secrets

Whitespace-only changes.

biocompute/apis.py

Lines changed: 299 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,34 @@
44
"""BioCompute Object APIs
55
"""
66

7+
from biocompute.services import (
8+
BcoDraftSerializer,
9+
BcoValidator,
10+
ModifyBcoDraftSerializer,
11+
publish_draft,
12+
bco_counter_increment
13+
)
14+
from biocompute.selectors import (
15+
retrieve_bco,
16+
user_can_modify_bco,
17+
user_can_publish_bco,
18+
object_id_deconstructor,
19+
)
20+
from config.services import (
21+
legacy_api_converter,
22+
response_constructor,
23+
response_status,
24+
)
725
from drf_yasg import openapi
826
from drf_yasg.utils import swagger_auto_schema
927
from django.conf import settings
1028
from django.db import utils
11-
from rest_framework.views import APIView
29+
from prefix.selectors import user_can_draft_prefix
1230
from rest_framework import status
13-
from rest_framework.permissions import IsAuthenticated
31+
from rest_framework.views import APIView
32+
from rest_framework.permissions import IsAuthenticated, AllowAny
1433
from rest_framework.response import Response
1534
from tests.fixtures.example_bco import BCO_000001
16-
from config.services import legacy_api_converter, response_constructor
17-
from biocompute.services import BcoDraftSerializer, bco_counter_increment, ModifyBcoDraftSerializer
18-
from biocompute.selectors import retrieve_bco, user_can_modify_bco
19-
from prefix.selectors import user_can_draft
2035

2136
hostname = settings.PUBLIC_HOSTNAME
2237

@@ -93,7 +108,7 @@ def post(self, request) -> Response:
93108
for index, object in enumerate(data):
94109
response_id = object.get("object_id", index)
95110
bco_prefix = object.get("prefix", index)
96-
prefix_permitted = user_can_draft(owner, bco_prefix)
111+
prefix_permitted = user_can_draft_prefix(owner, bco_prefix)
97112

98113
if prefix_permitted is None:
99114
response_data.append(response_constructor(
@@ -147,23 +162,131 @@ def post(self, request) -> Response:
147162
))
148163
rejected_requests = True
149164

150-
if accepted_requests is False and rejected_requests == True:
151-
return Response(
152-
status=status.HTTP_400_BAD_REQUEST,
153-
data=response_data
154-
)
165+
status_code = response_status(accepted_requests, rejected_requests)
166+
return Response(status=status_code, data=response_data)
167+
168+
class DraftsModifyApi(APIView):
169+
"""Modify BCO Draft [Bulk Enabled]
170+
171+
API endpoint for modifying BioCompute Object (BCO) drafts, with support
172+
for bulk operations.
173+
174+
This endpoint allows authenticated users to modify existing BCO drafts
175+
individually or in bulk by submitting a list of BCO drafts. The operation
176+
can be performed for one or more drafts in a single request. Each draft is
177+
validated and processed independently, allowing for mixed response
178+
statuses (HTTP_207_MULTI_STATUS) in the case of bulk submissions.
179+
180+
NOTE: If a list of `authorized_users` is provided, this method replaces
181+
the current list of authorized users with the new list, allowing for
182+
dynamic access control to the BCO. Users not included in the new list will
183+
lose their access unless they are the owner or have other permissions.
184+
"""
185+
186+
permission_classes = [IsAuthenticated,]
187+
188+
@swagger_auto_schema(
189+
operation_id="api_objects_drafts_modify",
190+
request_body=openapi.Schema(
191+
type=openapi.TYPE_ARRAY,
192+
title="Modify BCO Draft Schema",
193+
items=openapi.Schema(
194+
type=openapi.TYPE_OBJECT,
195+
required=[],
196+
properties={
197+
"authorized_users": openapi.Schema(
198+
type=openapi.TYPE_ARRAY,
199+
description="Users which can access the BCO draft.",
200+
items=openapi.Schema(type=openapi.TYPE_STRING,
201+
example="tester")
202+
),
203+
"contents": openapi.Schema(
204+
type=openapi.TYPE_OBJECT,
205+
description="Contents of the BCO.",
206+
example=BCO_000001
207+
),
208+
},
209+
),
210+
description="Modify BCO draft [Bulk Enabled].",
211+
),
212+
responses={
213+
200: "All requests were accepted.",
214+
207: "Some requests failed and some succeeded. Each object submitted"
215+
" will have it's own response object with it's own status"
216+
" code and message.\n",
217+
400: "All requests were rejected.",
218+
403: "Invalid token.",
219+
},
220+
tags=["BCO Management"],
221+
)
222+
223+
def post(self, request) -> Response:
224+
response_data = []
225+
requester = request.user
226+
data = request.data
227+
rejected_requests = False
228+
accepted_requests = False
229+
if 'POST_api_objects_drafts_modify' in request.data:
230+
data = legacy_api_converter(request.data)
155231

156-
if accepted_requests is True and rejected_requests is True:
157-
return Response(
158-
status=status.HTTP_207_MULTI_STATUS,
159-
data=response_data
160-
)
232+
for index, object in enumerate(data):
233+
response_id = object.get("object_id", index)
234+
modify_permitted = user_can_modify_bco(response_id, requester)
235+
236+
if modify_permitted is None:
237+
response_data.append(response_constructor(
238+
identifier=response_id,
239+
status = "NOT FOUND",
240+
code= 404,
241+
message= f"Invalid BCO: {response_id}.",
242+
))
243+
rejected_requests = True
244+
continue
161245

162-
if accepted_requests is True and rejected_requests is False:
163-
return Response(
164-
status=status.HTTP_200_OK,
165-
data=response_data
166-
)
246+
if modify_permitted is False:
247+
response_data.append(response_constructor(
248+
identifier=response_id,
249+
status = "FORBIDDEN",
250+
code= 400,
251+
message= f"User, {requester}, does not have draft permissions"\
252+
+ f" for BCO {response_id}.",
253+
))
254+
rejected_requests = True
255+
continue
256+
257+
bco = ModifyBcoDraftSerializer(data=object)
258+
259+
if bco.is_valid():
260+
try:
261+
bco.update(bco.validated_data)
262+
response_data.append(response_constructor(
263+
identifier=response_id,
264+
status = "SUCCESS",
265+
code= 200,
266+
message= f"BCO {response_id} updated",
267+
))
268+
accepted_requests = True
269+
270+
except Exception as err:
271+
response_data.append(response_constructor(
272+
identifier=response_id,
273+
status = "SERVER ERROR",
274+
code= 500,
275+
message= f"BCO {response_id} failed",
276+
))
277+
278+
else:
279+
response_data.append(response_constructor(
280+
identifier=response_id,
281+
status = "REJECTED",
282+
code= 400,
283+
message= f"BCO {response_id} rejected",
284+
data=bco.errors
285+
))
286+
rejected_requests = True
287+
288+
status_code = response_status(accepted_requests, rejected_requests)
289+
return Response(status=status_code, data=response_data)
167290

168291
class DraftsModifyApi(APIView):
169292
"""Modify BCO Draft [Bulk Enabled]
@@ -355,6 +478,156 @@ def get(self, request, bco_accession):
355478
bco_counter_increment(bco_instance)
356479
return Response(status=status.HTTP_200_OK, data=bco_instance.contents)
357480

481+
class DraftsPublishApi(APIView):
482+
"""Publish Draft BCO [Bulk Enabled]
483+
484+
API endpoint for publishing BioCompute Object (BCO) drafts, with support
485+
for bulk operations.
486+
487+
This endpoint allows authenticated users to publish existing BCO drafts
488+
individually or in bulk by submitting a list of BCO drafts. The operation
489+
can be performed for one or more drafts in a single request. Each draft is
490+
validated and processed independently, allowing for mixed response
491+
statuses (HTTP_207_MULTI_STATUS) in the case of bulk submissions.
492+
"""
493+
494+
permission_classes = [IsAuthenticated]
495+
496+
@swagger_auto_schema(
497+
operation_id="api_objects_drafts_publish",
498+
request_body=openapi.Schema(
499+
type=openapi.TYPE_ARRAY,
500+
title="Publish BCO Draft Schema",
501+
description="Publish draft BCO [Bulk Enabled]",
502+
items=openapi.Schema(
503+
type=openapi.TYPE_OBJECT,
504+
required=["object_id"],
505+
properties={
506+
"published_object_id": openapi.Schema(
507+
type=openapi.TYPE_STRING,
508+
description="BCO Object Draft ID.",
509+
example="http://127.0.0.1:8000/TEST_000001/1.0"
510+
),
511+
"object_id": openapi.Schema(
512+
type=openapi.TYPE_STRING,
513+
description="BCO Object ID to use for published object.",
514+
example="http://127.0.0.1:8000/TEST_000001/DRAFT"
515+
),
516+
"delete_draft": openapi.Schema(
517+
type=openapi.TYPE_BOOLEAN,
518+
description="Whether or not to delete the draft."\
519+
+" False by default.",
520+
example=False
521+
),
522+
}
523+
)
524+
),
525+
responses={
526+
200: "All requests were accepted.",
527+
207: "Some requests failed and some succeeded. Each object submitted"
528+
" will have it's own response object with it's own status"
529+
" code and message.\n",
530+
400: "All requests were rejected.",
531+
403: "Invalid token.",
532+
},
533+
tags=["BCO Management"],
534+
)
535+
536+
def post(self, request) -> Response:
537+
validator = BcoValidator()
538+
response_data = []
539+
requester = request.user
540+
data = request.data
541+
rejected_requests = False
542+
accepted_requests = False
543+
if 'POST_api_objects_drafts_publish' in request.data:
544+
data = legacy_api_converter(request.data)
545+
546+
for index, object in enumerate(data):
547+
response_id = object.get("object_id", index)
548+
bco_instance = user_can_publish_bco(object, requester)
549+
550+
if bco_instance is None:
551+
response_data.append(response_constructor(
552+
identifier=response_id,
553+
status = "NOT FOUND",
554+
code= 404,
555+
message= f"Invalid BCO: {response_id} does not exist.",
556+
))
557+
rejected_requests = True
558+
continue
559+
560+
if bco_instance is False:
561+
response_data.append(response_constructor(
562+
identifier=response_id,
563+
status = "FORBIDDEN",
564+
code= 403,
565+
message= f"User, {requester}, does not have draft permissions"\
566+
+ f" for BCO {response_id}.",
567+
))
568+
rejected_requests = True
569+
continue
570+
571+
if type(bco_instance) is str:
572+
response_data.append(response_constructor(
573+
identifier=response_id,
574+
status = "BAD REQUEST",
575+
code= 400,
576+
message= bco_instance
577+
))
578+
rejected_requests = True
579+
continue
580+
581+
if type(bco_instance) is tuple:
582+
response_data.append(response_constructor(
583+
identifier=response_id,
584+
status = "BAD REQUEST",
585+
code= 400,
586+
message= f"Invalid `published_object_id`."\
587+
+ f"{bco_instance[0]} and {bco_instance[1]}"\
588+
+ " do not match.",
589+
))
590+
rejected_requests = True
591+
continue
592+
593+
if bco_instance.state == 'PUBLISHED':
594+
object_id = bco_instance.object_id
595+
response_data.append(response_constructor(
596+
identifier=response_id,
597+
status = "CONFLICT",
598+
code= 409,
599+
message= f"Invalid `object_id`: {object_id} already"\
600+
+ " exists.",
601+
))
602+
rejected_requests = True
603+
continue
604+
605+
bco_results = validator.parse_and_validate(bco_instance.contents)
606+
for identifier, result in bco_results.items():
607+
if result["number_of_errors"] > 0:
608+
response_data.append(response_constructor(
609+
identifier=response_id,
610+
status = "REJECTED",
611+
code= 400,
612+
message= f"Publishing BCO {response_id} rejected",
613+
data=bco_results
614+
))
615+
rejected_requests = True
616+
617+
else:
618+
published_bco = publish_draft(bco_instance, requester, object)
619+
identifier=published_bco.object_id
620+
response_data.append(response_constructor(
621+
identifier=identifier,
622+
status = "SUCCESS",
623+
code= 201,
624+
message= f"BCO {identifier} has been published.",
625+
))
626+
accepted_requests = True
627+
628+
status_code = response_status(accepted_requests, rejected_requests)
629+
return Response(status=status_code, data=response_data)
630+
358631
class PublishedRetrieveApi(APIView):
359632
"""Get Published BCO
360633
@@ -374,7 +647,10 @@ class PublishedRetrieveApi(APIView):
374647
- `bco_version`:
375648
Specifies the version of the BCO to be retrieved.
376649
"""
377-
650+
651+
authentication_classes = []
652+
permission_classes = [AllowAny]
653+
378654
@swagger_auto_schema(
379655
operation_id="api_get_published",
380656
manual_parameters=[

0 commit comments

Comments
 (0)