Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5bd439c
organization message displayed
ihsaan-ullah Jun 22, 2023
5b2e73c
spelling mistake fixed
ihsaan-ullah Jun 22, 2023
ab69972
add organization added in dropdown
ihsaan-ullah Jun 22, 2023
b954507
hide organization members emails from non-members
ihsaan-ullah Jul 31, 2023
07e9584
Competition api security solved
ihsaan-ullah Aug 1, 2023
0c3175c
404 page on edit competition which does not belong to you
ihsaan-ullah Aug 1, 2023
734c02e
competition create and update view changed
ihsaan-ullah Aug 1, 2023
e3b4c30
extra print removed
ihsaan-ullah Aug 2, 2023
a17036e
organization shown if submission is submitted by it
ihsaan-ullah Aug 2, 2023
c7aeccf
V2 unpacker: raise error when phase1 end and phase2 start dates are same
ihsaan-ullah Aug 3, 2023
8e290a3
V1 unpacker: phase with no end date, use next phase start date -1 as …
ihsaan-ullah Aug 3, 2023
673ec9a
import resolved
ihsaan-ullah Aug 3, 2023
06b17a9
Phase testing data updated to match the new change
ihsaan-ullah Aug 3, 2023
4201dd6
V2 test data updated to match the new logic
ihsaan-ullah Aug 3, 2023
a04b5a6
submission panel see your org and orgs where you are added
ihsaan-ullah Aug 3, 2023
badb078
score in bold if more than one score and score is primary i.e. index …
ihsaan-ullah Aug 5, 2023
933d9f9
when queue is updated to private, make it null in all competitions be…
ihsaan-ullah Aug 5, 2023
e9e7250
default condition added to filter out private competitions from other…
ihsaan-ullah Aug 9, 2023
64f76f3
cache removed
ihsaan-ullah Aug 9, 2023
e31937d
primary col bold fixed, hidden scores not returned in api anymore
ihsaan-ullah Aug 10, 2023
09bea7a
Merge pull request #1047 from codalab/api_security_competitions
Didayolo Aug 10, 2023
05650ff
index was repeated
ihsaan-ullah Aug 10, 2023
3da317c
Merge pull request #1062 from codalab/queue_make_private
Didayolo Aug 10, 2023
32b4c4b
Merge pull request #1061 from codalab/leaderboard_bold_score
Didayolo Aug 10, 2023
819087f
Merge pull request #1054 from codalab/phase_dates
Didayolo Aug 10, 2023
8d05cb1
Merge pull request #1046 from codalab/security_organization_members
Didayolo Aug 10, 2023
a1f9dc0
Merge pull request #1056 from codalab/submission_panel_organization
Didayolo Aug 10, 2023
3b1a8d8
Merge branch 'develop' into submit_as_organization
Didayolo Aug 10, 2023
e10d9b9
Update leaderboards.tag
Didayolo Aug 10, 2023
bf85cc3
Merge pull request #1053 from codalab/submit_as_organization
Didayolo Aug 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/apps/api/serializers/leaderboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 = (
Expand All @@ -140,6 +144,7 @@ class Meta:
'tasks',
'leaderboard',
'columns',
'primary_index',
)
depth = 1

Expand Down
79 changes: 67 additions & 12 deletions src/apps/api/views/competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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':
Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion src/apps/api/views/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
22 changes: 21 additions & 1 deletion src/apps/api/views/queues.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions src/apps/competitions/tests/unpacker_test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
},
{
Expand Down Expand Up @@ -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],
Expand Down
6 changes: 6 additions & 0 deletions src/apps/competitions/unpackers/base_unpacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
7 changes: 6 additions & 1 deletion src/apps/competitions/unpackers/v1.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import datetime

from competitions.unpackers.base_unpacker import BaseUnpacker
from competitions.unpackers.utils import CompetitionUnpackingException, get_datetime
Expand Down Expand Up @@ -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':
Expand Down
4 changes: 2 additions & 2 deletions src/apps/competitions/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
# path('', views.CompetitionList.as_view(), name="list"),
path('', views.CompetitionManagement.as_view(), name="management"),
path('<int:pk>/', views.CompetitionDetail.as_view(), name="detail"),
path('create/', views.CompetitionForm.as_view(), name="create"),
path('edit/<int:pk>/', views.CompetitionForm.as_view(), name="edit"),
path('create/', views.CompetitionCreateForm.as_view(), name="create"),
path('edit/<int:pk>/', views.CompetitionUpdateForm.as_view(), name="edit"),
path('upload/', views.CompetitionUpload.as_view(), name="upload"),
path('public/', views.CompetitionPublic.as_view(), name="public"),
path('<int:pk>/detailed_results/<int:submission_id>/', views.CompetitionDetailedResults.as_view(), name="detailed_results"),
Expand Down
43 changes: 42 additions & 1 deletion src/apps/competitions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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'
1 change: 1 addition & 0 deletions src/apps/profiles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/apps/profiles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
10 changes: 8 additions & 2 deletions src/static/js/ours/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/static/riot/competitions/detail/detail.tag
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading