diff --git a/src/apps/api/serializers/leaderboards.py b/src/apps/api/serializers/leaderboards.py index 74aede7f5..2ab509898 100644 --- a/src/apps/api/serializers/leaderboards.py +++ b/src/apps/api/serializers/leaderboards.py @@ -123,6 +123,7 @@ class LeaderboardPhaseSerializer(serializers.ModelSerializer): submissions = serializers.SerializerMethodField(read_only=True) columns = serializers.SerializerMethodField() tasks = PhaseTaskInstanceSerializer(source='task_instances', many=True) + primary_index = serializers.SerializerMethodField() def get_columns(self, instance): columns = Column.objects.filter(leaderboard=instance.leaderboard, hidden=False) @@ -131,6 +132,9 @@ def get_columns(self, instance): else: return ColumnSerializer(columns, many=len(columns) >= 1).data + def get_primary_index(self, instance): + return instance.leaderboard.primary_index + class Meta: model = Phase fields = ( @@ -140,6 +144,7 @@ class Meta: 'tasks', 'leaderboard', 'columns', + 'primary_index', ) depth = 1 diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index 206eedccf..8fcb69a4b 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -45,15 +45,24 @@ class CompetitionViewSet(ModelViewSet): permission_classes = (AllowAny,) def get_queryset(self): + qs = super().get_queryset() + + # filter by competition_type first, 'competition' by default + competition_type = self.request.query_params.get('type', Competition.COMPETITION) + if competition_type != 'any' and self.detail is False: + qs = qs.filter(competition_type=competition_type) + # Filter for search bar search_query = self.request.query_params.get('search') + + # Competition Secret key check + secret_key = self.request.query_params.get('secret_key') + # If user is logged in if self.request.user.is_authenticated: - # filter by competition_type first, 'competition' by default - competition_type = self.request.query_params.get('type', Competition.COMPETITION) - if competition_type != 'any' and self.detail is False: - qs = qs.filter(competition_type=competition_type) + + # `mine` is true when this is called from "Benchmarks I'm Running" # Filter to only see competitions you own mine = self.request.query_params.get('mine', None) if mine: @@ -65,19 +74,19 @@ def get_queryset(self): (Q(collaborators__in=[self.request.user])) ).distinct() + # `participating_in` is true when this is called from "Benchmarks I'm in" participating_in = self.request.query_params.get('participating_in', None) if participating_in: qs = qs.filter(participants__user=self.request.user, participants__status="approved") + participant_status_query = CompetitionParticipant.objects.filter( competition=OuterRef('pk'), user=self.request.user ).values_list('status')[:1] qs = qs.annotate(participant_status=Subquery(participant_status_query)) - # `mine` is true when this is called from "Benchmarks I'm Running" - # `participating_in` is true when this is called from "Benchmarks I'm in" - # `search_query` is true when this is called from the search bar + + # if `search_query` is true, this is called form search bar if search_query: - # User is logged in then filter # competitions which this user owns # or # competitions in which this user is collaborator @@ -91,10 +100,40 @@ def get_queryset(self): (Q(published=True) & ~Q(created_by=self.request.user)) | (Q(participants__user=self.request.user) & Q(participants__status="approved")) ).distinct() + + # if `secret_key` is true, this is called for a secret competition + if secret_key: + print(secret_key) + qs = qs.filter(Q(secret_key=secret_key)) + + # Default condition + # not called from my competitions tab + # not called from i'm participating in tab + # not called from search bar + # not called with a valid secret key + # Return the following --- + # All competitions which belongs to you (private or public) + # And competitions where you are admin + # And public competitions + # And competitions where you are approved participant + # this filters out all private compettions from other users + if (not mine) and (not participating_in) and (not secret_key) and (not search_query): + qs = qs.filter( + (Q(created_by=self.request.user)) | + (Q(collaborators__in=[self.request.user])) | + (Q(published=True) & ~Q(created_by=self.request.user)) | + (Q(participants__user=self.request.user) & Q(participants__status="approved")) + ).distinct() + else: - # if user is not authenticated only show public competitions in the search - if (search_query): - qs = qs.filter(Q(published=True)) + # if user is not authenticated only show + # public competitions + # or + # competition with valid secret key + qs = qs.filter( + (Q(published=True)) | + (Q(secret_key=secret_key)) + ) # On GETs lets optimize the query to reduce DB calls if self.request.method == 'GET': @@ -570,6 +609,7 @@ def get_leaderboard(self, request, pk): 'submissions': [], 'tasks': [], 'fact_sheet_keys': fact_sheet_keys or None, + 'primary_index': query['leaderboard']['primary_index'] } columns = [col for col in query['columns']] submissions_keys = {} @@ -617,21 +657,36 @@ def get_leaderboard(self, request, pk): }) for score in submission['scores']: + # to check if a column is found + # this is useful because of `hidden` field + # if a column is hidden it will not be shown here so + # we will not return that score to the front-end + column_found = False # default precision is set to 2 precision = 2 + # default hidden is set to false + hidden = False # loop over columns to find a column with the same index # replace default precision with column precision for col in columns: if col["index"] == score["index"]: precision = col["precision"] + hidden = col["hidden"] + column_found = True break tempScore = score tempScore['task_id'] = submission['task'] # round the score to 'precision' decimal points tempScore['score'] = str(round(float(tempScore["score"]), precision)) - response['submissions'][submissions_keys[submission_key]]['scores'].append(tempScore) + + # only add scores to the scores list + # if this column is found + # and + # column is not hidden + if column_found and not hidden: + response['submissions'][submissions_keys[submission_key]]['scores'].append(tempScore) # put detailed results in its submission for k, v in submissions_keys.items(): diff --git a/src/apps/api/views/profiles.py b/src/apps/api/views/profiles.py index 6492a5efb..d2068b4c0 100644 --- a/src/apps/api/views/profiles.py +++ b/src/apps/api/views/profiles.py @@ -45,7 +45,7 @@ def update(self, request, *args, **kwargs): @action(detail=False, methods=['get'], permission_classes=[IsAuthenticated]) def participant_organizations(self, request): - memberships = request.user.membership_set.filter(group__in=Membership.PARTICIPANT_GROUP).prefetch_related('organization') + memberships = request.user.membership_set.filter(group__in=Membership.ALL_GROUP).prefetch_related('organization') data = SimpleOrganizationSerializer([member.organization for member in memberships], many=True).data return Response(data) diff --git a/src/apps/api/views/queues.py b/src/apps/api/views/queues.py index 4a5870c64..db96188c6 100644 --- a/src/apps/api/views/queues.py +++ b/src/apps/api/views/queues.py @@ -46,7 +46,27 @@ def update(self, request, *args, **kwargs): queue = self.get_object() if request.user != queue.owner and not request.user.is_superuser: raise PermissionDenied("Cannot update a queue that is not yours") - return super().update(request, *args, **kwargs) + + # Get the original value of is_public before updating + before_update_queue_is_public = queue.is_public + + # Get the competitions that are using this queue + competitions = queue.competitions.all() + + # Update the queue + updated_queue_response = super().update(request, *args, **kwargs) + + # If the queue `is_public`` field is updated to False, then update competitions + if 'is_public' in request.data and not request.data['is_public'] and before_update_queue_is_public: + + # Set the queue field in all competitions to NULL + # which do not belong to the user + for competition in competitions: + if competition.created_by != request.user: + competition.queue = None + competition.save() + + return updated_queue_response def destroy(self, request, *args, **kwargs): instance = self.get_object() diff --git a/src/apps/competitions/tests/unpacker_test_data.py b/src/apps/competitions/tests/unpacker_test_data.py index 45cd096d5..0d7aa3630 100644 --- a/src/apps/competitions/tests/unpacker_test_data.py +++ b/src/apps/competitions/tests/unpacker_test_data.py @@ -106,7 +106,7 @@ "execution_time_limit": 500, "max_submissions_per_day": 5, "start": "2019-01-01", - "end": "2019-09-30", + "end": "2019-09-29", "tasks": [0] }, { @@ -204,7 +204,7 @@ 'max_submissions_per_person': None, 'auto_migrate_to_this_phase': False, 'has_max_submissions': True, - 'end': datetime.datetime(2019, 9, 30, 0, 0, tzinfo=timezone.now().tzinfo), + 'end': datetime.datetime(2019, 9, 29, 0, 0, tzinfo=timezone.now().tzinfo), 'public_data': None, 'starting_kit': None, 'tasks': [0], diff --git a/src/apps/competitions/unpackers/base_unpacker.py b/src/apps/competitions/unpackers/base_unpacker.py index a769dc2eb..bf9f686db 100644 --- a/src/apps/competitions/unpackers/base_unpacker.py +++ b/src/apps/competitions/unpackers/base_unpacker.py @@ -127,6 +127,12 @@ def _validate_phase_ordering(self): f'Phases must be sequential. Phase: {phase2.get("name", phase2["index"])}' f'starts before Phase: {phase1.get("name", phase1["index"])} has ended' ) + elif phase1['end'] == phase2['start']: + # Current phase start date and previous phase end dates are same, raise error + raise CompetitionUnpackingException( + f'Phases dates conflict. Phase: {phase2.get("name", phase2["index"])} ' + f'should start after Phase: {phase1.get("name", phase1["index"])} has ended' + ) def _unpack_pages(self): """ diff --git a/src/apps/competitions/unpackers/v1.py b/src/apps/competitions/unpackers/v1.py index 27f9c9956..63a808629 100644 --- a/src/apps/competitions/unpackers/v1.py +++ b/src/apps/competitions/unpackers/v1.py @@ -1,4 +1,5 @@ import os +import datetime from competitions.unpackers.base_unpacker import BaseUnpacker from competitions.unpackers.utils import CompetitionUnpackingException, get_datetime @@ -90,7 +91,11 @@ def _unpack_phases(self): new_phase['has_max_submissions'] = True try: next_phase = phases[index + 1] - new_phase['end'] = get_datetime(next_phase['start_date']) + # V1 phases have no end dates. + # to set an end date of a phase, get the next phase starting date + next_phase_start_date = get_datetime(next_phase['start_date']) + # subtract one day from it and use it as this phase end date + new_phase['end'] = next_phase_start_date - datetime.timedelta(days=1) except IndexError: end = self.competition.get('end_date') if end and end != 'null': diff --git a/src/apps/competitions/urls.py b/src/apps/competitions/urls.py index a3d4419a9..705c1d77c 100644 --- a/src/apps/competitions/urls.py +++ b/src/apps/competitions/urls.py @@ -8,8 +8,8 @@ # path('', views.CompetitionList.as_view(), name="list"), path('', views.CompetitionManagement.as_view(), name="management"), path('/', views.CompetitionDetail.as_view(), name="detail"), - path('create/', views.CompetitionForm.as_view(), name="create"), - path('edit//', views.CompetitionForm.as_view(), name="edit"), + path('create/', views.CompetitionCreateForm.as_view(), name="create"), + path('edit//', views.CompetitionUpdateForm.as_view(), name="edit"), path('upload/', views.CompetitionUpload.as_view(), name="upload"), path('public/', views.CompetitionPublic.as_view(), name="public"), path('/detailed_results//', views.CompetitionDetailedResults.as_view(), name="detailed_results"), diff --git a/src/apps/competitions/views.py b/src/apps/competitions/views.py index 61d09e0f3..6d2aa3c3e 100644 --- a/src/apps/competitions/views.py +++ b/src/apps/competitions/views.py @@ -13,10 +13,40 @@ class CompetitionPublic(TemplateView): template_name = 'competitions/public.html' -class CompetitionForm(LoginRequiredMixin, TemplateView): +class CompetitionCreateForm(LoginRequiredMixin, TemplateView): template_name = 'competitions/form.html' +class CompetitionUpdateForm(LoginRequiredMixin, DetailView): + template_name = 'competitions/form.html' + queryset = Competition.objects.all() + + def get_object(self, *args, **kwargs): + competition = super().get_object(*args, **kwargs) + + is_admin, is_creator, is_collaborator = False, False, False + + # check if user is loggedin + if self.request.user.is_authenticated: + + # check if user is admin + is_admin = self.request.user.is_superuser + + # check if user is the creator of this competition + is_creator = self.request.user == competition.created_by + + # check if user is collaborator of this competition + is_collaborator = self.request.user in competition.collaborators.all() + + if ( + is_admin or + is_creator or + is_collaborator + ): + return competition + raise Http404() + + class CompetitionUpload(LoginRequiredMixin, TemplateView): template_name = 'competitions/upload.html' @@ -60,6 +90,17 @@ def get_object(self, *args, **kwargs): return competition raise Http404() + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # Retrieve the secret_key from the request.GET dictionary + secret_key = self.request.GET.get('secret_key') + + # Add the secret_key to the context dictionary + context['secret_key'] = secret_key + + return context + class CompetitionDetailedResults(LoginRequiredMixin, TemplateView): template_name = 'competitions/detailed_results.html' diff --git a/src/apps/profiles/models.py b/src/apps/profiles/models.py index 7c18e21bf..200f64de6 100644 --- a/src/apps/profiles/models.py +++ b/src/apps/profiles/models.py @@ -199,6 +199,7 @@ class Membership(models.Model): EDITORS_GROUP = [OWNER, MANAGER] PARTICIPANT_GROUP = EDITORS_GROUP + [PARTICIPANT] SETTABLE_PERMISSIONS = [MANAGER, PARTICIPANT, MEMBER] + ALL_GROUP = EDITORS_GROUP + [PARTICIPANT, MEMBER] group = models.TextField(choices=PERMISSIONS, default=INVITED, null=False, blank=False) organization = models.ForeignKey(Organization, on_delete=models.CASCADE) diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index cfa4f2e10..ab5296e9a 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -261,8 +261,11 @@ def get_context_data(self, **kwargs): membership = self.object.membership_set.filter(user=self.request.user) if len(membership) == 1: context['is_editor'] = membership.first().group in Membership.EDITORS_GROUP + context['is_member'] = membership.first().group in Membership.SETTABLE_PERMISSIONS else: context['is_editor'] = False + context['is_member'] = False + return context diff --git a/src/static/js/ours/client.js b/src/static/js/ours/client.js index f3e1a21dc..80f7cf3a0 100644 --- a/src/static/js/ours/client.js +++ b/src/static/js/ours/client.js @@ -22,8 +22,14 @@ CODALAB.api = { /*--------------------------------------------------------------------- Competitions ---------------------------------------------------------------------*/ - get_competition: function (pk) { - return CODALAB.api.request('GET', URLS.API + "competitions/" + pk + "/") + get_competition: function (pk, secret_key) { + + if(secret_key == undefined || secret_key == 'None'){ + return CODALAB.api.request('GET', URLS.API + "competitions/" + pk + "/") + }else{ + return CODALAB.api.request('GET', URLS.API + "competitions/" + pk + "/?secret_key="+secret_key) + } + }, get_competitions: function (query) { return CODALAB.api.request('GET', URLS.API + "competitions/", query) diff --git a/src/static/riot/competitions/detail/detail.tag b/src/static/riot/competitions/detail/detail.tag index 9c2c074b4..06da45698 100644 --- a/src/static/riot/competitions/detail/detail.tag +++ b/src/static/riot/competitions/detail/detail.tag @@ -17,7 +17,7 @@ }) self.update_competition_data = function () { - CODALAB.api.get_competition(self.opts.competition_pk) + CODALAB.api.get_competition(self.opts.competition_pk, self.opts.secret_key) .done(function (data) { self.competition = data CODALAB.events.trigger('competition_loaded', self.competition) diff --git a/src/static/riot/competitions/detail/leaderboards.tag b/src/static/riot/competitions/detail/leaderboards.tag index 7176cc213..e21238b23 100644 --- a/src/static/riot/competitions/detail/leaderboards.tag +++ b/src/static/riot/competitions/detail/leaderboards.tag @@ -56,17 +56,15 @@ {index + 1} { submission.owner } + { submission.organization.name } {submission.num_entries} {submission.last_entry_date} - { submission.organization.name } - {get_score(column, submission)} + {get_score(column, submission)} - - @@ -81,6 +79,22 @@ self.competition_id = null self.enable_detailed_results = false + + self.bold_class = function(column, submission){ + // Return `text-bold` if submission has + // more than one scores and score index == leaderbaord.primary_index + // otherwise return empty string + return_class = '' // default class value + if(column.task_id != -1){ // factsheet check + if(submission.scores.length > 1){ // score length check + let column_index = _.get(column, 'index') + if(column_index === self.selected_leaderboard.primary_index){ // column index check + return_class = 'text-bold' + } + } + } + return return_class + } self.get_score = function(column, submission) { if(column.task_id === -1){ return _.get(submission, 'fact_sheet_answers[' + column.key + ']', 'n/a') @@ -106,7 +120,6 @@ 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){ @@ -162,7 +175,6 @@ }) task.colWidth += 1 } - console.log(task) } self.filter_columns() $('#leaderboardTable').tablesort() @@ -220,5 +232,7 @@ top: 50% left: 50% transform: translate(-50%, -50%) + .text-bold + font-weight: bold diff --git a/src/static/riot/competitions/detail/submission_upload.tag b/src/static/riot/competitions/detail/submission_upload.tag index e6a4e5289..fe4211b77 100644 --- a/src/static/riot/competitions/detail/submission_upload.tag +++ b/src/static/riot/competitions/detail/submission_upload.tag @@ -61,7 +61,7 @@ diff --git a/src/static/riot/competitions/editor/_competition_details.tag b/src/static/riot/competitions/editor/_competition_details.tag index a91445998..6ed3f5427 100644 --- a/src/static/riot/competitions/editor/_competition_details.tag +++ b/src/static/riot/competitions/editor/_competition_details.tag @@ -155,6 +155,7 @@ // Note: Passing `public=true` so default behavior is users can search for public queues apiSettings: { url: `${URLS.API}queues/?search={query}&public=true`, + cache: false }, clearable: true, minCharacters: 2, diff --git a/src/static/riot/queues/management.tag b/src/static/riot/queues/management.tag index 062693bc2..fafb43ede 100644 --- a/src/static/riot/queues/management.tag +++ b/src/static/riot/queues/management.tag @@ -69,9 +69,8 @@ - + +