Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"version": "0.0.1",
"dependencies": {
"jquery": "^4.0.0",
"js-beautify": "^1.15.4",
"npm-watch": "^0.13.0",
"riot": "^3.13.2",
"stylus": "^0.64.0",
"uglify-js": "^3.19.3"
},
"devDependencies": {},
"watch": {
"build-stylus": {
"patterns": [
Expand Down
2 changes: 1 addition & 1 deletion src/apps/api/serializers/leaderboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def get_submissions(self, instance):
leaderboard=instance,
is_specific_task_re_run=False
)
.select_related('owner')
.select_related('owner', 'queue')
.prefetch_related('scores')
.annotate(primary_col=Sum('scores__score', filter=Q(scores__column=primary_col)))
)
Expand Down
34 changes: 33 additions & 1 deletion src/apps/api/serializers/submission_leaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,43 @@ class SubmissionLeaderBoardSerializer(serializers.ModelSerializer):
slug_url = serializers.CharField(source='owner.slug_url')
organization = SimpleOrganizationSerializer(allow_null=True)
created_when = serializers.DateTimeField()
queue_id = serializers.SerializerMethodField()
queue_name = serializers.SerializerMethodField()

def _get_effective_queue(self, obj):
if obj.queue:
return obj.queue

if obj.parent and obj.parent.queue:
return obj.parent.queue

if obj.phase and obj.phase.competition and obj.phase.competition.queue:
return obj.phase.competition.queue

return None

def _get_display_queue_name(self, obj):
queue = self._get_effective_queue(obj)
if not queue:
return None

raw_name = queue.name or ""
group_name = raw_name.rsplit("__", 1)[-1] # comp10__CLB -> CLB, APHP -> APHP
submission_parent_id = obj.parent_id or obj.id

return f"{submission_parent_id}_{group_name}"

def get_queue_name(self, obj):
return self._get_display_queue_name(obj)

def get_queue_id(self, obj):
queue = self._get_effective_queue(obj)
return queue.id if queue else None

class Meta:
model = Submission
fields = (
'id', 'parent', 'owner', 'leaderboard_id', 'fact_sheet_answers',
'task', 'scores', 'display_name', 'slug_url', 'organization',
'detailed_result', 'created_when'
'detailed_result', 'created_when', 'queue_name', 'queue_id',
)
141 changes: 108 additions & 33 deletions src/apps/api/views/competitions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import zipfile
import json
import csv
from collections import OrderedDict
from collections import Counter, OrderedDict
from io import StringIO
from django.http import HttpResponse
from tempfile import SpooledTemporaryFile
Expand Down Expand Up @@ -424,26 +424,43 @@ def collect_leaderboard_data(self, competition, phase_pk=None):
phase_id = phases[0].id

leaderboard = Leaderboard.objects.prefetch_related('columns').get(phases=phase_id)
leaderboard_titles = {phase['id']: f'{leaderboard.title} - {phase["name"]}({phase["id"]})' for phase in submission_query}
leaderboard_titles = {
phase['id']: f'{leaderboard.title} - {phase["name"]}({phase["id"]})'
for phase in submission_query
}
leaderboard_data = {title: {} for title in leaderboard_titles.values()}

for phase in submission_query:
generated_columns = OrderedDict()
for task in phase['tasks']:
for col in leaderboard.columns.all():
generated_columns.update({f'{col.key}-{task["id"]}': f'{task["name"]}({task["id"]})-{col.title}'})
generated_columns.update({
f'{col.key}-{task["id"]}': f'{task["name"]}({task["id"]})-{col.title}'
})

for submission in phase['submissions']:
submission_key = f'{submission["owner"]}-{submission["parent"] or submission["id"]}'
if submission_key not in leaderboard_data[leaderboard_titles[phase['id']]].keys():
leaderboard_data[leaderboard_titles[phase['id']]].update({submission_key: OrderedDict()})
if 'fact_sheet_answers' in submission.keys() and submission['fact_sheet_answers']:
leaderboard_data[leaderboard_titles[phase['id']]][submission_key]\
.update({'fact_sheet_answers': submission['fact_sheet_answers']})
queue_name = submission.get('queue_name') or ''
submission_key = f'{submission["owner"]}-{submission["id"]}'
if queue_name:
submission_key = f'{submission_key}-{queue_name}'

if submission_key not in leaderboard_data[leaderboard_titles[phase['id']]]:
leaderboard_data[leaderboard_titles[phase['id']]][submission_key] = OrderedDict()

if submission.get('fact_sheet_answers'):
leaderboard_data[leaderboard_titles[phase['id']]][submission_key].update({
'fact_sheet_answers': submission['fact_sheet_answers']
})

for col_title in generated_columns.values():
leaderboard_data[leaderboard_titles[phase['id']]][submission_key].update({col_title: ""})

for score in submission['scores']:
score_column = generated_columns[f'{score["column_key"]}-{submission["task"]}']
leaderboard_data[leaderboard_titles[phase['id']]][submission_key].update({score_column: score['score']})
leaderboard_data[leaderboard_titles[phase['id']]][submission_key].update({
score_column: score['score']
})

return leaderboard_data

@action(detail=True, methods=['GET'], renderer_classes=[JSONRenderer, CSVRenderer, ZipRenderer])
Expand Down Expand Up @@ -773,6 +790,23 @@ def rerun_submissions(self, request, pk):
@action(detail=True, methods=['GET'], permission_classes=[AllowAny])
def get_leaderboard(self, request, pk):
phase = self.get_object()

def _clean_group_label(raw_name, submission_parent_id=None):
if not raw_name:
return None

label = str(raw_name)

if submission_parent_id is not None:
prefix = f"{submission_parent_id}_"
if label.startswith(prefix):
label = label[len(prefix):]

if "__" in label:
label = label.rsplit("__", 1)[1]

return label or None

if phase.competition.fact_sheet:
fact_sheet_keys = [
(
Expand All @@ -792,21 +826,73 @@ def get_leaderboard(self, request, pk):
'submissions': [],
'tasks': [],
'fact_sheet_keys': fact_sheet_keys or None,
'primary_index': query['leaderboard']['primary_index']
'primary_index': query['leaderboard']['primary_index'],
'has_group_queues': False,
}

columns = [col for col in query['columns']]
columns = list(query['columns'])
submissions_keys = {}
submission_detailed_results = {}

group_name_by_user_queue = {}
for group in phase.competition.participant_groups.filter(
queue__isnull=False
).select_related('queue').prefetch_related('user_set'):
cleaned_group_name = _clean_group_label(group.name)
for user in group.user_set.all():
group_name_by_user_queue[(user.username, group.queue_id)] = cleaned_group_name

parent_ids = {
s['parent']
for s in query['submissions']
if s['parent'] is not None
}
parent_task_counts = Counter(
(s['parent'], s['task'])
for s in query['submissions']
if s['parent'] is not None
)

for submission in query['submissions']:
submission_key = f"{submission['owner']}{submission['parent'] or submission['id']}"
if submission['id'] in parent_ids:
continue

submission_parent_id = submission.get('parent') or submission.get('id')
raw_queue_name = submission.get('queue_name') or ''
queue_id = submission.get('queue_id')

group_name = group_name_by_user_queue.get(
(submission['owner'], queue_id)
) if queue_id else None

group_label = _clean_group_label(
group_name or raw_queue_name,
submission_parent_id=submission_parent_id
)

display_group = (
f"{submission_parent_id}_{group_label}"
if group_label
else None
)

parent_id = submission['parent']
task_id = submission.get('task')

# Cas particulier: plusieurs submissions d'un même parent sans queue explicite
is_multi_group_null_queue = (
parent_id is not None
and not queue_id
and parent_task_counts.get((parent_id, task_id), 0) > 1
)

if is_multi_group_null_queue:
submission_key = f"{submission['owner']}{parent_id}_{submission['id']}"
else:
submission_key = f"{submission['owner']}{submission_parent_id}_{group_label or ''}"

# 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']
})
Expand All @@ -821,23 +907,18 @@ def get_leaderboard(self, request, pk):
'fact_sheet_answers': submission['fact_sheet_answers'],
'slug_url': submission['slug_url'],
'organization': submission['organization'],
'created_when': submission['created_when']
'created_when': submission['created_when'],
'queue_name': display_group,
})

for score in submission['scores']:
if queue_id or is_multi_group_null_queue:
response['has_group_queues'] = True

# 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
for score in submission['scores']:
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"]
Expand All @@ -847,13 +928,8 @@ def get_leaderboard(self, request, pk):

tempScore = score
tempScore['task_id'] = submission['task']
# round the score to 'precision' decimal points
tempScore['score'] = str(round(float(tempScore["score"]), precision))

# 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)

Expand All @@ -877,7 +953,6 @@ def get_leaderboard(self, request, pk):
# --- end pagination addition ---

for task in query['tasks']:
# This can be used to rendered variable columns on each task
tempTask = {
'name': task['name'],
'id': task['id'],
Expand Down
48 changes: 47 additions & 1 deletion src/apps/competitions/admin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from django.contrib import admin
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Group

from django.utils.translation import gettext_lazy as _
import json
import csv
from django.http import HttpResponse
from profiles.models import User
from profiles.models import CustomGroup, User
from . import models


Expand Down Expand Up @@ -409,3 +413,45 @@ class PhaseExpansion(admin.ModelAdmin):
admin.site.register(models.Page, PageExpansion)
admin.site.register(models.Phase, PhaseExpansion)
admin.site.register(models.Submission, SubmissionExpansion)


class CustomGroupAdminForm(forms.ModelForm):
users = forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
required=False,
widget=FilteredSelectMultiple("Users", is_stacked=False),
help_text="Add/Remove users for this group."
)

class Meta:
model = CustomGroup
fields = ('name', 'permissions', 'queue', 'users')

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk:
self.fields['users'].initial = self.instance.user_set.all()


@admin.register(CustomGroup)
class CustomGroupAdmin(admin.ModelAdmin):
form = CustomGroupAdminForm
list_display = ('name', 'queue')
search_fields = ('name',)
filter_horizontal = ('permissions',)
fieldsets = (
(None, {'fields': ('name',)}),
('Permissions', {'fields': ('permissions',)}),
('Utilisateurs', {'fields': ('users',)}),
('Options', {'fields': ('queue',)}),
)

def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)

def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
form.instance.user_set.set(form.cleaned_data['users'])


admin.site.unregister(Group)
5 changes: 4 additions & 1 deletion src/apps/competitions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from celery_config import app, app_for_vhost
from leaderboards.models import SubmissionScore
from profiles.models import User, Organization
from profiles.models import CustomGroup, User, Organization
from utils.data import PathWrapper
from utils.storage import BundleStorage
from PIL import Image
Expand Down Expand Up @@ -55,6 +55,9 @@ class Competition(models.Model):
make_programs_available = models.BooleanField(default=False)
make_input_data_available = models.BooleanField(default=False)

participant_groups = models.ManyToManyField(CustomGroup, blank=True, related_name='competitions', verbose_name="group of participants",
help_text="Competition owner being able to create groups of users.")

queue = models.ForeignKey('queues.Queue', on_delete=models.SET_NULL, null=True, blank=True,
related_name='competitions')

Expand Down
Loading