From 13e4bfcfd4376ac7cc194c1cb3c372fa7fdc74ac Mon Sep 17 00:00:00 2001 From: Suryansh Pathak <34577232+Suryansh5545@users.noreply.github.com> Date: Wed, 6 Sep 2023 22:47:27 +0530 Subject: [PATCH 1/3] [Backend] Add or delete challenge phases and splits (#4157) * Ability to remove and add challenge phase and phase split * Remove changes to data split * Added check for submission existence * Added function to Zip function too * Added check to delete the challenge phase split * Added fix for challenge phase split removal * Remove not used error messages and add split deletion to zip method --------- Co-authored-by: Gunjan Chhablani --- apps/challenges/challenge_config_utils.py | 49 +------------ apps/challenges/views.py | 86 ++++++++++++++++++++++- 2 files changed, 88 insertions(+), 47 deletions(-) diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index 65040b0db4..4db564dfa2 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -258,10 +258,8 @@ def get_value_from_field(data, base_location, field_name): "dataset_split_schema_errors": "ERROR: Dataset split {} has the following schema errors:\n {}", "dataset_split_addition": "ERROR: Dataset split {} doesn't exist. Addition of a new dataset split after challenge creation is not allowed.", "missing_existing_dataset_split_id": "ERROR: Dataset split {} not found in config. Deletion of existing dataset split after challenge creation is not allowed.", - "challenge_phase_split_not_exist": "ERROR: Challenge phase split (leaderboard_id: {}, challenge_phase_id: {}, dataset_split_id: {}) doesn't exist. Addition of challenge phase split after challenge creation is not allowed.", "challenge_phase_split_schema_errors": "ERROR: Challenge phase split {} has the following schema errors:\n {}", "missing_keys_in_challenge_phase_splits": "ERROR: The following keys are missing in the challenge phase splits of YAML file (phase_split: {}): {}", - "challenge_phase_split_not_found": "ERROR: Challenge phase split (leaderboard_id: {}, challenge_phase_id: {}, dataset_split_id: {}) not found in config. Deletion of existing challenge phase split after challenge creation is not allowed.", "no_key_for_challenge_phase_splits": "ERROR: There is no key for challenge phase splits.", "no_codename_for_challenge_phase": "ERROR: No codename found for the challenge phase. Please add a codename and try again!", "duplicate_codename_for_phase": "ERROR: Duplicate codename {} for phase {}. Please ensure codenames are unique", @@ -269,8 +267,6 @@ def get_value_from_field(data, base_location, field_name): "submission_meta_attribute_option_missing": "ERROR: Please include at least one option in the attribute for challenge phase {}", "missing_submission_meta_attribute_fields": "ERROR: Please enter the following fields for the submission meta attribute in challenge phase {}: {}", "challenge_phase_schema_errors": "ERROR: Challenge phase {} has the following schema errors:\n {}", - "challenge_phase_addition": "ERROR: Challenge phase {} doesn't exist. Addition of a new challenge phase after challenge creation is not allowed.", - "challenge_phase_not_found": "ERROR: Challenge phase {} not found in config. Deletion of existing challenge phase after challenge creation is not allowed.", "is_submission_public_restricted": "ERROR: is_submission_public can't be 'True' for challenge phase '{}' with is_restricted_to_select_one_submission 'True'. Please change is_submission_public to 'False' and try again!", "missing_option_in_submission_meta_attribute": "ERROR: Please include at least one option in the attribute for challenge phase {}", "missing_fields_in_submission_meta_attribute": "ERROR: Please enter the following fields for the submission meta attribute in challenge phase {}: {}", @@ -782,23 +778,8 @@ def validate_challenge_phases(self, current_phase_config_ids): ].format(data["id"], serializer_error) self.error_messages.append(message) else: - if ( - current_phase_config_ids - and int(data["id"]) not in current_phase_config_ids - ): - message = self.error_messages_dict[ - "challenge_phase_addition" - ].format(data["id"]) - self.error_messages.append(message) self.phase_ids.append(data["id"]) - for current_challenge_phase_id in current_phase_config_ids: - if current_challenge_phase_id not in self.phase_ids: - message = self.error_messages_dict[ - "challenge_phase_not_found" - ].format(current_challenge_phase_id) - self.error_messages.append(message) - def validate_challenge_phase_splits(self, current_phase_split_ids): challenge_phase_splits = self.yaml_file_data.get( "challenge_phase_splits" @@ -849,31 +830,13 @@ def validate_challenge_phase_splits(self, current_phase_split_ids): "challenge_phase_id", } if expected_keys.issubset(data.keys()): - if ( - current_phase_split_ids - and ( - data["leaderboard_id"], - data["challenge_phase_id"], - data["dataset_split_id"], - ) - not in current_phase_split_ids - ): - message = self.error_messages_dict[ - "challenge_phase_split_not_exist" - ].format( + challenge_phase_split_uuids.append( + ( data["leaderboard_id"], data["challenge_phase_id"], data["dataset_split_id"], ) - self.error_messages.append(message) - else: - challenge_phase_split_uuids.append( - ( - data["leaderboard_id"], - data["challenge_phase_id"], - data["dataset_split_id"], - ) - ) + ) ( is_mapping_valid, @@ -904,12 +867,6 @@ def validate_challenge_phase_splits(self, current_phase_split_ids): "missing_keys_in_challenge_phase_splits" ].format(phase_split, missing_keys_string) self.error_messages.append(message) - for uuid in current_phase_split_ids: - if uuid not in challenge_phase_split_uuids: - message = self.error_messages_dict[ - "challenge_phase_split_not_found" - ].format(uuid[0], uuid[1], uuid[2]) - self.error_messages.append(message) else: message = self.error_messages_dict[ "no_key_for_challenge_phase_splits" diff --git a/apps/challenges/views.py b/apps/challenges/views.py index c0457843f5..75b24876e7 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -1596,6 +1596,23 @@ def create_challenge_using_zip_file(request, challenge_host_team_pk): # Create Challenge Phase challenge_phase_ids = {} + + # Delete the challenge phase if it is not present in the yaml file + existing_challenge_phases = ChallengePhase.objects.filter(challenge=challenge) + existing_challenge_phase_ids = [str(challenge_phase.config_id) for challenge_phase in existing_challenge_phases] + challenge_phases_data_ids = [str(challenge_phase_data["id"]) for challenge_phase_data in challenge_phases_data] + challenge_phase_ids_to_delete = list(set(existing_challenge_phase_ids) - set(challenge_phases_data_ids)) + for challenge_phase_id_to_delete in challenge_phase_ids_to_delete: + challenge_phase = ChallengePhase.objects.filter(challenge__pk=challenge.pk, config_id=challenge_phase_id_to_delete).first() + submission_exist = Submission.objects.filter(challenge_phase=challenge_phase).exists() + if submission_exist: + response_data = { + "error": "Sorry, you cannot delete a challenge phase with submissions." + } + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + else: + challenge_phase.delete() + for data in challenge_phases_data: # Check for challenge phase description file phase_description_file_path = join( @@ -1694,6 +1711,30 @@ def create_challenge_using_zip_file(request, challenge_host_team_pk): response_data = {"error": message} return Response(response_data, status.HTTP_406_NOT_ACCEPTABLE) + # Delete the challenge phase split if it is not present in the yaml file + existing_challenge_phase_splits = ChallengePhaseSplit.objects.filter(challenge_phase__challenge=challenge) + challenge_phase_splits_set = set() + for challenge_phase_split in existing_challenge_phase_splits: + challenge_phase = challenge_phase_split.challenge_phase + dataset_split = challenge_phase_split.dataset_split + leaderboard = challenge_phase_split.leaderboard + combination = (challenge_phase, dataset_split, leaderboard) + challenge_phase_splits_set.add(combination) + for data in challenge_phase_splits_data: + challenge_phase = challenge_phase_ids[str(data["challenge_phase_id"])] + dataset_split = dataset_split_ids[str(data["dataset_split_id"])] + leaderboard = leaderboard_ids[str(data["leaderboard_id"])] + combination = (challenge_phase, dataset_split, leaderboard) + if combination in challenge_phase_splits_set: + challenge_phase_splits_set.remove(combination) + for challenge_phase_split in challenge_phase_splits_set: + challenge_phase_split_qs = ChallengePhaseSplit.objects.filter( + challenge_phase=challenge_phase_split[0], + dataset_split=challenge_phase_split[1], + leaderboard=challenge_phase_split[2] + ) + challenge_phase_split_qs.delete() + for data in challenge_phase_splits_data: if challenge_phase_ids.get(str(data["challenge_phase_id"])) is None: message = ( @@ -4006,6 +4047,25 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): # Updating ChallengePhase objects challenge_phase_ids = {} challenge_phases_data = yaml_file_data["challenge_phases"] + + # Delete the challenge phase if it is not present in the yaml file + existing_challenge_phases = ChallengePhase.objects.filter(challenge=challenge) + existing_challenge_phase_ids = [str(challenge_phase.config_id) for challenge_phase in existing_challenge_phases] + challenge_phases_data_ids = [str(challenge_phase_data["id"]) for challenge_phase_data in challenge_phases_data] + challenge_phase_ids_to_delete = list(set(existing_challenge_phase_ids) - set(challenge_phases_data_ids)) + for challenge_phase_id_to_delete in challenge_phase_ids_to_delete: + challenge_phase = ChallengePhase.objects.filter( + challenge__pk=challenge.pk, config_id=challenge_phase_id_to_delete + ).first() + submission_exist = Submission.objects.filter(challenge_phase=challenge_phase).exists() + if submission_exist: + response_data = { + "error": "Sorry, you cannot delete a challenge phase with submissions." + } + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + else: + challenge_phase.delete() + for data, challenge_test_annotation_file in zip( challenge_phases_data, files["challenge_test_annotation_files"] ): @@ -4025,7 +4085,6 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): ).first() if ( challenge_test_annotation_file - and not challenge_phase.annotations_uploaded_using_cli ): serializer = ChallengePhaseCreateSerializer( challenge_phase, @@ -4099,6 +4158,31 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): challenge_phase_splits_data = yaml_file_data[ "challenge_phase_splits" ] + + # Delete the challenge phase split if it is not present in the yaml file + existing_challenge_phase_splits = ChallengePhaseSplit.objects.filter(challenge_phase__challenge=challenge) + challenge_phase_splits_set = set() + for challenge_phase_split in existing_challenge_phase_splits: + challenge_phase = challenge_phase_split.challenge_phase + dataset_split = challenge_phase_split.dataset_split + leaderboard = challenge_phase_split.leaderboard + combination = (challenge_phase, dataset_split, leaderboard) + challenge_phase_splits_set.add(combination) + for data in challenge_phase_splits_data: + challenge_phase = challenge_phase_ids[str(data["challenge_phase_id"])] + dataset_split = dataset_split_ids[str(data["dataset_split_id"])] + leaderboard = leaderboard_ids[str(data["leaderboard_id"])] + combination = (challenge_phase, dataset_split, leaderboard) + if combination in challenge_phase_splits_set: + challenge_phase_splits_set.remove(combination) + for challenge_phase_split in challenge_phase_splits_set: + challenge_phase_split_qs = ChallengePhaseSplit.objects.filter( + challenge_phase=challenge_phase_split[0], + dataset_split=challenge_phase_split[1], + leaderboard=challenge_phase_split[2] + ) + challenge_phase_split_qs.delete() + for data in challenge_phase_splits_data: if challenge_phase_ids.get(str(data["challenge_phase_id"])) is None: message = ( From b31ca622c5d922d4e0170ebfac853568f39ed088 Mon Sep 17 00:00:00 2001 From: Suryansh Pathak <34577232+Suryansh5545@users.noreply.github.com> Date: Wed, 6 Sep 2023 22:48:22 +0530 Subject: [PATCH 2/3] [Frontend] Add sponsors and prizes to challenge (#4155) * Added Sponsors and Domain * Changed Prize , sponsor page frontend, editted sponsor model, added test cases * Fix Test case * fixed migration * Fix migrations * Update challenge_config_utils.py * Update challenge_config_utils.py * Fix migration * Fix flake8 issues --------- Co-authored-by: Gunjan Chhablani --- apps/challenges/admin.py | 16 ++ apps/challenges/challenge_config_utils.py | 40 +++++ .../0105_challenge_sponsors_prizes.py | 53 ++++++ apps/challenges/models.py | 43 +++++ apps/challenges/serializers.py | 49 ++++++ apps/challenges/urls.py | 10 ++ apps/challenges/utils.py | 115 +++++++++++++ apps/challenges/views.py | 74 ++++++++- frontend/src/css/modules/challenge.scss | 34 ++++ frontend/src/js/controllers/challengeCtrl.js | 56 +++++++ frontend/src/js/route-config/route-config.js | 18 +++ .../views/web/challenge/challenge-page.html | 4 + frontend/src/views/web/challenge/prizes.html | 32 ++++ .../src/views/web/challenge/sponsors.html | 38 +++++ tests/unit/challenges/test_views.py | 152 +++++++++++++++++- tests/unit/participants/test_views.py | 4 + 16 files changed, 736 insertions(+), 2 deletions(-) create mode 100644 apps/challenges/migrations/0105_challenge_sponsors_prizes.py create mode 100644 frontend/src/views/web/challenge/prizes.html create mode 100644 frontend/src/views/web/challenge/sponsors.html diff --git a/apps/challenges/admin.py b/apps/challenges/admin.py index 761e686cfe..adf9e0a3b1 100644 --- a/apps/challenges/admin.py +++ b/apps/challenges/admin.py @@ -28,6 +28,8 @@ PWCChallengeLeaderboard, StarChallenge, UserInvitation, + ChallengeSponsor, + ChallengePrize, ) @@ -432,3 +434,17 @@ def get_challenge_name_and_id(self, obj): get_challenge_name_and_id.short_description = "Challenge Name - ID" get_challenge_name_and_id.admin_order_field = "challenge_phase__challenge" + + +@admin.register(ChallengeSponsor) +class ChallengeSponsorAdmin(ImportExportTimeStampedAdmin): + list_display = ("id", "name", "website") + list_filter = ("name",) + search_fields = ("id", "name") + + +@admin.register(ChallengePrize) +class ChallengePrizeAdmin(ImportExportTimeStampedAdmin): + list_display = ("id", "amount", "rank") + list_filter = ("rank",) + search_fields = ("id", "rank") diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index 4db564dfa2..39dcb82f20 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -5,6 +5,7 @@ from django.core.files.base import ContentFile +import re from os.path import basename, isfile, join from challenges.models import ChallengePhase, ChallengePhaseSplit, DatasetSplit, Leaderboard, Challenge from rest_framework import status @@ -277,6 +278,11 @@ def get_value_from_field(data, base_location, field_name): "extra_tags": "ERROR: Tags are limited to 4. Please remove extra tags then try again!", "wrong_domain": "ERROR: Domain name is incorrect. Please enter correct domain name then try again!", "duplicate_combinations_in_challenge_phase_splits": "ERROR: Duplicate combinations of leaderboard_id {}, challenge_phase_id {} and dataset_split_id {} found in challenge phase splits.", + "sponsor_not_found": "ERROR: Sponsor name or url not found in YAML data.", + "prize_not_found": "ERROR: Prize rank or amount not found in YAML data.", + "duplicate_rank": "ERROR: Duplicate rank {} found in YAML data.", + "prize_amount_wrong": "ERROR: Invalid amount value {}. Amount should be in decimal format with three-letter currency code (e.g. 100.00USD, 500EUR, 1000INR).", + "prize_rank_wrong": "ERROR: Invalid rank value {}. Rank should be an integer.", } @@ -948,6 +954,35 @@ def check_domain(self): message = self.error_messages_dict["wrong_domain"] self.error_messages.append(message) + def check_sponsor(self): + # Verify Sponsor is correct + if "sponsors" in self.yaml_file_data: + for sponsor in self.yaml_file_data["sponsors"]: + if 'name' not in sponsor or 'website' not in sponsor: + message = self.error_messages_dict["sponsor_not_found"] + self.error_messages.append(message) + + def check_prizes(self): + # Verify Prizes are correct + if "prizes" in self.yaml_file_data: + rank_set = set() + for prize in self.yaml_file_data["prizes"]: + if 'rank' not in prize or 'amount' not in prize: + message = self.error_messages_dict["prize_not_found"] + self.error_messages.append(message) + # Check for duplicate rank. + rank = prize['rank'] + if rank in rank_set: + message = self.error_messages_dict["duplicate_rank"].format(rank) + self.error_messages.append(message) + rank_set.add(rank) + if not isinstance(rank, int) or rank < 1: + message = self.error_messages_dict["invalid_rank"].format(rank) + self.error_messages.append(message) + if not re.match(r'^\d+(\.\d{1,2})?[A-Z]{3}$', prize["amount"]): + message = self.error_messages_dict["invalid_amount"].format(prize["amount"]) + self.error_messages.append(message) + def validate_challenge_config_util( request, @@ -1063,6 +1098,11 @@ def validate_challenge_config_util( # Validate domain val_config_util.check_domain() + # Check for Sponsor + # val_config_util.check_sponsor() + + # Check for Prize + val_config_util.check_prizes() return ( val_config_util.error_messages, diff --git a/apps/challenges/migrations/0105_challenge_sponsors_prizes.py b/apps/challenges/migrations/0105_challenge_sponsors_prizes.py new file mode 100644 index 0000000000..352aa97fc9 --- /dev/null +++ b/apps/challenges/migrations/0105_challenge_sponsors_prizes.py @@ -0,0 +1,53 @@ +# Generated by Django 2.2.20 on 2023-08-11 17:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('challenges', '0104_challenge_evaluation_module_error'), + ] + + operations = [ + migrations.AddField( + model_name='challenge', + name='has_prize', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='challenge', + name='has_sponsors', + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name='ChallengeSponsor', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(blank=True, max_length=200, null=True)), + ('website', models.URLField(blank=True, null=True)), + ('challenge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='challenges.Challenge')), + ], + options={ + 'db_table': 'challenge_sponsor', + }, + ), + migrations.CreateModel( + name='ChallengePrize', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('amount', models.CharField(max_length=10)), + ('description', models.CharField(blank=True, max_length=25, null=True)), + ('rank', models.PositiveIntegerField()), + ('challenge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='challenges.Challenge')), + ], + options={ + 'db_table': 'challenge_prize', + }, + ), + ] diff --git a/apps/challenges/models.py b/apps/challenges/models.py index 65304d0377..978b8042e7 100644 --- a/apps/challenges/models.py +++ b/apps/challenges/models.py @@ -69,6 +69,8 @@ def __init__(self, *args, **kwargs): ) domain = models.CharField(max_length=50, choices=DOMAIN_OPTIONS, null=True, blank=True) list_tags = ArrayField(models.TextField(null=True, blank=True), default=list, blank=True) + has_prize = models.BooleanField(default=False) + has_sponsors = models.BooleanField(default=False) published = models.BooleanField( default=False, verbose_name="Publicly Available", db_index=True ) @@ -666,3 +668,44 @@ class PWCChallengeLeaderboard(TimeStampedModel): class Meta: app_label = "challenges" db_table = "pwc_challenge_leaderboard" + + +class ChallengeSponsor(TimeStampedModel): + """ + Model to store challenge sponsors + Arguments: + TimeStampedModel {[model class]} -- An abstract base class model that provides self-managed `created_at` and + `modified_at` fields. + """ + + challenge = models.ForeignKey("Challenge", on_delete=models.CASCADE) + name = models.CharField(max_length=200, blank=True, null=True) + website = models.URLField(max_length=200, blank=True, null=True) + + class Meta: + app_label = "challenges" + db_table = "challenge_sponsor" + + def __str__(self): + return f"Sponsor for {self.challenge}: {self.name}" + + +class ChallengePrize(TimeStampedModel): + """ + Model to store challenge prizes + Arguments: + TimeStampedModel {[model class]} -- An abstract base class model that provides self-managed `created_at` and + `modified_at` fields. + """ + + challenge = models.ForeignKey("Challenge", on_delete=models.CASCADE) + amount = models.CharField(max_length=10) + description = models.CharField(max_length=25, blank=True, null=True) + rank = models.PositiveIntegerField() + + class Meta: + app_label = "challenges" + db_table = "challenge_prize" + + def __str__(self): + return f"Prize for {self.challenge}: Rank {self.rank}, Amount {self.amount}" diff --git a/apps/challenges/serializers.py b/apps/challenges/serializers.py index 5b5c5c82a3..0e6e91c726 100644 --- a/apps/challenges/serializers.py +++ b/apps/challenges/serializers.py @@ -16,6 +16,8 @@ PWCChallengeLeaderboard, StarChallenge, UserInvitation, + ChallengePrize, + ChallengeSponsor, ) @@ -53,6 +55,8 @@ class Meta: "domain", "domain_name", "list_tags", + "has_prize", + "has_sponsors", "published", "submission_time_limit", "is_registration_open", @@ -560,3 +564,48 @@ class Meta: "result", "error", ) + + +class ChallengePrizeSerializer(serializers.ModelSerializer): + """ + Serialize the ChallengePrize Model. + """ + + def __init__(self, *args, **kwargs): + super(ChallengePrizeSerializer, self).__init__(*args, **kwargs) + context = kwargs.get("context") + if context: + challenge = context.get("challenge") + if challenge: + kwargs["data"]["challenge"] = challenge.pk + + class Meta: + model = ChallengePrize + fields = ( + "challenge", + "amount", + "rank", + "description" + ) + + +class ChallengeSponsorSerializer(serializers.ModelSerializer): + """ + Serialize the ChallengeSponsor Model. + """ + + def __init__(self, *args, **kwargs): + super(ChallengeSponsorSerializer, self).__init__(*args, **kwargs) + context = kwargs.get("context") + if context: + challenge = context.get("challenge") + if challenge: + kwargs["data"]["challenge"] = challenge.pk + + class Meta: + model = ChallengeSponsor + fields = ( + "challenge", + "name", + "website" + ) diff --git a/apps/challenges/urls.py b/apps/challenges/urls.py index bd4b67b298..60e4c1db2b 100644 --- a/apps/challenges/urls.py +++ b/apps/challenges/urls.py @@ -284,6 +284,16 @@ views.update_challenge_approval, name="update_challenge_approval", ), + url( + r"^challenge/(?P[0-9]+)/prizes/$", + views.get_prizes_by_challenge, + name="get_prizes_by_challenge", + ), + url( + r"^challenge/(?P[0-9]+)/sponsors/$", + views.get_sponsors_by_challenge, + name="get_sponsors_by_challenge", + ), ] app_name = "challenges" diff --git a/apps/challenges/utils.py b/apps/challenges/utils.py index 356b0010d6..11cd79a134 100644 --- a/apps/challenges/utils.py +++ b/apps/challenges/utils.py @@ -24,6 +24,14 @@ DatasetSplit, ChallengePhaseSplit, ParticipantTeam, + ChallengePrize, + ChallengeSponsor +) + +from .serializers import ( + ChallengePrizeSerializer, + ChallengeSponsorSerializer + ) logger = logging.getLogger(__name__) @@ -509,3 +517,110 @@ def add_domain_to_challenge(yaml_file_data, challenge): else: challenge.domain = None challenge.save() + + +def add_prizes_to_challenge(yaml_file_data, challenge): + if "prizes" in yaml_file_data: + prizes_data = yaml_file_data['prizes'] + rank_set = set() + for prize in prizes_data: + if 'rank' not in prize or 'amount' not in prize: + message = "Prize rank or amount not found in YAML data." + response_data = {"error": message} + return response_data + + # Check for duplicate rank. + rank = prize['rank'] + if rank in rank_set: + message = f"Duplicate rank {rank} found in YAML data." + response_data = {"error": message} + return response_data + rank_set.add(rank) + + rank = prize['rank'] + amount = prize["amount"] + description = prize["description"] + + prize_obj = ChallengePrize.objects.filter(rank=rank, challenge=challenge).first() + if prize_obj: + data = { + "amount": amount, + "description": description, + } + serializer = ChallengePrizeSerializer( + prize_obj, data=data, + context={ + "challenge": challenge, + }, + partial=True + ) + else: + data = { + "challenge": challenge, + "amount": amount, + "rank": rank, + "description": description, + } + serializer = ChallengePrizeSerializer( + data=data, + context={ + "challenge": challenge, + } + ) + if serializer.is_valid(): + challenge.has_prize = True + challenge.save() + serializer.save() + else: + message = serializer.errors + challenge.has_prize = False + challenge.save() + else: + challenge.has_prize = False + challenge.save() + + +def add_sponsors_to_challenge(yaml_file_data, challenge): + if "sponsors" in yaml_file_data: + sponsors_data = yaml_file_data['sponsors'] + sponsor_name_set = set() + + for sponsor in sponsors_data: + # Checking if the sponsors already exists in the database. + sponsor_name_set.add(sponsor['name']) + check_sponsor_status = ChallengeSponsor.objects.filter(name=sponsor['name'], challenge=challenge).exists() + if not check_sponsor_status: + if 'name' not in sponsor or 'website' not in sponsor: + message = "Sponsor name or url not found in YAML data." + response_data = {"error": message} + return response_data + data = { + "name": sponsor['name'], + "website": sponsor['website'], + } + serializer = ChallengeSponsorSerializer( + data=data, + context={ + "challenge": challenge, + } + ) + if serializer.is_valid(): + serializer.save() + challenge.has_sponsors = True + challenge.save() + else: + message = serializer.errors + challenge.has_sponsors = False + challenge.save() + raise response_data + + # Check if the sponsor exist in database. but not in the YAML file. + existing_sponsors = ChallengeSponsor.objects.filter(challenge=challenge) + existing_sponsors_names = [sponsor.name for sponsor in existing_sponsors] + for existing_sponsor in existing_sponsors_names: + if existing_sponsor is not None and existing_sponsor not in sponsor_name_set: + ChallengeSponsor.objects.filter(challenge=challenge, name=existing_sponsor).delete() + + else: + challenge.has_sponsors = False + challenge.save() diff --git a/apps/challenges/views.py b/apps/challenges/views.py index 75b24876e7..731a5f464b 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -70,6 +70,8 @@ parse_submission_meta_attributes, add_domain_to_challenge, add_tags_to_challenge, + add_prizes_to_challenge, + add_sponsors_to_challenge, ) from challenges.challenge_config_utils import ( download_and_write_file, @@ -111,6 +113,8 @@ PWCChallengeLeaderboard, StarChallenge, UserInvitation, + ChallengePrize, + ChallengeSponsor, ) from .permissions import IsChallengeCreator from .serializers import ( @@ -128,7 +132,9 @@ UserInvitationSerializer, ZipChallengeSerializer, ZipChallengePhaseSplitSerializer, - LeaderboardDataSerializer + LeaderboardDataSerializer, + ChallengePrizeSerializer, + ChallengeSponsorSerializer, ) from .aws_utils import ( @@ -1580,6 +1586,16 @@ def create_challenge_using_zip_file(request, challenge_host_team_pk): if verify_complete is not None: return Response(verify_complete, status=status.HTTP_400_BAD_REQUEST) + # Add Sponsors + error_messages = add_sponsors_to_challenge(yaml_file_data, challenge) + if error_messages is not None: + return Response(error_messages, status=status.HTTP_400_BAD_REQUEST) + + # Add Prizes + error_messages = add_prizes_to_challenge(yaml_file_data, challenge) + if error_messages is not None: + return Response(error_messages, status=status.HTTP_400_BAD_REQUEST) + # Create Leaderboard yaml_file_data_of_leaderboard = yaml_file_data["leaderboard"] leaderboard_ids = {} @@ -2676,6 +2692,42 @@ def get_domain_choices(request): return Response(response_data, status.HTTP_405_METHOD_NOT_ALLOWED) +@api_view(["GET"]) +@throttle_classes([UserRateThrottle]) +def get_prizes_by_challenge(request, challenge_pk): + """ + Returns a list of prizes for a given challenge. + """ + try: + challenge = Challenge.objects.get(pk=challenge_pk) + except Challenge.DoesNotExist: + response_data = {"error": "Challenge does not exist"} + return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) + + prizes = ChallengePrize.objects.filter(challenge=challenge) + serializer = ChallengePrizeSerializer(prizes, many=True) + response_data = serializer.data + return Response(response_data, status=status.HTTP_200_OK) + + +@api_view(["GET"]) +@throttle_classes([UserRateThrottle]) +def get_sponsors_by_challenge(request, challenge_pk): + """ + Returns a list of sponsors for a given challenge. + """ + try: + challenge = Challenge.objects.get(pk=challenge_pk) + except Challenge.DoesNotExist: + response_data = {"error": "Challenge does not exist"} + return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) + + sponsors = ChallengeSponsor.objects.filter(challenge=challenge) + serializer = ChallengeSponsorSerializer(sponsors, many=True) + response_data = serializer.data + return Response(response_data, status=status.HTTP_200_OK) + + @api_view(["GET", "POST"]) @throttle_classes([UserRateThrottle]) @permission_classes((permissions.IsAuthenticatedOrReadOnly, HasVerifiedEmail)) @@ -3766,6 +3818,16 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): if verify_complete is not None: return Response(verify_complete, status=status.HTTP_400_BAD_REQUEST) + # Add Sponsors + error_messages = add_sponsors_to_challenge(yaml_file_data, challenge) + if error_messages is not None: + return Response(error_messages, status=status.HTTP_400_BAD_REQUEST) + + # Add Prizes + error_messages = add_prizes_to_challenge(yaml_file_data, challenge) + if error_messages is not None: + return Response(error_messages, status=status.HTTP_400_BAD_REQUEST) + # Create Leaderboard yaml_file_data_of_leaderboard = yaml_file_data[ "leaderboard" @@ -4017,6 +4079,16 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): if verify_complete is not None: return Response(verify_complete, status=status.HTTP_400_BAD_REQUEST) + # Add/Update Sponsors + error_messages = add_sponsors_to_challenge(yaml_file_data, challenge) + if error_messages is not None: + return Response(error_messages, status=status.HTTP_400_BAD_REQUEST) + + # Add/Update Prizes + error_messages = add_prizes_to_challenge(yaml_file_data, challenge) + if error_messages is not None: + return Response(error_messages, status=status.HTTP_400_BAD_REQUEST) + # Updating Leaderboard object leaderboard_ids = {} yaml_file_data_of_leaderboard = yaml_file_data["leaderboard"] diff --git a/frontend/src/css/modules/challenge.scss b/frontend/src/css/modules/challenge.scss index 1ba839c878..337aa741d6 100644 --- a/frontend/src/css/modules/challenge.scss +++ b/frontend/src/css/modules/challenge.scss @@ -336,4 +336,38 @@ md-select .md-select-value span:first-child:after { .worker-actions-row { margin-bottom: 10px; +} + +.ev-card-prize { + height: 50px; + overflow: hidden; +} + +.trophy-icon { + padding: 10px; + float: right !important; +} + +.trophy-gold { + color: #FFD700; +} + +.trophy-silver { + color: #C0C0C0; +} + +.trophy-bronze { + color: #CD7F32; +} + +.trophy-blacks { + color: #000000; +} + +.domain-filter { + margin-top: 45px; +} + +.filter-icon { + padding: 10px; } \ No newline at end of file diff --git a/frontend/src/js/controllers/challengeCtrl.js b/frontend/src/js/controllers/challengeCtrl.js index 0285d7dea0..c8bf18a82b 100644 --- a/frontend/src/js/controllers/challengeCtrl.js +++ b/frontend/src/js/controllers/challengeCtrl.js @@ -31,6 +31,8 @@ vm.isActive = false; vm.phases = {}; vm.phaseSplits = {}; + vm.hasPrizes = false; + vm.has_sponsors = false; vm.orderLeaderboardBy = decodeURIComponent($stateParams.metric); vm.phaseSplitLeaderboardSchema = {}; vm.submissionMetaAttributes = []; // Stores the attributes format and phase ID for all the phases of a challenge. @@ -390,6 +392,8 @@ vm.allowParticipantsResubmissions = details.allow_participants_resubmissions; vm.selectedWorkerResources = [details.worker_cpu_cores, details.worker_memory]; vm.manual_participant_approval = details.manual_participant_approval; + vm.hasPrizes = details.has_prize; + vm.has_sponsors = details.has_sponsors; vm.queueName = details.queue; vm.evaluation_module_error = details.evaluation_module_error; vm.getTeamName(vm.challengeId); @@ -398,6 +402,43 @@ } + + // Get challenge prizes + vm.prizes = []; + if (vm.hasPrizes) { + parameters.url = 'challenges/challenge/' + vm.challengeId + '/prizes/'; + parameters.method = 'GET'; + parameters.data = {}; + parameters.callback = { + onSuccess: function(response) { + vm.prizes = response.data; + }, + onError: function(response) { + var error = response.data; + $rootScope.notify("error", error); + } + }; + utilities.sendRequest(parameters); + } + + // Get challenge Sponsors + vm.sponsors = {}; + if (vm.has_sponsors) { + parameters.url = 'challenges/challenge/' + vm.challengeId + '/sponsors/'; + parameters.method = 'GET'; + parameters.data = {}; + parameters.callback = { + onSuccess: function(response) { + vm.sponsors = response.data; + }, + onError: function(response) { + var error = response.data; + $rootScope.notify("error", error); + } + }; + utilities.sendRequest(parameters); + } + if (userKey) { // get details of challenges corresponding to participant teams of that user @@ -3065,6 +3106,21 @@ } vm.leaderboardDropdown = !vm.leaderboardDropdown; }; + + vm.getTrophySize = function(rank) { + switch (rank) { + case 1: + return 'trophy-gold'; + case 2: + return 'trophy-silver'; + case 3: + return 'trophy-bronze'; + // Add more cases for other ranks if needed + default: + return 'trophy-black'; // Default size, change this according to your preference + } + }; + } })(); diff --git a/frontend/src/js/route-config/route-config.js b/frontend/src/js/route-config/route-config.js index 66dec96c83..cc678f2c82 100644 --- a/frontend/src/js/route-config/route-config.js +++ b/frontend/src/js/route-config/route-config.js @@ -325,6 +325,22 @@ title: 'Leaderboard', }; + var prizes = { + name: "web.challenge-main.challenge-page.prizes", + parent: "web.challenge-main.challenge-page", + url: "/prizes", + templateUrl: baseUrl + "/web/challenge/prizes.html", + title: 'Prizes', + }; + + var sponsors = { + name: "web.challenge-main.challenge-page.sponsors", + parent: "web.challenge-main.challenge-page", + url: "/sponsors", + templateUrl: baseUrl + "/web/challenge/sponsors.html", + title: 'Sponsors', + }; + var manage = { name: "web.challenge-main.challenge-page.manage", parent: "web.challenge-main.challenge-page", @@ -622,6 +638,8 @@ $stateProvider.state(my_challenge_all_submission); $stateProvider.state(approval_team); $stateProvider.state(leaderboard); + $stateProvider.state(prizes); + $stateProvider.state(sponsors); $stateProvider.state(challenge_phase_leaderboard); $stateProvider.state(challenge_phase_metric_leaderboard); diff --git a/frontend/src/views/web/challenge/challenge-page.html b/frontend/src/views/web/challenge/challenge-page.html index f064d314e3..dd6373771a 100644 --- a/frontend/src/views/web/challenge/challenge-page.html +++ b/frontend/src/views/web/challenge/challenge-page.html @@ -137,6 +137,10 @@

{{challenge.page.title}} Team Approval
  • Leaderboard
  • +
  • Prizes
  • +
  • Sponsors
  • Manage
  • + + \ No newline at end of file diff --git a/frontend/src/views/web/challenge/sponsors.html b/frontend/src/views/web/challenge/sponsors.html new file mode 100644 index 0000000000..9ad45af018 --- /dev/null +++ b/frontend/src/views/web/challenge/sponsors.html @@ -0,0 +1,38 @@ +
    +
    +
    +
    +
    Sponsors
    +
    +
    +
    +
    + + + + + + + + + + + + + +
    + Name + + Url +
    {{sponsor.name}} + + {{ sponsor.website}} +
    +
    +

    {{challenge.prizes.error.error}}

    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index 1dcfbb1ff5..6ad3a66c77 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -14,7 +14,7 @@ from challenges.models import (Challenge, ChallengeConfiguration, ChallengePhase, ChallengePhaseSplit, DatasetSplit, Leaderboard, StarChallenge, - LeaderboardData) + LeaderboardData, ChallengePrize, ChallengeSponsor) from django.conf import settings from django.contrib.auth.models import User from django.core.files.uploadedfile import SimpleUploadedFile @@ -153,6 +153,8 @@ def test_get_challenge(self): "domain": self.challenge.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge.list_tags, + "has_prize": self.challenge.has_prize, + "has_sponsors": self.challenge.has_sponsors, "published": self.challenge.published, "submission_time_limit": self.challenge.submission_time_limit, "is_registration_open": self.challenge.is_registration_open, @@ -502,6 +504,8 @@ def test_get_particular_challenge(self): "domain": self.challenge.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge.list_tags, + "has_prize": self.challenge.has_prize, + "has_sponsors": self.challenge.has_sponsors, "published": self.challenge.published, "submission_time_limit": self.challenge.submission_time_limit, "is_registration_open": self.challenge.is_registration_open, @@ -596,6 +600,8 @@ def test_update_challenge_when_user_is_its_creator(self): "domain": self.challenge.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge.list_tags, + "has_prize": self.challenge.has_prize, + "has_sponsors": self.challenge.has_sponsors, "published": self.challenge.published, "submission_time_limit": self.challenge.submission_time_limit, "is_registration_open": self.challenge.is_registration_open, @@ -711,6 +717,8 @@ def test_particular_challenge_partial_update(self): "domain": self.challenge.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge.list_tags, + "has_prize": self.challenge.has_prize, + "has_sponsors": self.challenge.has_sponsors, "published": self.challenge.published, "submission_time_limit": self.challenge.submission_time_limit, "is_registration_open": self.challenge.is_registration_open, @@ -782,6 +790,8 @@ def test_particular_challenge_update(self): "domain": self.challenge.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge.list_tags, + "has_prize": self.challenge.has_prize, + "has_sponsors": self.challenge.has_sponsors, "published": self.challenge.published, "submission_time_limit": self.challenge.submission_time_limit, "is_registration_open": self.challenge.is_registration_open, @@ -1378,6 +1388,8 @@ def test_get_past_challenges(self): "domain": self.challenge3.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge3.list_tags, + "has_prize": self.challenge3.has_prize, + "has_sponsors": self.challenge3.has_sponsors, "published": self.challenge3.published, "submission_time_limit": self.challenge3.submission_time_limit, "is_registration_open": self.challenge3.is_registration_open, @@ -1455,6 +1467,8 @@ def test_get_present_challenges(self): "domain": self.challenge2.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge2.list_tags, + "has_prize": self.challenge2.has_prize, + "has_sponsors": self.challenge2.has_sponsors, "published": self.challenge2.published, "submission_time_limit": self.challenge2.submission_time_limit, "is_registration_open": self.challenge2.is_registration_open, @@ -1532,6 +1546,8 @@ def test_get_future_challenges(self): "domain": self.challenge4.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge4.list_tags, + "has_prize": self.challenge4.has_prize, + "has_sponsors": self.challenge4.has_sponsors, "published": self.challenge4.published, "submission_time_limit": self.challenge4.submission_time_limit, "is_registration_open": self.challenge4.is_registration_open, @@ -1609,6 +1625,8 @@ def test_get_all_challenges(self): "domain": self.challenge4.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge4.list_tags, + "has_prize": self.challenge4.has_prize, + "has_sponsors": self.challenge4.has_sponsors, "published": self.challenge4.published, "submission_time_limit": self.challenge4.submission_time_limit, "is_registration_open": self.challenge4.is_registration_open, @@ -1670,6 +1688,8 @@ def test_get_all_challenges(self): "domain": self.challenge3.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge3.list_tags, + "has_prize": self.challenge3.has_prize, + "has_sponsors": self.challenge3.has_sponsors, "published": self.challenge3.published, "submission_time_limit": self.challenge3.submission_time_limit, "is_registration_open": self.challenge3.is_registration_open, @@ -1731,6 +1751,8 @@ def test_get_all_challenges(self): "domain": self.challenge2.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge2.list_tags, + "has_prize": self.challenge2.has_prize, + "has_sponsors": self.challenge2.has_sponsors, "published": self.challenge2.published, "submission_time_limit": self.challenge2.submission_time_limit, "is_registration_open": self.challenge2.is_registration_open, @@ -1863,6 +1885,8 @@ def test_get_featured_challenges(self): "domain": self.challenge3.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge3.list_tags, + "has_prize": self.challenge3.has_prize, + "has_sponsors": self.challenge3.has_sponsors, "published": self.challenge3.published, "submission_time_limit": self.challenge3.submission_time_limit, "is_registration_open": self.challenge3.is_registration_open, @@ -2019,6 +2043,8 @@ def test_get_challenge_by_pk_when_user_is_challenge_host(self): "domain": self.challenge3.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge3.list_tags, + "has_prize": self.challenge3.has_prize, + "has_sponsors": self.challenge3.has_sponsors, "published": self.challenge3.published, "submission_time_limit": self.challenge3.submission_time_limit, "is_registration_open": self.challenge3.is_registration_open, @@ -2104,6 +2130,8 @@ def test_get_challenge_by_pk_when_user_is_participant(self): "domain": self.challenge4.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge4.list_tags, + "has_prize": self.challenge4.has_prize, + "has_sponsors": self.challenge4.has_sponsors, "published": self.challenge4.published, "submission_time_limit": self.challenge4.submission_time_limit, "is_registration_open": self.challenge4.is_registration_open, @@ -2249,6 +2277,8 @@ def test_get_challenge_when_host_team_is_given(self): "domain": self.challenge2.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge2.list_tags, + "has_prize": self.challenge2.has_prize, + "has_sponsors": self.challenge2.has_sponsors, "published": self.challenge2.published, "submission_time_limit": self.challenge2.submission_time_limit, "is_registration_open": self.challenge2.is_registration_open, @@ -2322,6 +2352,8 @@ def test_get_challenge_when_participant_team_is_given(self): "domain": self.challenge2.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge2.list_tags, + "has_prize": self.challenge2.has_prize, + "has_sponsors": self.challenge2.has_sponsors, "published": self.challenge2.published, "submission_time_limit": self.challenge2.submission_time_limit, "is_registration_open": self.challenge2.is_registration_open, @@ -2395,6 +2427,8 @@ def test_get_challenge_when_mode_is_participant(self): "domain": self.challenge2.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge2.list_tags, + "has_prize": self.challenge2.has_prize, + "has_sponsors": self.challenge2.has_sponsors, "published": self.challenge2.published, "submission_time_limit": self.challenge2.submission_time_limit, "is_registration_open": self.challenge2.is_registration_open, @@ -2466,6 +2500,8 @@ def test_get_challenge_when_mode_is_host(self): "domain": self.challenge.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge.list_tags, + "has_prize": self.challenge.has_prize, + "has_sponsors": self.challenge.has_sponsors, "published": self.challenge.published, "submission_time_limit": self.challenge.submission_time_limit, "is_registration_open": self.challenge.is_registration_open, @@ -2527,6 +2563,8 @@ def test_get_challenge_when_mode_is_host(self): "domain": self.challenge2.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge2.list_tags, + "has_prize": self.challenge2.has_prize, + "has_sponsors": self.challenge2.has_sponsors, "published": self.challenge2.published, "submission_time_limit": self.challenge2.submission_time_limit, "is_registration_open": self.challenge2.is_registration_open, @@ -2596,6 +2634,118 @@ def test_get_challenge_with_incorrect_url_pattern_with_all_values(self): self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE) +class ChallengePrizesTest(BaseAPITestClass): + + def setUp(self): + super().setUp() + self.challenge = Challenge.objects.create( + title="Test Challenge", + short_description="Short description for test challenge", + description="Description for test challenge", + terms_and_conditions="Terms and conditions for test challenge", + submission_guidelines="Submission guidelines for test challenge", + creator=self.challenge_host_team, + published=True, + is_registration_open=True, + enable_forum=True, + approved_by_admin=True, + anonymous_leaderboard=False, + start_date=timezone.now() - timedelta(days=2), + end_date=timezone.now() + timedelta(days=1), + ) + + def test_challenge_has_prize_false(self): + self.url = reverse_lazy( + "challenges:get_challenge_by_pk", + kwargs={"pk": self.challenge.pk}, + ) + + self.challenge.has_prize = False + self.challenge.save() + response = self.client.get(self.url, {}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(response.data["has_prize"]) + + def test_challenge_has_prize_true(self): + self.url = reverse_lazy( + "challenges:get_prizes_by_challenge", + kwargs={"challenge_pk": self.challenge.pk}, + ) + + self.challenge.has_prize = True + self.challenge.save() + prize = ChallengePrize.objects.create( + challenge=self.challenge, + amount="100USD", + rank=1, + ) + prize1 = ChallengePrize.objects.create( + challenge=self.challenge, + amount="500USD", + rank=2, + ) + response = self.client.get(self.url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0]["challenge"], prize.challenge.pk) + self.assertEqual(response.data[0]["amount"], prize.amount) + self.assertEqual(response.data[0]["rank"], prize.rank) + self.assertEqual(response.data[1]["challenge"], prize1.challenge.pk) + self.assertEqual(response.data[1]["amount"], prize1.amount) + self.assertEqual(response.data[1]["rank"], prize1.rank) + + +class ChallengeSponsorTest(BaseAPITestClass): + + def setUp(self): + super().setUp() + self.challenge = Challenge.objects.create( + title="Test Challenge", + short_description="Short description for test challenge", + description="Description for test challenge", + terms_and_conditions="Terms and conditions for test challenge", + submission_guidelines="Submission guidelines for test challenge", + creator=self.challenge_host_team, + published=True, + is_registration_open=True, + enable_forum=True, + approved_by_admin=True, + anonymous_leaderboard=False, + start_date=timezone.now() - timedelta(days=2), + end_date=timezone.now() + timedelta(days=1), + ) + + def test_challenge_has_sponsor_false(self): + self.url = reverse_lazy( + "challenges:get_challenge_by_pk", + kwargs={"pk": self.challenge.pk}, + ) + + self.challenge.has_sponsors = False + self.challenge.save() + response = self.client.get(self.url, {}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(response.data["has_sponsors"]) + + def test_challenge_has_sponsor_true(self): + self.url = reverse_lazy( + "challenges:get_sponsors_by_challenge", + kwargs={"challenge_pk": self.challenge.pk}, + ) + + self.challenge.has_sponsors = True + self.challenge.save() + sponsor = ChallengeSponsor.objects.create( + challenge=self.challenge, + name="Sponsor 1", + website="https://evalai.com", + ) + response = self.client.get(self.url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0]["challenge"], sponsor.challenge.pk) + self.assertEqual(response.data[0]["name"], sponsor.name) + self.assertEqual(response.data[0]["website"], sponsor.website) + + class BaseChallengePhaseClass(BaseAPITestClass): def setUp(self): super(BaseChallengePhaseClass, self).setUp() diff --git a/tests/unit/participants/test_views.py b/tests/unit/participants/test_views.py index d3f213d024..dcd352a96a 100644 --- a/tests/unit/participants/test_views.py +++ b/tests/unit/participants/test_views.py @@ -842,6 +842,8 @@ def test_get_teams_and_corresponding_challenges_for_a_participant(self): "domain": self.challenge1.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge1.list_tags, + "has_prize": self.challenge1.has_prize, + "has_sponsors": self.challenge1.has_sponsors, "published": self.challenge1.published, "submission_time_limit": self.challenge1.submission_time_limit, "is_registration_open": self.challenge1.is_registration_open, @@ -930,6 +932,8 @@ def test_get_participant_team_challenge_list(self): "domain": self.challenge1.domain, "domain_name": 'Computer Vision', "list_tags": self.challenge1.list_tags, + "has_prize": self.challenge1.has_prize, + "has_sponsors": self.challenge1.has_sponsors, "published": self.challenge1.published, "submission_time_limit": self.challenge1.submission_time_limit, "is_registration_open": self.challenge1.is_registration_open, From 4556fcfac04ef9b727943e0ed6cceb579a2a4f7f Mon Sep 17 00:00:00 2001 From: Suryansh Pathak <34577232+Suryansh5545@users.noreply.github.com> Date: Thu, 7 Sep 2023 23:32:47 +0530 Subject: [PATCH 3/3] [BugFix] Fix error message for prizes and sponsors (#4165) --- apps/challenges/challenge_config_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index 39dcb82f20..f8fb3c3189 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -977,10 +977,10 @@ def check_prizes(self): self.error_messages.append(message) rank_set.add(rank) if not isinstance(rank, int) or rank < 1: - message = self.error_messages_dict["invalid_rank"].format(rank) + message = self.error_messages_dict["prize_rank_wrong"].format(rank) self.error_messages.append(message) if not re.match(r'^\d+(\.\d{1,2})?[A-Z]{3}$', prize["amount"]): - message = self.error_messages_dict["invalid_amount"].format(prize["amount"]) + message = self.error_messages_dict["prize_amount_wrong"].format(prize["amount"]) self.error_messages.append(message)