diff --git a/src/apps/api/serializers/competitions.py b/src/apps/api/serializers/competitions.py index 2d1e1639c..58d71c300 100644 --- a/src/apps/api/serializers/competitions.py +++ b/src/apps/api/serializers/competitions.py @@ -245,6 +245,7 @@ class Meta: 'registration_auto_approve', 'queue', 'enable_detailed_results', + 'auto_run_submissions', 'make_programs_available', 'make_input_data_available', 'docker_image', @@ -359,6 +360,7 @@ class Meta: 'submission_count', 'queue', 'enable_detailed_results', + 'auto_run_submissions', 'make_programs_available', 'make_input_data_available', 'docker_image', diff --git a/src/apps/api/serializers/submissions.py b/src/apps/api/serializers/submissions.py index ef3cdf39b..a487168fa 100644 --- a/src/apps/api/serializers/submissions.py +++ b/src/apps/api/serializers/submissions.py @@ -41,6 +41,7 @@ class SubmissionSerializer(serializers.ModelSerializer): on_leaderboard = serializers.BooleanField(read_only=True) task = TaskSerializer() created_when = serializers.DateTimeField(format="%Y-%m-%d %H:%M") + auto_run = serializers.SerializerMethodField(read_only=True) class Meta: model = Submission @@ -66,6 +67,7 @@ class Meta: 'leaderboard', 'on_leaderboard', 'task', + 'auto_run' ) read_only_fields = ( 'pk', @@ -79,6 +81,10 @@ class Meta: def get_filename(self, instance): return basename(instance.data.data_file.name) + def get_auto_run(self, instance): + # returns this submission's competition auto_run_submissions Flag + return instance.phase.competition.auto_run_submissions + class SubmissionLeaderBoardSerializer(serializers.ModelSerializer): scores = SubmissionScoreSerializer(many=True) @@ -149,9 +155,12 @@ def get_filename(self, instance): def create(self, validated_data): tasks = validated_data.pop('tasks', None) - sub = super().create(validated_data) - sub.start(tasks=tasks) + + # 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) return sub diff --git a/src/apps/api/views/submissions.py b/src/apps/api/views/submissions.py index b61963f9b..7bb635ef9 100644 --- a/src/apps/api/views/submissions.py +++ b/src/apps/api/views/submissions.py @@ -93,7 +93,7 @@ def check_object_permissions(self, request, obj): not_bot_user = self.request.user.is_authenticated and not self.request.user.is_bot - if self.action in ['update_fact_sheet', 're_run_submission']: + if self.action in ['update_fact_sheet', 'run_submission', 're_run_submission']: # get_queryset will stop us from re-running something we're not supposed to pass elif not self.request.user.is_authenticated or not_bot_user: @@ -246,6 +246,21 @@ def cancel_submission(self, request, pk): canceled = submission.cancel() return Response({'canceled': canceled}) + @action(detail=True, methods=('POST',)) + def run_submission(self, request, pk): + submission = self.get_object() + + # Only organizer of the competition can run the submission + if not self.has_admin_permission(request.user, submission): + raise PermissionDenied('You do not have permission to run this submission') + + # Allow only to run a submission with status `Submitting` + if submission.status != Submission.SUBMITTING: + raise PermissionDenied('Cannot run a submission which is not in submitting status') + + new_sub = submission.run() + return Response({'id': new_sub.id}) + @action(detail=True, methods=('POST',)) def re_run_submission(self, request, pk): submission = self.get_object() diff --git a/src/apps/competitions/migrations/0045_competition_auto_run_submissions.py b/src/apps/competitions/migrations/0045_competition_auto_run_submissions.py new file mode 100644 index 000000000..86161e98c --- /dev/null +++ b/src/apps/competitions/migrations/0045_competition_auto_run_submissions.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.17 on 2024-01-22 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('competitions', '0044_merge_20231221_1416'), + ] + + operations = [ + migrations.AddField( + model_name='competition', + name='auto_run_submissions', + field=models.BooleanField(default=True), + ), + ] diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index 4b87ec35a..ac316be9f 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -60,6 +60,10 @@ class Competition(ChaHubSaveMixin, models.Model): reward = models.CharField(max_length=256, null=True, blank=True) report = models.CharField(max_length=256, null=True, blank=True) + # if true, submissions are auto-run when submitted + # if false, submissions run will be intiiated by organizer + auto_run_submissions = models.BooleanField(default=True) + def __str__(self): return f"competition-{self.title}-{self.pk}-{self.competition_type}" @@ -538,6 +542,13 @@ def start(self, tasks=None): from .tasks import run_submission run_submission(self.pk, tasks=tasks) + def run(self): + # get tasks from the phase + tasks = self.phase.tasks.all() + # start submission providing the tasks + self.start(tasks=tasks) + return self + def re_run(self, task=None): submission_arg_dict = { 'owner': self.owner, diff --git a/src/apps/competitions/unpackers/v1.py b/src/apps/competitions/unpackers/v1.py index 6802002ac..91496ddca 100644 --- a/src/apps/competitions/unpackers/v1.py +++ b/src/apps/competitions/unpackers/v1.py @@ -23,6 +23,7 @@ def __init__(self, *args, **kwargs): "description": self.competition_yaml.get("description", ""), "docker_image": docker_image, "enable_detailed_results": self.competition_yaml.get('enable_detailed_results', False), + "auto_run_submissions": self.competition_yaml.get('auto_run_submissions', True), "make_programs_available": self.competition_yaml.get('make_programs_available', False), "make_input_data_available": self.competition_yaml.get('make_input_data_available', False), "end_date": self.competition_yaml.get('end_date', None), diff --git a/src/apps/competitions/unpackers/v2.py b/src/apps/competitions/unpackers/v2.py index b3c87b2f4..8de2a42ad 100644 --- a/src/apps/competitions/unpackers/v2.py +++ b/src/apps/competitions/unpackers/v2.py @@ -14,6 +14,7 @@ def __init__(self, *args, **kwargs): "registration_auto_approve": self.competition_yaml.get('registration_auto_approve', False), "docker_image": self.competition_yaml.get('docker_image', 'codalab/codalab-legacy:py37'), "enable_detailed_results": self.competition_yaml.get('enable_detailed_results', False), + "auto_run_submissions": self.competition_yaml.get('auto_run_submissions', True), "make_programs_available": self.competition_yaml.get('make_programs_available', False), "make_input_data_available": self.competition_yaml.get('make_input_data_available', False), "description": self.competition_yaml.get("description", ""), diff --git a/src/static/js/ours/client.js b/src/static/js/ours/client.js index dc789958e..9a8801a6e 100644 --- a/src/static/js/ours/client.js +++ b/src/static/js/ours/client.js @@ -102,6 +102,9 @@ CODALAB.api = { cancel_submission: function (id) { return CODALAB.api.request('GET', `${URLS.API}submissions/${id}/cancel_submission/`) }, + run_submission: function (id) { + return CODALAB.api.request('POST', `${URLS.API}submissions/${id}/run_submission/`) + }, re_run_submission: function (id) { return CODALAB.api.request('POST', `${URLS.API}submissions/${id}/re_run_submission/`) }, diff --git a/src/static/riot/competitions/detail/submission_manager.tag b/src/static/riot/competitions/detail/submission_manager.tag index 7b9aa43d7..7b8f5cb78 100644 --- a/src/static/riot/competitions/detail/submission_manager.tag +++ b/src/static/riot/competitions/detail/submission_manager.tag @@ -94,49 +94,57 @@ + + + {get_score(submission)} - - - + + + + + + - + - + - + + + +
+
+ + +
+ + + + + +
@@ -213,6 +226,7 @@ self.data["description"] = self.markdown_editor.value() self.data["queue"] = self.refs.queue.value self.data["enable_detailed_results"] = self.refs.detailed_results.checked + self.data["auto_run_submissions"] = self.refs.auto_run_submissions.checked self.data["make_programs_available"] = self.refs.make_programs_available.checked self.data["make_input_data_available"] = self.refs.make_input_data_available.checked self.data["docker_image"] = $(self.refs.docker_image).val() @@ -346,6 +360,7 @@ .dropdown('set value', competition.queue.id) } self.refs.detailed_results.checked = competition.enable_detailed_results + self.refs.auto_run_submissions.checked = competition.auto_run_submissions self.refs.make_programs_available.checked = competition.make_programs_available self.refs.make_input_data_available.checked = competition.make_input_data_available $(self.refs.docker_image).val(competition.docker_image)