From 2c0d15c0f1830c9074443e5ccb72dc74ad41fc33 Mon Sep 17 00:00:00 2001 From: Idir Chikhoune Date: Tue, 8 Sep 2026 11:55:57 +0200 Subject: [PATCH 1/2] feature Ok, needs tests --- src/apps/api/serializers/submissions.py | 19 ++++++-- src/apps/competitions/models.py | 4 +- src/apps/competitions/tasks.py | 21 ++++---- src/apps/competitions/urls.py | 1 + src/apps/competitions/views.py | 19 +++++++- src/static/js/ours/client.js | 4 +- .../competitions/detail/submission_upload.tag | 48 +++++++++++++++++-- 7 files changed, 94 insertions(+), 22 deletions(-) diff --git a/src/apps/api/serializers/submissions.py b/src/apps/api/serializers/submissions.py index 9c91737ca..2b9a7913d 100644 --- a/src/apps/api/serializers/submissions.py +++ b/src/apps/api/serializers/submissions.py @@ -3,6 +3,7 @@ from django.core.cache import cache from django.core.exceptions import ValidationError +from profiles.models import CustomGroup from rest_framework import serializers from rest_framework.exceptions import PermissionDenied @@ -86,6 +87,7 @@ class SubmissionCreationSerializer(DefaultUserCreateMixin, serializers.ModelSeri data = serializers.SlugRelatedField(queryset=Data.objects.all(), required=False, allow_null=True, slug_field='key') filename = serializers.SerializerMethodField(read_only=True) tasks = serializers.PrimaryKeyRelatedField(queryset=Task.objects.all(), required=False, write_only=True, many=True) + selected_groups = serializers.PrimaryKeyRelatedField(queryset=CustomGroup.objects.all(), required=False, write_only=True, many=True) phase = serializers.PrimaryKeyRelatedField(queryset=Phase.objects.all(), required=True) queue = serializers.PrimaryKeyRelatedField(queryset=Queue.objects.all(), required=False, allow_null=True) created_when = serializers.DateTimeField(format="%Y-%m-%d %H:%M", required=False) @@ -105,6 +107,7 @@ class Meta: 'secret', 'md5', 'tasks', + 'selected_groups', 'fact_sheet_answers', 'organization', 'queue', @@ -122,12 +125,14 @@ def get_filename(self, instance): def create(self, validated_data): tasks = validated_data.pop('tasks', None) + selected_groups = validated_data.pop('selected_groups', None) sub = super().create(validated_data) # Check if auto_run_submissions is enabled then run the submission # Otherwise organizer will run manually if sub.phase.competition.auto_run_submissions: - sub.start(tasks=tasks) + group_ids = [g.id for g in selected_groups] if selected_groups else None + sub.start(tasks=tasks, group_ids=group_ids) return sub @@ -147,12 +152,20 @@ def validate(self, attrs): elif not value and fact_sheet[key]['is_required'] == 'true' and not isinstance(value, bool): raise ValidationError(f'{fact_sheet[key]["title"]}({key}) requires an answer') - # Make sure selected tasks are part of the phase if attrs.get('tasks'): if not all(_ in attrs['phase'].tasks.all() for _ in attrs['tasks']): raise ValidationError("All tasks must be part of the current phase.") - # Only on create (when we don't have instance set) check permissions + if attrs.get('selected_groups'): + competition = data['phase'].competition + user = self.context['request'].user + valid_group_ids = set( + competition.participant_groups.filter(user=user).values_list('id', flat=True) + ) + submitted_ids = set(g.id for g in attrs['selected_groups']) + if not submitted_ids.issubset(valid_group_ids): + raise ValidationError("You can only submit to groups you are a member of.") + if not self.instance: is_in_competition = data["phase"].competition.participants.filter( user=self.context["request"].user, diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index 97d7ff057..545c07558 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -614,9 +614,9 @@ def save(self, ignore_submission_limit=False, **kwargs): self.phase.competition.submissions_count += 1 self.phase.competition.save() - def start(self, tasks=None): + def start(self, tasks=None, group_ids=None): from .tasks import run_submission - run_submission(self.pk, tasks=tasks) + run_submission(self.pk, tasks=tasks, group_ids=group_ids) def run(self): # get tasks from the phase diff --git a/src/apps/competitions/tasks.py b/src/apps/competitions/tasks.py index 220e93677..9698a1e28 100644 --- a/src/apps/competitions/tasks.py +++ b/src/apps/competitions/tasks.py @@ -123,12 +123,13 @@ ) # time limit of the default queue -def _get_user_group_queues(user, competition): - all_user_groups = list( - competition.participant_groups.filter(user__pk=user.pk) - .select_related("queue") - .distinct() - ) +def _get_user_group_queues(user, competition, selected_group_ids=None): + qs = competition.participant_groups.filter(user__pk=user.pk).select_related("queue").distinct() + + if selected_group_ids is not None: + qs = qs.filter(id__in=selected_group_ids) + + all_user_groups = list(qs) if not all_user_groups: return [] @@ -341,9 +342,9 @@ def create_detailed_output_file(detail_name, submission): return make_url_sassy(new_details.data_file.name, permission="w") -def run_submission(submission_pk, tasks=None, is_scoring=False): +def run_submission(submission_pk, tasks=None, is_scoring=False, group_ids=None): task_ids = [t.id for t in tasks] if tasks else None - return _run_submission.apply_async((submission_pk, task_ids, is_scoring)) + return _run_submission.apply_async((submission_pk, task_ids, is_scoring, group_ids)) def send_submission_message(submission, data): @@ -374,7 +375,7 @@ def send_child_id(submission, child_id): @app.task(queue="site-worker", soft_time_limit=60) -def _run_submission(submission_pk, task_pks=None, is_scoring=False): +def _run_submission(submission_pk, task_pks=None, is_scoring=False, group_ids=None): """This function is wrapped so that when we run tests we can run this function not via celery""" select_models = ( @@ -422,7 +423,7 @@ def _run_submission(submission_pk, task_pks=None, is_scoring=False): if submission.parent is None and not is_scoring: group_queues = _get_user_group_queues( - submission.owner, submission.phase.competition + submission.owner, submission.phase.competition, selected_group_ids=group_ids ) else: group_queues = [] diff --git a/src/apps/competitions/urls.py b/src/apps/competitions/urls.py index b4b1a90ac..c7bf73191 100644 --- a/src/apps/competitions/urls.py +++ b/src/apps/competitions/urls.py @@ -18,4 +18,5 @@ path('/groups/create/', views.competition_create_group, name='competition_create_group'), path('/groups//update/', views.competition_update_group), path('/groups//delete/', views.competition_delete_group), + path('/user_groups/', views.competition_user_groups, name='competition_user_groups'), ] diff --git a/src/apps/competitions/views.py b/src/apps/competitions/views.py index 0070ff363..63429e1af 100644 --- a/src/apps/competitions/views.py +++ b/src/apps/competitions/views.py @@ -5,7 +5,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Q from django.contrib.auth.decorators import login_required -from django.views.decorators.http import require_POST +from django.views.decorators.http import require_GET, require_POST from django.shortcuts import get_object_or_404 from profiles.models import CustomGroup, User @@ -422,3 +422,20 @@ def _group_display_name(stored_name, competition_pk): if stored_name.startswith(prefix): return stored_name[len(prefix):] return stored_name + + +@login_required +@require_GET +def competition_user_groups(request, pk): + competition = get_object_or_404(Competition, pk=pk) + user = request.user + groups = competition.participant_groups.filter(user=user).distinct() + data = [ + { + 'id': g.id, + 'name': _group_display_name(g.name, competition.pk), + 'queue': g.queue.pk if g.queue else None, + } + for g in groups + ] + return JsonResponse(data, safe=False) \ No newline at end of file diff --git a/src/static/js/ours/client.js b/src/static/js/ours/client.js index fa169c5a8..2ed21d6d1 100644 --- a/src/static/js/ours/client.js +++ b/src/static/js/ours/client.js @@ -134,7 +134,9 @@ CODALAB.api = { { pks: pks } // body is JSON by convention ); }, - + get_user_participant_groups: function (competition_id) { + return CODALAB.api.request('GET', `/competitions/${competition_id}/user_groups/`) + }, /*--------------------------------------------------------------------- Leaderboards ---------------------------------------------------------------------*/ diff --git a/src/static/riot/competitions/detail/submission_upload.tag b/src/static/riot/competitions/detail/submission_upload.tag index 830617c23..17268369f 100644 --- a/src/static/riot/competitions/detail/submission_upload.tag +++ b/src/static/riot/competitions/detail/submission_upload.tag @@ -52,6 +52,27 @@ + +