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+ )
725from drf_yasg import openapi
826from drf_yasg .utils import swagger_auto_schema
927from django .conf import settings
1028from django .db import utils
11- from rest_framework . views import APIView
29+ from prefix . selectors import user_can_draft_prefix
1230from 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
1433from rest_framework .response import Response
1534from 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
2136hostname = 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
168291class 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+
358631class 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