From 8724a56cb7bbac94cdb78216c8657238550fb0d4 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 19 Jun 2023 01:11:50 +0500 Subject: [PATCH 1/7] do not show hidden column in leaderboard, unpacker v1 to handle hidden column --- src/apps/api/serializers/leaderboards.py | 4 ++-- src/apps/competitions/unpackers/v1.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/apps/api/serializers/leaderboards.py b/src/apps/api/serializers/leaderboards.py index 75834f7da..74aede7f5 100644 --- a/src/apps/api/serializers/leaderboards.py +++ b/src/apps/api/serializers/leaderboards.py @@ -125,7 +125,7 @@ class LeaderboardPhaseSerializer(serializers.ModelSerializer): tasks = PhaseTaskInstanceSerializer(source='task_instances', many=True) def get_columns(self, instance): - columns = Column.objects.filter(leaderboard=instance.leaderboard) + columns = Column.objects.filter(leaderboard=instance.leaderboard, hidden=False) if len(columns) == 0: raise serializers.ValidationError("No columns exist on the leaderboard") else: @@ -156,7 +156,7 @@ def get_submissions(self, instance): .select_related('owner').prefetch_related('scores') \ .annotate(primary_col=Sum('scores__score', filter=Q(scores__column=primary_col))) - for column in instance.leaderboard.columns.exclude(id=primary_col.id).order_by('index'): + for column in instance.leaderboard.columns.exclude(id=primary_col.id, hidden=False).order_by('index'): col_name = f'col{column.index}' ordering.append(f'{"-" if column.sorting == "desc" else ""}{col_name}') kwargs = { diff --git a/src/apps/competitions/unpackers/v1.py b/src/apps/competitions/unpackers/v1.py index 449569667..ce1e529c6 100644 --- a/src/apps/competitions/unpackers/v1.py +++ b/src/apps/competitions/unpackers/v1.py @@ -165,7 +165,8 @@ def _unpack_leaderboards(self): 'index': index, 'sorting': column.get('sort') or 'desc', # get precision as numeric_format, if not found, use default value = 2 - 'precision': column.get('numeric_format', 2) + 'precision': column.get('numeric_format', 2), + 'hidden': column.get('hidden', False) } for leaderboard_data in self.competition['leaderboards']: From 8ecfaf506ca7c91dff01bd9898997a0f0887824d Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 19 Jun 2023 01:18:45 +0500 Subject: [PATCH 2/7] LEADERBOARD_V1 data updated with hidden attribute --- src/apps/competitions/tests/unpacker_test_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/apps/competitions/tests/unpacker_test_data.py b/src/apps/competitions/tests/unpacker_test_data.py index eb38945e9..df7008974 100644 --- a/src/apps/competitions/tests/unpacker_test_data.py +++ b/src/apps/competitions/tests/unpacker_test_data.py @@ -154,6 +154,7 @@ "index": 0, "sorting": "desc", "precision": 4, + "hidden": False, }, { "title": "Duration", @@ -161,6 +162,7 @@ "index": 1, "sorting": "desc", "precision": 2, + "hidden": False, } ] }] From 452ce7c5deee3174eb9bb4fb4eae80addcba97c7 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 19 Jun 2023 13:48:30 +0500 Subject: [PATCH 3/7] filter: hidden condition removed --- src/apps/competitions/unpackers/v1.py | 1 + src/static/riot/competitions/detail/leaderboards.tag | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apps/competitions/unpackers/v1.py b/src/apps/competitions/unpackers/v1.py index ce1e529c6..bc1947abb 100644 --- a/src/apps/competitions/unpackers/v1.py +++ b/src/apps/competitions/unpackers/v1.py @@ -166,6 +166,7 @@ def _unpack_leaderboards(self): 'sorting': column.get('sort') or 'desc', # get precision as numeric_format, if not found, use default value = 2 'precision': column.get('numeric_format', 2), + # get hidden, use False if not found 'hidden': column.get('hidden', False) } diff --git a/src/static/riot/competitions/detail/leaderboards.tag b/src/static/riot/competitions/detail/leaderboards.tag index d289060ef..e21fda21e 100644 --- a/src/static/riot/competitions/detail/leaderboards.tag +++ b/src/static/riot/competitions/detail/leaderboards.tag @@ -106,7 +106,7 @@ for (const column of self.columns){ let key = column.key.toLowerCase() let title = column.title.toLowerCase() - if((key.includes(search_key) || title.includes(search_key)) && !column.hidden) { + if((key.includes(search_key) || title.includes(search_key))) { self.filtered_columns.push(column) } else { From f4a6da8956e3197bc88c42431a2896e3b66edf53 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sat, 24 Jun 2023 19:33:44 +0500 Subject: [PATCH 4/7] analytics download fixed --- src/apps/api/views/analytics.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/apps/api/views/analytics.py b/src/apps/api/views/analytics.py index f2cafabf7..084b042e7 100644 --- a/src/apps/api/views/analytics.py +++ b/src/apps/api/views/analytics.py @@ -113,22 +113,22 @@ def get(self, request): time_unit = request.query_params.get('time_unit') csv = request.query_params.get('format') == 'csv' - start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d').replace(tzinfo=pytz.UTC) - end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d').replace(hour=11, minute=59, second=59, tzinfo=pytz.UTC) + _start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d').replace(tzinfo=pytz.UTC) + _end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d').replace(hour=11, minute=59, second=59, tzinfo=pytz.UTC) - users = build_request_object(User, 'date_joined', time_unit, start_date, end_date, csv, 'users_data_date', 'users_data_count') - competitions = build_request_object(Competition, 'created_when', time_unit, start_date, end_date, csv, 'competitions_data_date', 'competitions_data_count') - submissions = build_request_object(Submission, 'created_when', time_unit, start_date, end_date, csv, 'submissions_data_date', 'submissions_data_count') + users = build_request_object(User, 'date_joined', time_unit, _start_date, _end_date, csv, 'users_data_date', 'users_data_count') + competitions = build_request_object(Competition, 'created_when', time_unit, _start_date, _end_date, csv, 'competitions_data_date', 'competitions_data_count') + submissions = build_request_object(Submission, 'created_when', time_unit, _start_date, _end_date, csv, 'submissions_data_date', 'submissions_data_count') if csv: ob = [{ 'start_date': start_date, 'end_date': end_date, 'time_unit': time_unit, - 'registered_user_count': User.objects.filter(date_joined__range=[start_date, end_date]).count(), - 'competition_count': Competition.objects.filter(created_when__range=[start_date, end_date]).count(), - 'competitions_published_count': Competition.objects.filter(published=True, created_when__range=[start_date, end_date]).count(), - 'submissions_made_count': Submission.objects.filter(created_when__range=[start_date, end_date]).count(), + 'registered_user_count': User.objects.filter(date_joined__range=[_start_date, _end_date]).count(), + 'competition_count': Competition.objects.filter(created_when__range=[_start_date, _end_date]).count(), + 'competitions_published_count': Competition.objects.filter(published=True, created_when__range=[_start_date, _end_date]).count(), + 'submissions_made_count': Submission.objects.filter(created_when__range=[_start_date, _end_date]).count(), }] max_len = max(len(users), len(competitions), len(submissions)) From 5e8a2865afb1cc8fd77d87db7e7e14ef76a16401 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Fri, 21 Jul 2023 19:53:21 +0500 Subject: [PATCH 5/7] default profile avatar changed --- src/static/img/user-avatar.png | Bin 0 -> 21570 bytes src/static/riot/profiles/profile_detail.tag | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/static/img/user-avatar.png diff --git a/src/static/img/user-avatar.png b/src/static/img/user-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..5401a48f7349ea9c816d5ed97f05f5d99ff43bc1 GIT binary patch literal 21570 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 - +
-
+
From 5d96408a76b99e0f5338696f6e44f5537ae74cd9 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Fri, 21 Jul 2023 21:31:08 +0500 Subject: [PATCH 6/7] Detailed results displayed for each task on leaderboard (#967) * Detailed results displayed for each task on leaderboard * more explanation added to detailed result fetching. url commented * detailed results removed from factsheets --- src/apps/api/serializers/submissions.py | 3 +- src/apps/api/views/competitions.py | 18 +++++++++++ .../riot/competitions/detail/leaderboards.tag | 31 ++++++++++++++++--- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/apps/api/serializers/submissions.py b/src/apps/api/serializers/submissions.py index a8d8ed071..db4b91e91 100644 --- a/src/apps/api/serializers/submissions.py +++ b/src/apps/api/serializers/submissions.py @@ -98,7 +98,8 @@ class Meta: 'scores', 'display_name', 'slug_url', - 'organization' + 'organization', + 'detailed_result' ) extra_kwargs = { "scores": {"read_only": True}, diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index b293e7e3a..f7c3f6763 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -541,6 +541,7 @@ def get_leaderboard(self, request, pk): } columns = [col for col in query['columns']] submissions_keys = {} + submission_detailed_results = {} for submission in query['submissions']: # count number of entries/number of submissions for the owner of this submission for this phase # count all submissions with no parent and count all parents without counting the children @@ -558,12 +559,24 @@ def get_leaderboard(self, request, pk): .strftime('%Y-%m-%d') submission_key = f"{submission['owner']}{submission['parent'] or submission['id']}" + + # gather detailed result from submissions for each task + # detailed_results are gathered based on submission key + # `id` is used to fetch the right detailed result in detailed results page + # `detailed_result` url is not needed + submission_detailed_results.setdefault(submission_key, []).append({ + # 'detailed_result': submission['detailed_result'], + 'task': submission['task'], + 'id': submission['id'] + }) + if submission_key not in submissions_keys: submissions_keys[submission_key] = len(response['submissions']) response['submissions'].append({ 'id': submission['id'], 'owner': submission['display_name'] or submission['owner'], 'scores': [], + 'detailed_results': [], 'fact_sheet_answers': submission['fact_sheet_answers'], 'slug_url': submission['slug_url'], 'organization': submission['organization'], @@ -588,6 +601,11 @@ def get_leaderboard(self, request, pk): tempScore['score'] = str(round(float(tempScore["score"]), precision)) response['submissions'][submissions_keys[submission_key]]['scores'].append(tempScore) + # put detailed results in its submission + for k, v in submissions_keys.items(): + response['submissions'][v]['detailed_results'] = submission_detailed_results[k] + print(f"\n{response['submissions']}\n") + for task in query['tasks']: # This can be used to rendered variable columns on each task tempTask = { diff --git a/src/static/riot/competitions/detail/leaderboards.tag b/src/static/riot/competitions/detail/leaderboards.tag index e21fda21e..96e4db63e 100644 --- a/src/static/riot/competitions/detail/leaderboards.tag +++ b/src/static/riot/competitions/detail/leaderboards.tag @@ -30,7 +30,6 @@ Task: { task.name } - # @@ -38,7 +37,7 @@ Entries Date of last entry {column.title} - Detailed Results + @@ -60,8 +59,12 @@ {submission.num_entries} {submission.last_entry_date} { submission.organization.name } - { get_score(column, submission) } - Show detailed results + + Show detailed results + {get_score(column, submission)} + + + @@ -101,6 +104,7 @@ self.filter_columns = () => { let search_key = self.refs.leaderboardFilter.value.toLowerCase() self.filtered_tasks = JSON.parse(JSON.stringify(self.selected_leaderboard.tasks)) + console.log(self.filtered_tasks) if(search_key){ self.filtered_columns = [] for (const column of self.columns){ @@ -143,10 +147,20 @@ self.selected_leaderboard.tasks.unshift(fake_metadata_task) } for(task of self.selected_leaderboard.tasks){ + for(column of task.columns){ column.task_id = task.id self.columns.push(column) } + // -1 id is used for fact sheet answers + if(self.enable_detailed_results & task.id != -1){ + self.columns.push({ + task_id: task.id, + title: "Detailed Results" + }) + task.colWidth += 1 + } + console.log(task) } self.filter_columns() $('#leaderboardTable').tablesort() @@ -154,6 +168,15 @@ }) } + self.get_detailed_result_submisison_id = function(column, submisison){ + for (index in submisison.detailed_results) { + if(column.task_id == submisison.detailed_results[index].task){ + return submisison.detailed_results[index].id + } + } + } + + CODALAB.events.on('phase_selected', data => { self.phase_id = data.id self.update_leaderboard() From 958071fa6a6191cae1d302960462beb9b544d429 Mon Sep 17 00:00:00 2001 From: Benjamin Bearce Date: Tue, 25 Jul 2023 05:41:53 -0400 Subject: [PATCH 7/7] Download buttons (#970) * download_buttons progress 06_03_2023 * List the Files on Files Tab * download buttons update * dataset download added to UI. * 06_24_2023 progress * unpackers, polishing and fixing test errors * last bit of test fixes. * flake issues * pytest errors * single select and final touches * flake and fixing tests * remove dev docker-compose file * hide all but public_startingkit data for no admin * Don't show non-registered\unlogged-in participants * Improve display --------- Co-authored-by: didayolo --- bin/pg_dump.py | 2 - compute_worker/compute_worker.py | 9 +- fabfile.py | 2 + src/apps/api/serializers/competitions.py | 13 +- src/apps/api/serializers/datasets.py | 5 + src/apps/api/serializers/tasks.py | 31 ++ src/apps/api/views/competitions.py | 15 + src/apps/api/views/datasets.py | 1 - .../migrations/0033_auto_20230617_1753.py | 25 ++ src/apps/competitions/models.py | 3 + src/apps/competitions/tasks.py | 1 - .../competitions/tests/unpacker_test_data.py | 6 +- .../competitions/unpackers/base_unpacker.py | 9 + src/apps/competitions/unpackers/v1.py | 21 ++ src/apps/competitions/unpackers/v2.py | 21 ++ src/apps/profiles/views.py | 1 - src/static/js/ours/utils.js | 277 ++++++++++-------- src/static/riot/competitions/detail/_tabs.tag | 109 ++++++- .../riot/competitions/editor/_phases.tag | 186 +++++++++++- src/static/riot/competitions/editor/form.tag | 1 - src/static/riot/tasks/management.tag | 1 - 21 files changed, 591 insertions(+), 148 deletions(-) create mode 100644 src/apps/competitions/migrations/0033_auto_20230617_1753.py diff --git a/bin/pg_dump.py b/bin/pg_dump.py index f8b29c254..88f93d548 100755 --- a/bin/pg_dump.py +++ b/bin/pg_dump.py @@ -27,5 +27,3 @@ call([ 'docker', 'exec', 'codabench-django-1', 'python', 'manage.py', 'upload_backup', f'{dump_name}' ]) - - diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 5057e3778..1171bb590 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -41,7 +41,7 @@ # Setup base directories used by all submissions # note: we need to pass this directory to docker-compose so it knows where to store things! HOST_DIRECTORY = os.environ.get("HOST_DIRECTORY", "/tmp/codabench/") -BASE_DIR = "/codabench/" # base directory inside the container +BASE_DIR = "/codabench/" # base directory inside the container CACHE_DIR = os.path.join(BASE_DIR, "cache") MAX_CACHE_DIR_SIZE_GB = float(os.environ.get('MAX_CACHE_DIR_SIZE_GB', 10)) @@ -74,6 +74,7 @@ else: CONTAINER_ENGINE_EXECUTABLE = "docker" + class SubmissionException(Exception): pass @@ -182,7 +183,7 @@ def __init__(self, run_args): self.bundle_dir = os.path.join(self.root_dir, "bundles") self.input_dir = os.path.join(self.root_dir, "input") self.output_dir = os.path.join(self.root_dir, "output") - self.data_dir = os.path.join(HOST_DIRECTORY, "data") # absolute path to data in the host + self.data_dir = os.path.join(HOST_DIRECTORY, "data") # absolute path to data in the host self.logs = {} # Details for submission @@ -497,10 +498,10 @@ async def _run_program_directory(self, program_dir, kind, can_be_output=False): logger.info(f"Metadata path is {os.path.join(program_dir, metadata_path)}") with open(os.path.join(program_dir, metadata_path), 'r') as metadata_file: - try: # try to find a command in the metadata, in other cases set metadata to None + try: # try to find a command in the metadata, in other cases set metadata to None metadata = yaml.load(metadata_file.read(), Loader=yaml.FullLoader) logger.info(f"Metadata contains:\n {metadata}") - if isinstance(metadata, dict): # command found + if isinstance(metadata, dict): # command found command = metadata.get("command") else: command = None diff --git a/fabfile.py b/fabfile.py index 580a715ab..de5e6a891 100644 --- a/fabfile.py +++ b/fabfile.py @@ -17,6 +17,7 @@ # $ fab -R role_name env.roledefs = yaml.load(open('server_config.yaml').read()) + # ---------------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------------- @@ -24,6 +25,7 @@ def _reconnect_current_host(): network.disconnect_all() connections.connect(env.host + ':%s' % env.port) + # ---------------------------------------------------------------------------- # Tasks # ---------------------------------------------------------------------------- diff --git a/src/apps/api/serializers/competitions.py b/src/apps/api/serializers/competitions.py index 69d352c67..d29ca7ba7 100644 --- a/src/apps/api/serializers/competitions.py +++ b/src/apps/api/serializers/competitions.py @@ -4,6 +4,7 @@ from api.fields import NamedBase64ImageField from api.mixins import DefaultUserCreateMixin +from api.serializers.datasets import DataDetailSerializer from api.serializers.leaderboards import LeaderboardSerializer, ColumnSerializer from api.serializers.profiles import CollaboratorSerializer from api.serializers.submissions import SubmissionScoreSerializer @@ -41,6 +42,8 @@ class Meta: 'auto_migrate_to_this_phase', 'hide_output', 'leaderboard', + 'public_data', + 'starting_kit', 'is_final_phase', ) @@ -90,6 +93,9 @@ class PhaseDetailSerializer(serializers.ModelSerializer): tasks = PhaseTaskInstanceSerializer(source='task_instances', many=True) status = serializers.SerializerMethodField() + public_data = DataDetailSerializer(read_only=True) + starting_kit = DataDetailSerializer(read_only=True) + class Meta: model = Phase fields = ( @@ -100,13 +106,16 @@ class Meta: 'name', 'description', 'status', + 'execution_time_limit', 'tasks', - 'auto_migrate_to_this_phase', 'has_max_submissions', 'max_submissions_per_day', 'max_submissions_per_person', - 'execution_time_limit', + 'auto_migrate_to_this_phase', 'hide_output', + # no leaderboard + 'public_data', + 'starting_kit', 'is_final_phase', ) diff --git a/src/apps/api/serializers/datasets.py b/src/apps/api/serializers/datasets.py index 0b44b7ec1..25e069afc 100644 --- a/src/apps/api/serializers/datasets.py +++ b/src/apps/api/serializers/datasets.py @@ -26,6 +26,7 @@ class Meta: 'was_created_by_competition', 'competition', 'file_name', + ) read_only_fields = ( 'key', @@ -61,6 +62,7 @@ def create(self, validated_data): class DataSimpleSerializer(serializers.ModelSerializer): + class Meta: model = Data fields = ( @@ -74,6 +76,7 @@ class Meta: class DataDetailSerializer(serializers.ModelSerializer): created_by = serializers.CharField(source='created_by.username') competition = serializers.SerializerMethodField() + value = serializers.CharField(source='key', required=False) class Meta: model = Data @@ -86,6 +89,8 @@ class Meta: 'description', 'is_public', 'key', + # Value is used for Semantic Multiselect dropdown api calls + 'value', 'was_created_by_competition', 'in_use', 'file_size', diff --git a/src/apps/api/serializers/tasks.py b/src/apps/api/serializers/tasks.py index deac682cc..b98ce36ea 100644 --- a/src/apps/api/serializers/tasks.py +++ b/src/apps/api/serializers/tasks.py @@ -13,6 +13,7 @@ class SolutionSerializer(WritableNestedModelSerializer): tasks = serializers.SlugRelatedField(queryset=Task.objects.all(), required=False, allow_null=True, slug_field='key', many=True) data = serializers.SlugRelatedField(queryset=Data.objects.all(), required=False, allow_null=True, slug_field='key') + size = serializers.SerializerMethodField() class Meta: model = Solution @@ -23,8 +24,16 @@ class Meta: 'tasks', 'data', 'md5', + 'size', ] + def get_size(self, instance): + try: + return instance.data.file_size + except AttributeError: + print("This solution has no data associated with it...might be a test") + return None + class SolutionListSerializer(serializers.ModelSerializer): data = DataDetailSerializer() @@ -38,6 +47,7 @@ class Meta: class TaskSerializer(DefaultUserCreateMixin, WritableNestedModelSerializer): + input_data = serializers.SlugRelatedField(queryset=Data.objects.all(), required=False, allow_null=True, slug_field='key') ingestion_program = serializers.SlugRelatedField(queryset=Data.objects.all(), required=False, allow_null=True, slug_field='key') reference_data = serializers.SlugRelatedField(queryset=Data.objects.all(), required=False, allow_null=True, slug_field='key') @@ -159,6 +169,8 @@ class PhaseTaskInstanceSerializer(serializers.HyperlinkedModelSerializer): key = serializers.CharField(source='task.key', required=False) created_when = serializers.DateTimeField(source='task.created_when', required=False) name = serializers.CharField(source='task.name', required=False) + solutions = serializers.SerializerMethodField() + public_datasets = serializers.SerializerMethodField() class Meta: model = PhaseTaskInstance @@ -172,4 +184,23 @@ class Meta: 'key', 'created_when', 'name', + 'solutions', + 'public_datasets' ) + + def get_solutions(self, instance): + qs = instance.task.solutions.all() + return SolutionSerializer(qs, many=True).data + + def get_public_datasets(self, instance): + input_data = instance.task.input_data + reference_data = instance.task.reference_data + ingestion_program = instance.task.ingestion_program + scoring_program = instance.task.scoring_program + try: + dataset_list_ids = [input_data.id, reference_data.id, ingestion_program.id, scoring_program.id] + qs = Data.objects.filter(id__in=dataset_list_ids) + return DataDetailSerializer(qs, many=True).data + except AttributeError: + print("This phase task has no datasets") + return None diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index f7c3f6763..4dc0e4b30 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -30,6 +30,7 @@ from competitions.emails import send_participation_requested_emails, send_participation_accepted_emails, \ send_participation_denied_emails, send_direct_participant_email from competitions.models import Competition, Phase, CompetitionCreationTaskStatus, CompetitionParticipant, Submission +from datasets.models import Data from competitions.tasks import batch_send_email, manual_migration, create_competition_dump from competitions.utils import get_popular_competitions, get_featured_competitions from leaderboards.models import Leaderboard @@ -228,7 +229,21 @@ def update(self, request, *args, **kwargs): phase['leaderboard'] = leaderboard_id + # Get public_data and starting_kit + for phase in data['phases']: + # We just need to know what public_data and starting_kit go with this phase + # We don't need to serialize the whole object + try: + phase['public_data'] = Data.objects.filter(key=phase['public_data']['value'])[0].id + except TypeError: + phase['public_data'] = None + try: + phase['starting_kit'] = Data.objects.filter(key=phase['starting_kit']['value'])[0].id + except TypeError: + phase['starting_kit'] = None + serializer = self.get_serializer(instance, data=data, partial=partial) + type(serializer) serializer.is_valid(raise_exception=True) self.perform_update(serializer) diff --git a/src/apps/api/views/datasets.py b/src/apps/api/views/datasets.py index d1a817c24..fd2ae17cb 100644 --- a/src/apps/api/views/datasets.py +++ b/src/apps/api/views/datasets.py @@ -79,7 +79,6 @@ def get_serializer_class(self): return serializers.DataSerializer def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) new_dataset = serializer.save() # request_sassy_file_name is temporarily set via this serializer diff --git a/src/apps/competitions/migrations/0033_auto_20230617_1753.py b/src/apps/competitions/migrations/0033_auto_20230617_1753.py new file mode 100644 index 000000000..3603af13e --- /dev/null +++ b/src/apps/competitions/migrations/0033_auto_20230617_1753.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.17 on 2023-06-17 17:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('datasets', '0007_auto_20230609_1738'), + ('competitions', '0032_submission_worker_hostname'), + ] + + operations = [ + migrations.AddField( + model_name='phase', + name='public_data', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='phase_public_data', to='datasets.Data'), + ), + migrations.AddField( + model_name='phase', + name='starting_kit', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='phase_starting_kit', to='datasets.Data'), + ), + ] diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index 9f96346dd..b30988106 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -283,6 +283,9 @@ class Phase(ChaHubSaveMixin, models.Model): leaderboard = models.ForeignKey('leaderboards.Leaderboard', on_delete=models.DO_NOTHING, null=True, blank=True, related_name="phases") + public_data = models.ForeignKey('datasets.Data', on_delete=models.SET_NULL, null=True, blank=True, related_name="phase_public_data") + starting_kit = models.ForeignKey('datasets.Data', on_delete=models.SET_NULL, null=True, blank=True, related_name="phase_starting_kit") + class Meta: ordering = ('index',) diff --git a/src/apps/competitions/tasks.py b/src/apps/competitions/tasks.py index 96a1a1d16..0a6c38ac8 100644 --- a/src/apps/competitions/tasks.py +++ b/src/apps/competitions/tasks.py @@ -381,7 +381,6 @@ def mark_status_as_failed_and_delete_dataset(competition_creation_status, detail ) unpacker.unpack() - try: competition = unpacker.save() except ValidationError as e: diff --git a/src/apps/competitions/tests/unpacker_test_data.py b/src/apps/competitions/tests/unpacker_test_data.py index df7008974..45cd096d5 100644 --- a/src/apps/competitions/tests/unpacker_test_data.py +++ b/src/apps/competitions/tests/unpacker_test_data.py @@ -205,6 +205,8 @@ 'auto_migrate_to_this_phase': False, 'has_max_submissions': True, 'end': datetime.datetime(2019, 9, 30, 0, 0, tzinfo=timezone.now().tzinfo), + 'public_data': None, + 'starting_kit': None, 'tasks': [0], 'status': 'Previous', }, @@ -218,9 +220,11 @@ 'max_submissions_per_person': None, 'auto_migrate_to_this_phase': True, 'has_max_submissions': True, + 'end': None, + 'public_data': None, + 'starting_kit': None, 'tasks': [1], 'status': 'Current', - 'end': None, 'is_final_phase': True, } ] diff --git a/src/apps/competitions/unpackers/base_unpacker.py b/src/apps/competitions/unpackers/base_unpacker.py index 281069ca1..a769dc2eb 100644 --- a/src/apps/competitions/unpackers/base_unpacker.py +++ b/src/apps/competitions/unpackers/base_unpacker.py @@ -231,6 +231,7 @@ def _unpack_phases(self): "description": phase_description, "start": phase_start (datetime.datetime), "end": phase_end (datetime.datetime), + # BB public_data and starting_kit # ... See serializer for complete fields list "tasks": [list of indices that should match self.competition['tasks']] } @@ -315,6 +316,14 @@ def _save_competition(self): for phase in self.competition['phases']: phase['tasks'] = [self.competition['tasks'][index].key for index in phase['tasks']] phase['leaderboard'] = self.competition['leaderboards'][0].id + phase_public_data_file_data = phase['public_data'] + phase_starting_kit_file_data = phase['starting_kit'] + if phase_public_data_file_data is not None: + public_data_key, public_data_temp_data_path = self._get_data_key(**phase_public_data_file_data) + phase['public_data'] = Data.objects.filter(key=public_data_key)[0].id + if phase_starting_kit_file_data is not None: + starting_kit_key, starting_kit_temp_data_path = self._get_data_key(**phase_starting_kit_file_data) + phase['starting_kit'] = Data.objects.filter(key=starting_kit_key)[0].id self.competition.pop('leaderboards') diff --git a/src/apps/competitions/unpackers/v1.py b/src/apps/competitions/unpackers/v1.py index bc1947abb..27f9c9956 100644 --- a/src/apps/competitions/unpackers/v1.py +++ b/src/apps/competitions/unpackers/v1.py @@ -98,6 +98,27 @@ def _unpack_phases(self): else: new_phase['end'] = None + # Public Data and Starting Kit + try: + new_phase['public_data'] = { + 'file_name': phase['public_data'], + 'file_path': os.path.join(self.temp_directory, phase['public_data']), + 'file_type': 'public_data', + 'creator': self.creator.id, + } + except KeyError: + new_phase['public_data'] = None + + try: + new_phase['starting_kit'] = { + 'file_name': phase['starting_kit'], + 'file_path': os.path.join(self.temp_directory, phase['starting_kit']), + 'file_type': 'starting_kit', + 'creator': self.creator.id, + } + except KeyError: + new_phase['starting_kit'] = None + task_index = len(self.competition['tasks']) new_phase['tasks'] = [task_index] self.competition['phases'].append(new_phase) diff --git a/src/apps/competitions/unpackers/v2.py b/src/apps/competitions/unpackers/v2.py index f6bd941fb..4f5488f22 100644 --- a/src/apps/competitions/unpackers/v2.py +++ b/src/apps/competitions/unpackers/v2.py @@ -203,6 +203,27 @@ def _unpack_phases(self): if new_phase['max_submissions_per_day'] or new_phase['max_submissions_per_person']: new_phase['has_max_submissions'] = True + # Public Data and Starting Kit + try: + new_phase['public_data'] = { + 'file_name': phase_data['public_data'], + 'file_path': os.path.join(self.temp_directory, phase_data['public_data']), + 'file_type': 'public_data', + 'creator': self.creator.id, + } + except KeyError: + new_phase['public_data'] = None + + try: + new_phase['starting_kit'] = { + 'file_name': phase_data['starting_kit'], + 'file_path': os.path.join(self.temp_directory, phase_data['starting_kit']), + 'file_type': 'starting_kit', + 'creator': self.creator.id, + } + except KeyError: + new_phase['starting_kit'] = None + self.competition['phases'].append(new_phase) self._validate_phase_ordering() self._set_phase_statuses() diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index 9a19090dd..cfa4f2e10 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -68,7 +68,6 @@ def get_context_data(self, **kwargs): def activate(request, uidb64, token): try: - # import pdb; pdb.set_trace(); uid = force_str(urlsafe_base64_decode(uidb64)) user = User.objects.get(pk=uid) except User.DoesNotExist: diff --git a/src/static/js/ours/utils.js b/src/static/js/ours/utils.js index 026be61d5..7407c5aa2 100644 --- a/src/static/js/ours/utils.js +++ b/src/static/js/ours/utils.js @@ -96,7 +96,30 @@ function get_form_fields(base_element) { //return $(':input', self.root).not('button').not('[readonly]').each(function (i, field) { // console.log(field) //}) - return $(':input', base_element).not('button').not('[readonly]') + form_fields = $(':input', base_element).not('button').not('[readonly]') + // Calendars come through as read-only and jQuery leaves them out + calendars = $('.two.fields .ui.calendar.field input[type="text"]') + readonly_calendars = $('.two.fields .ui.calendar.field [readonly]') + // If calendars is readonly_calendars, then append them to form_fields + if (calendars.length === readonly_calendars.length) { + var isIdentical = true; + calendars.each(function(index) { + if (!$(this).is(readonly_calendars.eq(index))) { + isIdentical = false; + return false; // exit the loop + } + }); + + if (isIdentical) { + form_fields = form_fields.add(calendars) + } else { + // console.log("The two sets are not identical."); + } + } else { + // console.log("The two sets have different lengths and are not identical."); + } + + return form_fields } function get_form_data(base_element) { @@ -170,134 +193,134 @@ function getBase64(file) { } /* - A simple, lightweight jQuery plugin for creating sortable tables. - https://github.com/kylefox/jquery-tablesort - Version 0.0.11 + A simple, lightweight jQuery plugin for creating sortable tables. + https://github.com/kylefox/jquery-tablesort + Version 0.0.11 */ (function($) { - $.tablesort = function ($table, settings) { - var self = this; - this.$table = $table; - this.$thead = this.$table.find('thead'); - this.settings = $.extend({}, $.tablesort.defaults, settings); - this.$sortCells = this.$thead.length > 0 ? this.$thead.find('th:not(.no-sort)') : this.$table.find('th:not(.no-sort)'); - this.$sortCells.on('click.tablesort', function() { - self.sort($(this)); - }); - this.index = null; - this.$th = null; - this.direction = null; - }; - - $.tablesort.prototype = { - - sort: function(th, direction) { - var start = new Date(), - self = this, - table = this.$table, - rowsContainer = table.find('tbody').length > 0 ? table.find('tbody') : table, - rows = rowsContainer.find('tr').has('td, th'), - cells = rows.find(':nth-child(' + (th.index() + 1) + ')').filter('td, th'), - sortBy = th.data().sortBy, - sortedMap = []; - - var unsortedValues = cells.map(function(idx, cell) { - if (sortBy) - return (typeof sortBy === 'function') ? sortBy($(th), $(cell), self) : sortBy; - return ($(this).data().sortValue != null ? $(this).data().sortValue : $(this).text()); - }); - if (unsortedValues.length === 0) return; - - //click on a different column - if (this.index !== th.index()) { - this.direction = 'asc'; - this.index = th.index(); - } - else if (direction !== 'asc' && direction !== 'desc') - this.direction = this.direction === 'asc' ? 'desc' : 'asc'; - else - this.direction = direction; - - direction = this.direction == 'asc' ? 1 : -1; - - self.$table.trigger('tablesort:start', [self]); - self.log("Sorting by " + this.index + ' ' + this.direction); - - // Try to force a browser redraw - self.$table.css("display"); - // Run sorting asynchronously on a timeout to force browser redraw after - // `tablesort:start` callback. Also avoids locking up the browser too much. - setTimeout(function() { - self.$sortCells.removeClass(self.settings.asc + ' ' + self.settings.desc); - for (var i = 0, length = unsortedValues.length; i < length; i++) - { - sortedMap.push({ - index: i, - cell: cells[i], - row: rows[i], - value: unsortedValues[i] - }); - } - - sortedMap.sort(function(a, b) { - return self.settings.compare(a.value, b.value) * direction; - }); - - $.each(sortedMap, function(i, entry) { - rowsContainer.append(entry.row); - }); - - th.addClass(self.settings[self.direction]); - - self.log('Sort finished in ' + ((new Date()).getTime() - start.getTime()) + 'ms'); - self.$table.trigger('tablesort:complete', [self]); - //Try to force a browser redraw - self.$table.css("display"); - }, unsortedValues.length > 2000 ? 200 : 10); - }, - - log: function(msg) { - if(($.tablesort.DEBUG || this.settings.debug) && console && console.log) { - console.log('[tablesort] ' + msg); - } - }, - - destroy: function() { - this.$sortCells.off('click.tablesort'); - this.$table.data('tablesort', null); - return null; - } - - }; - - $.tablesort.DEBUG = false; - - $.tablesort.defaults = { - debug: $.tablesort.DEBUG, - asc: 'sorted ascending', - desc: 'sorted descending', - compare: function(a, b) { - if (a > b) { - return 1; - } else if (a < b) { - return -1; - } else { - return 0; - } - } - }; - - $.fn.tablesort = function(settings) { - var table, sortable, previous; - return this.each(function() { - table = $(this); - previous = table.data('tablesort'); - if(previous) { - previous.destroy(); - } - table.data('tablesort', new $.tablesort(table, settings)); - }); - }; + $.tablesort = function ($table, settings) { + var self = this; + this.$table = $table; + this.$thead = this.$table.find('thead'); + this.settings = $.extend({}, $.tablesort.defaults, settings); + this.$sortCells = this.$thead.length > 0 ? this.$thead.find('th:not(.no-sort)') : this.$table.find('th:not(.no-sort)'); + this.$sortCells.on('click.tablesort', function() { + self.sort($(this)); + }); + this.index = null; + this.$th = null; + this.direction = null; + }; + + $.tablesort.prototype = { + + sort: function(th, direction) { + var start = new Date(), + self = this, + table = this.$table, + rowsContainer = table.find('tbody').length > 0 ? table.find('tbody') : table, + rows = rowsContainer.find('tr').has('td, th'), + cells = rows.find(':nth-child(' + (th.index() + 1) + ')').filter('td, th'), + sortBy = th.data().sortBy, + sortedMap = []; + + var unsortedValues = cells.map(function(idx, cell) { + if (sortBy) + return (typeof sortBy === 'function') ? sortBy($(th), $(cell), self) : sortBy; + return ($(this).data().sortValue != null ? $(this).data().sortValue : $(this).text()); + }); + if (unsortedValues.length === 0) return; + + //click on a different column + if (this.index !== th.index()) { + this.direction = 'asc'; + this.index = th.index(); + } + else if (direction !== 'asc' && direction !== 'desc') + this.direction = this.direction === 'asc' ? 'desc' : 'asc'; + else + this.direction = direction; + + direction = this.direction == 'asc' ? 1 : -1; + + self.$table.trigger('tablesort:start', [self]); + self.log("Sorting by " + this.index + ' ' + this.direction); + + // Try to force a browser redraw + self.$table.css("display"); + // Run sorting asynchronously on a timeout to force browser redraw after + // `tablesort:start` callback. Also avoids locking up the browser too much. + setTimeout(function() { + self.$sortCells.removeClass(self.settings.asc + ' ' + self.settings.desc); + for (var i = 0, length = unsortedValues.length; i < length; i++) + { + sortedMap.push({ + index: i, + cell: cells[i], + row: rows[i], + value: unsortedValues[i] + }); + } + + sortedMap.sort(function(a, b) { + return self.settings.compare(a.value, b.value) * direction; + }); + + $.each(sortedMap, function(i, entry) { + rowsContainer.append(entry.row); + }); + + th.addClass(self.settings[self.direction]); + + self.log('Sort finished in ' + ((new Date()).getTime() - start.getTime()) + 'ms'); + self.$table.trigger('tablesort:complete', [self]); + //Try to force a browser redraw + self.$table.css("display"); + }, unsortedValues.length > 2000 ? 200 : 10); + }, + + log: function(msg) { + if(($.tablesort.DEBUG || this.settings.debug) && console && console.log) { + console.log('[tablesort] ' + msg); + } + }, + + destroy: function() { + this.$sortCells.off('click.tablesort'); + this.$table.data('tablesort', null); + return null; + } + + }; + + $.tablesort.DEBUG = false; + + $.tablesort.defaults = { + debug: $.tablesort.DEBUG, + asc: 'sorted ascending', + desc: 'sorted descending', + compare: function(a, b) { + if (a > b) { + return 1; + } else if (a < b) { + return -1; + } else { + return 0; + } + } + }; + + $.fn.tablesort = function(settings) { + var table, sortable, previous; + return this.each(function() { + table = $(this); + previous = table.data('tablesort'); + if(previous) { + previous.destroy(); + } + table.data('tablesort', new $.tablesort(table, settings)); + }); + }; })(window.Zepto || window.jQuery); diff --git a/src/static/riot/competitions/detail/_tabs.tag b/src/static/riot/competitions/detail/_tabs.tag index 380ba377f..57ceacb71 100644 --- a/src/static/riot/competitions/detail/_tabs.tag +++ b/src/static/riot/competitions/detail/_tabs.tag @@ -29,9 +29,9 @@ data-tab="_tab_page{page.index}"> { page.title }
- +
@@ -40,7 +40,7 @@
- + yes {filesize(file.file_size * 1024)} + + + + No Files Available Yet + + - --> + @@ -193,17 +204,103 @@ self.competition = competition self.competition.files = [] _.forEach(competition.phases, phase => { + _.forEach(phase.tasks, task => { + // Over complicated data org but it is so we can order exactly how we want... + let input_data = {} + let reference_data = {} + let ingestion_program = {} + let scoring_program = {} + _.forEach(task.public_datasets, dataset => { + let type = 'input_data' + if(dataset.type === "input_data"){ + type = 'Input Data' + input_data = {key: dataset.key, name: dataset.name, file_size: dataset.file_size, phase: phase.name, task: task.name, type: type} + }else if(dataset.type === "reference_data"){ + type = 'Reference Data' + reference_data = {key: dataset.key, name: dataset.name, file_size: dataset.file_size, phase: phase.name, task: task.name, type: type} + }else if(dataset.type === "ingestion_program"){ + type = 'Ingestion Program' + ingestion_program = {key: dataset.key, name: dataset.name, file_size: dataset.file_size, phase: phase.name, task: task.name, type: type} + }else if(dataset.type === "scoring_program"){ + type = 'Scoring Program' + scoring_program = {key: dataset.key, name: dataset.name, file_size: dataset.file_size, phase: phase.name, task: task.name, type: type} + } + }) + if(self.competition.admin){ + self.competition.files.push(input_data) + self.competition.files.push(reference_data) + self.competition.files.push(ingestion_program) + self.competition.files.push(scoring_program) + } + }) _.forEach(phase.tasks, task => { _.forEach(task.solutions, solution => { self.competition.files.push({ - key: solution.data.key, + key: solution.data, name: solution.name, - file_size: solution.data.file_size, + file_size: solution.size, phase: phase.name, task: task.name, + type: 'Solution' }) }) }) + // Need code for public_data and starting_kit at phase level + if(self.competition.participant_status === 'approved'){ + if (phase.starting_kit != null){ + self.competition.files.push({ + key: phase.starting_kit.key, + name: phase.starting_kit.name, + file_size: phase.starting_kit.file_size, + phase: phase.name, + task: '-', + type: 'Starting Kit' + }) + } + if (phase.public_data != null){ + self.competition.files.push({ + key: phase.public_data.key, + name: phase.public_data.name, + file_size: phase.public_data.file_size, + phase: phase.name, + task: '-', + type: 'Public Data' + }) + } + } + }) + // loop over competition phases to mark if phase has started or ended + self.competition.phases.forEach(function (phase, index) { + + phase_ended = false + phase_started = false + + // check if phase has started + if((Date.parse(phase["start"]) - Date.parse(new Date())) > 0){ + // start date is in the future, phase started = NO + phase_started = false + }else{ + // start date is not in the future, phase started = YES + phase_started = true + } + + if(phase_started){ + // check if end data exists for this phase + if(phase["end"]){ + if((Date.parse(phase["end"]) - Date.parse(new Date())) < 0){ + // Phase cannote accept submissions if end date is in the past + phase_ended = true + }else{ + // Phase can accept submissions if end date is in the future + phase_ended = false + } + }else{ + // Phase can accept submissions if end date is not given + phase_ended = false + } + } + self.competition.phases[index]["phase_ended"] = phase_ended + self.competition.phases[index]["phase_started"] = phase_started }) self.competition.is_admin = CODALAB.state.user.has_competition_admin_privileges(competition) diff --git a/src/static/riot/competitions/editor/_phases.tag b/src/static/riot/competitions/editor/_phases.tag index e2dc437c1..62305a354 100644 --- a/src/static/riot/competitions/editor/_phases.tag +++ b/src/static/riot/competitions/editor/_phases.tag @@ -96,6 +96,27 @@ multiple="multiple"> + +
+ + +
+
+ + +
@@ -176,6 +197,8 @@ self.form_is_valid = false self.phases = [] self.phase_tasks = [] + self.phase_public_data = [] + self.phase_starting_kit = [] self.selected_phase_index = undefined self.warnings = [] @@ -197,6 +220,29 @@ onRemove: self.task_removed, }) + $(self.refs.public_data_multiselect).dropdown({ + apiSettings: { + url: `${URLS.API}datasets/?search={query}&type=public_data`, + onResponse: (data) => { + return {success: true, results: _.values(data.results)} + }, + }, + onAdd: self.public_data_added, + onRemove: self.public_data_removed, + }) + + $(self.refs.starting_kit_multiselect).dropdown({ + apiSettings: { + url: `${URLS.API}datasets/?search={query}&type=starting_kit`, + onResponse: (data) => { + return {success: true, results: _.values(data.results)} + }, + }, + onAdd: self.starting_kit_added, + onRemove: self.starting_kit_removed, + }) + // When adding \ removing phase we need to code it like above + // Form change events $(':input', self.root).not('[type="file"]').not('button').not('[readonly]').each(function (i, field) { this.addEventListener('keyup', self.form_updated) @@ -219,13 +265,14 @@ /*--------------------------------------------------------------------- Methods ---------------------------------------------------------------------*/ + // Tasks self.task_added = (key, text, item) => { let index = _.findIndex(self.phase_tasks, (task) => { return task.value === key }) if (index === -1) { let task = {name: text, value: key, selected: true} - self.phase_tasks.push(task) + self.phase_tasks.push(task) } self.form_updated() } @@ -238,6 +285,68 @@ self.form_updated() } + // Public Data + self.public_data_added = (key, text, item) => { + let index = _.findIndex(self.phase_public_data, (public_data) => { + if (public_data === null){ + return false + }else{ + if (public_data.value != key){ + // Remove if not first selected. We can have only one. + alert("Only one Public Data set allowed per phase.") + setTimeout(()=>{$('a[data-value="'+ key +'"] .delete.icon').click()},100) + } + return public_data.value === key + } + }) + if (index === -1 && (self.phase_public_data.length === 0 || self.phase_public_data[0] === null)) { + let public_data = {name: text, value: key, selected: true} + self.phase_public_data[0] = public_data + } + self.form_updated() + } + + self.public_data_removed = (key, text, item) => { + let index = _.findIndex(self.phase_public_data, (public_data) => { + return public_data.value === key + }) + if (index != -1){ + self.phase_public_data.splice(index, 1) + } + self.form_updated() + } + + // Starting Kit + self.starting_kit_added = (key, text, item) => { + let index = _.findIndex(self.phase_starting_kit, (starting_kit) => { + if (starting_kit === null){ + return false + }else{ + if (starting_kit.value != key){ + // Remove if not first selected. We can have only one. + alert("Only one Starting Kit set allowed per phase.") + setTimeout(()=>{$('a[data-value="'+ key +'"] .delete.icon').click()},100) + } + return starting_kit.value === key + } + }) + if (index === -1 && (self.phase_starting_kit.length === 0 || self.phase_starting_kit[0] === null)) { + let starting_kit = {name: text, value: key, selected: true} + self.phase_starting_kit[0] = starting_kit + } + self.form_updated() + } + + self.starting_kit_removed = (key, text, item) => { + let index = _.findIndex(self.phase_starting_kit, (starting_kit) => { + return starting_kit.value === key + }) + self.phase_starting_kit.splice(index, 1) + self.form_updated() + } + + + self.show_modal = function () { $(self.refs.modal).modal('show') @@ -286,6 +395,7 @@ is_valid = false } else { // Make sure each phase has the proper details + // BB - check for public_data and starting_kit - NOT DONE self.phases.forEach(function (phase) { if (!phase.name || !phase.start || phase.tasks.length === 0) { is_valid = false @@ -338,6 +448,8 @@ self.selected_phase_index = undefined self.phase_tasks = [] $(self.refs.multiselect).dropdown('clear') + $(self.refs.public_data_multiselect).dropdown('clear') + $(self.refs.starting_kit_multiselect).dropdown('clear') $(':input', self.refs.form) .not('[type="file"]') @@ -369,6 +481,9 @@ self.selected_phase_index = index var phase = self.phases[index] self.phase_tasks = phase.tasks + self.phase_public_data = [phase.public_data] + self.phase_starting_kit = [phase.starting_kit] + self.update() set_form_data(phase, self.refs.form) @@ -389,11 +504,40 @@ selected: true, } })) + // Setting Public Data + if(self.phase_public_data[0] != null){ + $(self.refs.public_data_multiselect) + .dropdown('change values', _.map(self.phase_public_data, public_data => { + // renaming things to work w/ semantic UI multiselect + return { + value: public_data.value, + text: public_data.name, + name: public_data.name, + selected: true, + } + })) + } + // Setting Starting Kit + if(self.phase_starting_kit[0] != null){ + $(self.refs.starting_kit_multiselect) + .dropdown('change values', _.map(self.phase_starting_kit, starting_kit => { + // renaming things to work w/ semantic UI multiselect + return { + //value: starting_kit.value, // Maybe need to grab from serializer? + value: starting_kit.value, + text: starting_kit.name, + name: starting_kit.name, + selected: true, + } + })) + } self.show_modal() // make semantic multiselect sortable -- Sortable library imported in competitions/form.html Sortable.create($('.search.dropdown.multiple', self.refs.tasks_select_container)[0]) + Sortable.create($('.search.dropdown.multiple', self.refs.public_data_select_container)[0]) + Sortable.create($('.search.dropdown.multiple', self.refs.starting_kit_select_container)[0]) self.form_check_is_valid() self.update() @@ -432,8 +576,48 @@ }) self.phase_tasks = sorted_phase_tasks.slice() + // Get public data from DOM + let public_data_from_dom = [] + $("#public_data_select_container a").each(function () { + public_data_from_dom.push($(this).data("value")) + }) + let sorted_phase_public_data = [] + public_data_from_dom.forEach( function(key) { + let found = false; + self.phase_public_data = self.phase_public_data.filter(function (item) { + if(!found && item['value'] == key){ + sorted_phase_public_data.push(item) + found = true + return false + } else + return true + }) + }) + self.phase_public_data = sorted_phase_public_data.slice() + + // Get starting kit from DOM + let starting_kit_from_dom = [] + $("#starting_kit_select_container a").each(function () { + starting_kit_from_dom.push($(this).data("value")) + }) + let sorted_phase_starting_kit = [] + starting_kit_from_dom.forEach( function(key) { + let found = false; + self.phase_starting_kit = self.phase_starting_kit.filter(function (item) { + if(!found && item['value'] == key){ + sorted_phase_starting_kit.push(item) + found = true + return false + } else + return true + }) + }) + self.phase_starting_kit = sorted_phase_starting_kit.slice() + var data = get_form_data(self.refs.form) data.tasks = self.phase_tasks + data.public_data = self.phase_public_data.length === 0 ? null : self.phase_public_data[0] + data.starting_kit = self.phase_starting_kit.length === 0 ? null : self.phase_starting_kit[0] data.task_instances = [] for(task of self.phase_tasks){ data.task_instances.push({ diff --git a/src/static/riot/competitions/editor/form.tag b/src/static/riot/competitions/editor/form.tag index 184590ffc..6a877d383 100644 --- a/src/static/riot/competitions/editor/form.tag +++ b/src/static/riot/competitions/editor/form.tag @@ -241,7 +241,6 @@ // Send competition_id for either create or update, won't hurt anything but is // useless for creation - api_endpoint(self.competition_return, self.opts.competition_id) .done(function (response) { self.errors = {} diff --git a/src/static/riot/tasks/management.tag b/src/static/riot/tasks/management.tag index 0fdf320f2..d9013d630 100644 --- a/src/static/riot/tasks/management.tag +++ b/src/static/riot/tasks/management.tag @@ -462,7 +462,6 @@ self.search_tasks = function () { var filter = self.refs.search.value - delay(() => self.update_tasks({search: filter}), 100) }