Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/apps/api/serializers/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -105,6 +107,7 @@ class Meta:
'secret',
'md5',
'tasks',
'selected_groups',
'fact_sheet_answers',
'organization',
'queue',
Expand All @@ -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

Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/apps/competitions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 11 additions & 10 deletions src/apps/competitions/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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 = []
Expand Down
1 change: 1 addition & 0 deletions src/apps/competitions/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@
path('<int:pk>/groups/create/', views.competition_create_group, name='competition_create_group'),
path('<int:pk>/groups/<int:group_id>/update/', views.competition_update_group),
path('<int:pk>/groups/<int:group_id>/delete/', views.competition_delete_group),
path('<int:pk>/user_groups/', views.competition_user_groups, name='competition_user_groups'),
]
19 changes: 18 additions & 1 deletion src/apps/competitions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
4 changes: 3 additions & 1 deletion src/static/js/ours/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------------------------------------------------------*/
Expand Down
48 changes: 43 additions & 5 deletions src/static/riot/competitions/detail/submission_upload.tag
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@
</div>
</div>

<div class="ui vertical accordion menu" style="width: 36%;" id="select_groups_accordion" if="{available_groups}">
<div class="item">
<a class="title">
<i class="dropdown icon"></i>
Submit to Group
</a>
<div class="content">
<div class="ui form">
<div class="grouped fields">
<div each="{group in available_groups}" class="field">
<div class="ui checkbox">
<input type="checkbox" name="group-{group.id}" id="group-{group.id}" checked>
<label for="group-{group.id}">{group.name}</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

<div class="ui six wide field">
<label>Submit as:
<span class="ui mini circular icon button"
Expand Down Expand Up @@ -177,6 +198,8 @@
self.datasets = {}
self.organizations = []

self.available_groups = []

self.one('mount', function () {
CODALAB.api.get_user_participant_organizations()
.done((data) => {
Expand All @@ -193,12 +216,17 @@
onClose: () => segment.hide(),
})

// File upload handler
CODALAB.api.get_user_participant_groups(self.opts.competition.id)
.done((data) => {
self.available_groups = data
self.update()
})

$(self.refs.data_file.refs.file_input).on('change', self.check_can_upload)
self.setup_autoscroll()
self.setup_websocket()
})
})

// Function to capture change of `submit as` dropdown
// Redirect to Add organization if selected option is Add Organizaiton
$(document).on('change','#organization_dropdown',function(){
Expand Down Expand Up @@ -432,7 +460,7 @@
let form_array = $(self.refs.form).serializeArray()
let form_json = {}
for (answer of form_array) {
if(!answer['name'].startsWith('task-')){
if(!answer['name'].startsWith('task-') && !answer['name'].startsWith('group-')){
if(answer['value'] === 'true'){
form_json[answer['name']] = true
}
Expand Down Expand Up @@ -460,6 +488,15 @@
} else if(self.selected_tasks.length === 1){
task_ids_to_run = [self.selected_tasks[0].id]
}

let selected_group_ids = []
$('#select_groups_accordion input[type="checkbox"]:checked', self.root).each(function () {
let match = $(this).attr('name').match(/^group-(\d+)$/)
if (match) {
selected_group_ids.push(_.toNumber(match[1]))
}
})

var data_file_metadata = {
type: 'submission',
competition: self.opts.competition.id
Expand All @@ -485,7 +522,8 @@
"fact_sheet_answers": self.get_fact_sheet_answers(),
"tasks": task_ids_to_run,
"organization": organization,
"queue": self.opts.competition.queue ? self.opts.competition.queue.id : null
"queue": self.opts.competition.queue ? self.opts.competition.queue.id : null,
"selected_groups": selected_group_ids
})
.done(function (data) {
CODALAB.events.trigger('new_submission_created', data)
Expand Down