diff --git a/apps/challenges/models.py b/apps/challenges/models.py index 1b0cb0656a..b7b6228cf9 100644 --- a/apps/challenges/models.py +++ b/apps/challenges/models.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import User from django.contrib.postgres.fields import ArrayField, JSONField from django.core import serializers +from django.core.exceptions import ValidationError from django.db import models from django.db.models import signals from django.db.models.signals import pre_save @@ -354,10 +355,26 @@ def get_end_date(self): @property def is_active(self): """Returns if the challenge is active or not""" - if self.start_date < timezone.now() and self.end_date > timezone.now(): + if ( + self.start_date + and self.end_date + and self.start_date < timezone.now() + and self.end_date > timezone.now() + ): return True return False + def save(self, *args, **kwargs): + if ( + self.start_date + and self.end_date + and self.start_date >= self.end_date + ): + raise ValidationError( + "Challenge start date must be before end date." + ) + super(Challenge, self).save(*args, **kwargs) + @receiver(signals.post_save, sender="challenges.Challenge") def create_eks_cluster_or_ec2_for_challenge( @@ -586,11 +603,58 @@ def get_end_date(self): @property def is_active(self): """Returns if the challenge is active or not""" - if self.start_date < timezone.now() and self.end_date > timezone.now(): + if ( + self.start_date + and self.end_date + and self.start_date < timezone.now() + and self.end_date > timezone.now() + ): return True return False def save(self, *args, **kwargs): + challenge = self.challenge + if challenge: + if ( + self.start_date + and challenge.start_date + and self.start_date < challenge.start_date + ): + raise ValidationError( + "Phase start date must be on or after challenge start date." + ) + if ( + self.start_date + and challenge.end_date + and self.start_date >= challenge.end_date + ): + raise ValidationError( + "Phase start date must be before challenge end date." + ) + if ( + self.end_date + and challenge.start_date + and self.end_date <= challenge.start_date + ): + raise ValidationError( + "Phase end date must be after challenge start date." + ) + if ( + self.end_date + and challenge.end_date + and self.end_date > challenge.end_date + ): + raise ValidationError( + "Phase end date must be on or before challenge end date." + ) + if ( + self.start_date + and self.end_date + and self.start_date >= self.end_date + ): + raise ValidationError( + "Phase start date must be before end date." + ) # If the max_submissions_per_day is less than the # max_concurrent_submissions_allowed. diff --git a/apps/challenges/serializers.py b/apps/challenges/serializers.py index 90fbf3bcba..63f4aa1030 100644 --- a/apps/challenges/serializers.py +++ b/apps/challenges/serializers.py @@ -36,6 +36,19 @@ class ChallengeSerializer(serializers.ModelSerializer): def get_domain_name(self, obj): return obj.get_domain_display() + def validate(self, attrs): + start_date = attrs.get( + "start_date", getattr(self.instance, "start_date", None) + ) + end_date = attrs.get( + "end_date", getattr(self.instance, "end_date", None) + ) + if start_date and end_date and start_date >= end_date: + raise serializers.ValidationError( + "Challenge start date must be before end date." + ) + return attrs + def validate_worker_python_version(self, value): if value in (None, ""): return DEFAULT_WORKER_PYTHON_VERSION @@ -147,6 +160,45 @@ class ChallengePhaseSerializer(serializers.ModelSerializer): is_active = serializers.ReadOnlyField() + def validate(self, attrs): + start_date = attrs.get( + "start_date", getattr(self.instance, "start_date", None) + ) + end_date = attrs.get( + "end_date", getattr(self.instance, "end_date", None) + ) + challenge = attrs.get( + "challenge", getattr(self.instance, "challenge", None) + ) + if challenge: + csd = challenge.start_date + ced = challenge.end_date + if start_date and csd and start_date < csd: + raise serializers.ValidationError( + "Phase start date must be on or after " + "challenge start date." + ) + if start_date and ced and start_date >= ced: + raise serializers.ValidationError( + "Phase start date must be before " + "challenge end date." + ) + if end_date and csd and end_date <= csd: + raise serializers.ValidationError( + "Phase end date must be after " + "challenge start date." + ) + if end_date and ced and end_date > ced: + raise serializers.ValidationError( + "Phase end date must be on or before " + "challenge end date." + ) + if start_date and end_date and start_date >= end_date: + raise serializers.ValidationError( + "Phase start date must be before end date." + ) + return attrs + def __init__(self, *args, **kwargs): super(ChallengePhaseSerializer, self).__init__(*args, **kwargs) context = kwargs.get("context") @@ -441,6 +493,45 @@ class ChallengePhaseCreateSerializer(serializers.ModelSerializer): is_active = serializers.ReadOnlyField() + def validate(self, attrs): + start_date = attrs.get( + "start_date", getattr(self.instance, "start_date", None) + ) + end_date = attrs.get( + "end_date", getattr(self.instance, "end_date", None) + ) + challenge = attrs.get( + "challenge", getattr(self.instance, "challenge", None) + ) + if challenge: + csd = challenge.start_date + ced = challenge.end_date + if start_date and csd and start_date < csd: + raise serializers.ValidationError( + "Phase start date must be on or after " + "challenge start date." + ) + if start_date and ced and start_date >= ced: + raise serializers.ValidationError( + "Phase start date must be before " + "challenge end date." + ) + if end_date and csd and end_date <= csd: + raise serializers.ValidationError( + "Phase end date must be after " + "challenge start date." + ) + if end_date and ced and end_date > ced: + raise serializers.ValidationError( + "Phase end date must be on or before " + "challenge end date." + ) + if start_date and end_date and start_date >= end_date: + raise serializers.ValidationError( + "Phase start date must be before end date." + ) + return attrs + def __init__(self, *args, **kwargs): super(ChallengePhaseCreateSerializer, self).__init__(*args, **kwargs) context = kwargs.get("context") diff --git a/frontend/src/js/controllers/challengeCtrl.js b/frontend/src/js/controllers/challengeCtrl.js index b485cec378..5858221f02 100644 --- a/frontend/src/js/controllers/challengeCtrl.js +++ b/frontend/src/js/controllers/challengeCtrl.js @@ -71,6 +71,7 @@ vm.isSubmissionUsingUrl = null; vm.isSubmissionUsingCli = null; vm.isSubmissionUsingFile = null; + vm.submissionType = null; vm.isRemoteChallenge = false; vm.allowResumingSubmisisons = false; vm.allowCancelRunningSubmissions = false; @@ -738,6 +739,10 @@ vm.projectUrl = ""; vm.publicationUrl = ""; vm.isPublicSubmission = null; + vm.isSubmissionUsingCli = null; + vm.isSubmissionUsingFile = null; + vm.isSubmissionUsingUrl = null; + vm.submissionType = null; $rootScope.notify("success", "Your submission has been recorded succesfully!"); vm.disableSubmit = true; vm.showSubmissionNumbers = false; @@ -755,6 +760,10 @@ vm.projectUrl = null; vm.publicationUrl = null; vm.isPublicSubmission = null; + vm.isSubmissionUsingCli = null; + vm.isSubmissionUsingFile = null; + vm.isSubmissionUsingUrl = null; + vm.submissionType = null; if (status == 404) { vm.subErrors.msg = "Please select phase!"; } else { diff --git a/frontend/src/views/web/challenge/submission.html b/frontend/src/views/web/challenge/submission.html index 860c402169..ebe0763564 100644 --- a/frontend/src/views/web/challenge/submission.html +++ b/frontend/src/views/web/challenge/submission.html @@ -224,14 +224,14 @@
Make Submission

Select submission type:

  • + value="cliSubmission" ng-model="challenge.submissionType" ng-click="challenge.isSubmissionUsingCli = true; challenge.isSubmissionUsingFile = false; challenge.isSubmissionUsingUrl = false">
    Use CLI (for file size > 400MB)
    diff --git a/tests/unit/challenges/test_models.py b/tests/unit/challenges/test_models.py index 0188e851b4..c952730e1e 100644 --- a/tests/unit/challenges/test_models.py +++ b/tests/unit/challenges/test_models.py @@ -13,6 +13,7 @@ LeaderboardData, ) from django.contrib.auth.models import User +from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from django.utils import timezone @@ -34,6 +35,8 @@ def setUp(self): team_name="Test Challenge Host Team", created_by=self.user ) + now = timezone.now() + self.challenge = Challenge.objects.create( title="Test Challenge", description="Description for test challenge", @@ -42,8 +45,8 @@ def setUp(self): creator=self.challenge_host_team, published=False, enable_forum=True, - start_date=timezone.now() - timedelta(days=2), - end_date=timezone.now() + timedelta(days=1), + start_date=now - timedelta(days=2), + end_date=now + timedelta(days=1), anonymous_leaderboard=False, ) @@ -58,8 +61,8 @@ def setUp(self): description="Description for Challenge Phase", leaderboard_public=False, is_public=False, - start_date=timezone.now() - timedelta(days=2), - end_date=timezone.now() + timedelta(days=1), + start_date=now - timedelta(days=1), + end_date=now + timedelta(hours=1), challenge=self.challenge, test_annotation=SimpleUploadedFile( "test_sample_file.txt", @@ -186,6 +189,7 @@ def test_end_date_change_triggers_eks_autoscale_for_docker_challenge( self.challenge.save() mock_trigger_eks_node_autoscale.reset_mock() + self.challenge.start_date = timezone.now() - timedelta(days=5) self.challenge.end_date = timezone.now() - timedelta(days=3) self.challenge.save() @@ -194,6 +198,26 @@ def test_end_date_change_triggers_eks_autoscale_for_docker_challenge( trigger_source="challenge_end_date_changed", ) + def test_save_raises_error_when_start_date_after_end_date(self): + self.challenge.start_date = timezone.now() + timedelta(days=1) + self.challenge.end_date = timezone.now() - timedelta(days=1) + with self.assertRaises(ValidationError): + self.challenge.save() + + def test_save_raises_error_when_start_date_equals_end_date(self): + now = timezone.now() + self.challenge.start_date = now + self.challenge.end_date = now + with self.assertRaises(ValidationError): + self.challenge.save() + + def test_save_succeeds_when_dates_are_valid(self): + self.challenge.start_date = timezone.now() - timedelta(days=2) + self.challenge.end_date = timezone.now() + timedelta(days=2) + self.challenge.save() + self.challenge.refresh_from_db() + self.assertTrue(self.challenge.is_active) + class DatasetSplitTestCase(BaseTestCase): def setUp(self): @@ -270,6 +294,71 @@ def test_submission_artifact_retention_policy_includes_supported_windows( retention_policies, ) + def test_save_raises_error_when_phase_start_before_challenge_start(self): + self.challenge_phase.start_date = self.challenge.start_date - timedelta( + days=1 + ) + with self.assertRaises(ValidationError): + self.challenge_phase.save() + + def test_save_raises_error_when_phase_start_on_or_after_challenge_end(self): + self.challenge_phase.start_date = self.challenge.end_date + with self.assertRaises(ValidationError): + self.challenge_phase.save() + + def test_save_raises_error_when_phase_end_on_or_before_challenge_start(self): + self.challenge_phase.end_date = self.challenge.start_date + with self.assertRaises(ValidationError): + self.challenge_phase.save() + + def test_save_raises_error_when_phase_end_after_challenge_end(self): + self.challenge_phase.end_date = self.challenge.end_date + timedelta(days=1) + with self.assertRaises(ValidationError): + self.challenge_phase.save() + + def test_save_raises_error_when_phase_start_after_phase_end(self): + self.challenge_phase.start_date = self.challenge.start_date + timedelta( + hours=5 + ) + self.challenge_phase.end_date = self.challenge.start_date + timedelta(hours=1) + with self.assertRaises(ValidationError): + self.challenge_phase.save() + + def test_save_succeeds_when_phase_dates_are_within_challenge_dates(self): + self.challenge_phase.start_date = self.challenge.start_date + timedelta( + hours=1 + ) + self.challenge_phase.end_date = self.challenge.end_date - timedelta(hours=1) + self.challenge_phase.save() + self.challenge_phase.refresh_from_db() + self.assertEqual( + self.challenge_phase.start_date, + self.challenge.start_date + timedelta(hours=1), + ) + self.assertEqual( + self.challenge_phase.end_date, + self.challenge.end_date - timedelta(hours=1), + ) + + def test_save_succeeds_when_challenge_dates_are_null(self): + self.challenge.start_date = None + self.challenge.end_date = None + self.challenge.save() + now = timezone.now() + self.challenge_phase.start_date = now - timedelta(days=1) + self.challenge_phase.end_date = now + timedelta(days=1) + self.challenge_phase.save() + self.challenge_phase.refresh_from_db() + self.assertTrue(self.challenge_phase.is_active) + + def test_save_succeeds_when_phase_dates_are_null(self): + self.challenge_phase.start_date = None + self.challenge_phase.end_date = None + self.challenge_phase.save() + self.challenge_phase.refresh_from_db() + self.assertIsNone(self.challenge_phase.start_date) + self.assertIsNone(self.challenge_phase.end_date) + class LeaderboardTestCase(BaseTestCase): def setUp(self): diff --git a/tests/unit/challenges/test_serializers.py b/tests/unit/challenges/test_serializers.py index 0b30b747ce..2251de61ae 100644 --- a/tests/unit/challenges/test_serializers.py +++ b/tests/unit/challenges/test_serializers.py @@ -192,7 +192,7 @@ def setUp(self): leaderboard_public=False, is_public=False, start_date=timezone.now() - timedelta(days=2), - end_date=timezone.now() + timedelta(days=1), + end_date=self.challenge.end_date, challenge=self.challenge, test_annotation=SimpleUploadedFile( "test_sample_file.txt", @@ -331,8 +331,8 @@ def test_internal_challenge_defaults_created_phase_to_days_14(self): "description": "Internal phase", "leaderboard_public": False, "is_public": False, - "start_date": timezone.now() - timedelta(days=1), - "end_date": timezone.now() + timedelta(days=1), + "start_date": self.challenge.start_date, + "end_date": self.challenge.end_date, "max_submissions_per_day": 100000, "max_submissions": 100000, "max_submissions_per_month": 100000, @@ -344,6 +344,12 @@ def test_internal_challenge_defaults_created_phase_to_days_14(self): self.assertTrue(serializer.is_valid(), serializer.errors) challenge_phase = serializer.save() + self.assertLessEqual( + challenge_phase.start_date, self.challenge.end_date + ) + self.assertLessEqual( + challenge_phase.end_date, self.challenge.end_date + ) self.assertEqual( ChallengePhase.DAYS_14, challenge_phase.submission_artifact_retention_policy,