66from django .core .files import File
77from django .test import RequestFactory
88from django .utils import timezone
9+ from django .db import transaction
910
1011from api .serializers .competitions import CompetitionSerializer
1112from api .serializers .leaderboards import LeaderboardSerializer
1415from datasets .models import Data
1516from queues .models import Queue
1617from tasks .models import Task , Solution
18+ from profiles .models import CustomGroup
1719from utils .storage import md5
1820from .utils import CompetitionUnpackingException , zip_if_directory
1921
@@ -27,6 +29,7 @@ def __init__(self, competition_yaml, temp_directory, creator):
2729 self .created_tasks = []
2830 self .created_solutions = []
2931 self .created_datasets = []
32+ self .created_groups = []
3033
3134 # We'll make a fake request to pass to DRF serializers for request.user context
3235 self .fake_request = RequestFactory ()
@@ -198,13 +201,133 @@ def _unpack_terms(self):
198201 raise NotImplementedError
199202
200203 def _unpack_image (self ):
204+
201205 try :
202206 image_name = self .competition_yaml ['image' ]
203207 except KeyError :
204208 raise CompetitionUnpackingException ('An image for this competition could not be found in the yaml' )
205209
206210 self .competition ['logo' ] = self ._read_image (image_name )
207211
212+ def _unpack_groups (self ):
213+ """
214+ Parse groups from YAML.
215+ Expected format:
216+ groups:
217+ - name: "Group A"
218+ queue: "Queue Name" # optional
219+ """
220+ raw = self .competition_yaml .get ('groups' )
221+
222+ if not raw :
223+ self .competition ['participant_groups_raw' ] = []
224+ return
225+
226+ parsed = []
227+
228+ for g in raw :
229+ name = (g .get ('name' ) or '' ).strip ()
230+
231+ if not name :
232+ raise CompetitionUnpackingException (
233+ 'Each group must have a non-empty "name" field.'
234+ )
235+
236+ parsed .append ({
237+ 'name' : name ,
238+ 'queue_field' : g .get ('queue' )
239+ })
240+
241+ self .competition ['participant_groups_raw' ] = parsed
242+
243+
244+
245+ def _save_groups (self , competition ):
246+ """
247+ Create CustomGroup objects and attach them to competition.
248+ If group already exists, it is reused.
249+ """
250+
251+ groups_raw = self .competition .get ('participant_groups_raw' ) or []
252+
253+ if not groups_raw :
254+ return
255+
256+ import uuid
257+
258+ with transaction .atomic ():
259+ for grp in groups_raw :
260+ name = grp ['name' ].strip ()
261+ queue_field = grp .get ('queue_field' )
262+
263+ if not name :
264+ raise CompetitionUnpackingException (
265+ "Participant group name cannot be empty."
266+ )
267+ queue_obj = None
268+
269+ if queue_field :
270+ if isinstance (queue_field , int ) or (
271+ isinstance (queue_field , str ) and queue_field .isdigit ()
272+ ):
273+ queue_obj = Queue .objects .filter (pk = int (queue_field )).first ()
274+
275+ else :
276+ try :
277+ uuid_value = uuid .UUID (str (queue_field ))
278+ queue_obj = Queue .objects .filter (vhost = uuid_value ).first ()
279+ except ValueError :
280+ queue_obj = None
281+
282+ if not queue_obj :
283+ queues = Queue .objects .filter (name = queue_field )
284+
285+ if queues .count () > 1 :
286+ raise CompetitionUnpackingException (
287+ f"Multiple queues found with name '{ queue_field } '. "
288+ f"Use id or UUID instead for group '{ name } '."
289+ )
290+
291+ queue_obj = queues .first ()
292+
293+ if not queue_obj :
294+ raise CompetitionUnpackingException (
295+ f"Queue '{ queue_field } ' does not exist "
296+ f"for group '{ name } '."
297+ )
298+
299+ if not queue_obj .is_public :
300+ organizers = queue_obj .organizers .values_list (
301+ 'username' , flat = True
302+ )
303+
304+ if (
305+ queue_obj .owner != self .creator
306+ and self .creator .username not in organizers
307+ ):
308+ raise CompetitionUnpackingException (
309+ f"You do not have access to queue '{ queue_field } ' "
310+ f"for group '{ name } '."
311+ )
312+
313+ group , created = CustomGroup .objects .get_or_create (
314+ name = name ,
315+ defaults = {'queue' : queue_obj }
316+ )
317+
318+ if not created and queue_field :
319+ if group .queue != queue_obj :
320+ group .queue = queue_obj
321+ group .save ()
322+
323+ if created :
324+ self .created_groups .append (group )
325+
326+ competition .participant_groups .add (group )
327+
328+
329+
330+
208331 def _unpack_queue (self ):
209332 # Get Queue by vhost/uuid. If instance not returned, or we don't have access don't set it!
210333 vhost = self .competition_yaml .get ('queue' )
@@ -343,6 +466,11 @@ def _save_competition(self):
343466 def _clean (self ):
344467 for dataset in self .created_datasets :
345468 dataset .delete ()
469+ for group in getattr (self , 'created_groups' , []):
470+ try :
471+ group .delete ()
472+ except Exception :
473+ pass
346474 for task in self .created_tasks :
347475 task .delete ()
348476 for solution in self .created_solutions :
@@ -353,7 +481,15 @@ def save(self):
353481 self ._save_tasks ()
354482 self ._save_solutions ()
355483 self ._save_leaderboards ()
356- return self ._save_competition ()
484+ self ._unpack_groups ()
485+
486+ # Create competition
487+ competition_instance = self ._save_competition ()
488+
489+ # Create and attach groups
490+ self ._save_groups (competition_instance )
491+
492+ return competition_instance
357493 except Exception as e :
358494 self ._clean ()
359495 raise e
0 commit comments