Skip to content

Commit ff23181

Browse files
IdirLISNDidayolo
andauthored
Feature group routing for submissions (#2393)
* backend & frontend OK / TODO: site worker and leaderboad * site worker sending submissions to group queue OK * leaderboad group feature * logs removed * fix json leaderboard * Clean up leaderboard ordering logic * competition queue on groups with out queue * some bugfix * UI bugfix * UI bugfix * leaderboard groups format parentsubID_groupname * fix conflicts issues * resolve conflict * clean site worker * branch update and linter fix * linter fix * linter fix * linter fix * bugfix group form * adding migrations files * fix logic to fix tests problem * fix logic to fix tests problem * E2E test fixed * E2E test fixed * E2E test fixed * E2E test fixed * Fix queue name in server status * fix queues visibility * fix queue visibility for groups * Flake8 --------- Co-authored-by: didayolo <adrien.pavao@gmail.com>
1 parent 6293127 commit ff23181

18 files changed

Lines changed: 1304 additions & 305 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"version": "0.0.1",
44
"dependencies": {
55
"jquery": "^4.0.0",
6+
"js-beautify": "^1.15.4",
67
"npm-watch": "^0.13.0",
78
"riot": "^3.13.2",
89
"stylus": "^0.64.0",
910
"uglify-js": "^3.19.3"
1011
},
11-
"devDependencies": {},
1212
"watch": {
1313
"build-stylus": {
1414
"patterns": [

src/apps/api/serializers/leaderboards.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from django.db.models import Sum, Q, F
1+
from django.db.models import Prefetch, Sum, Q, F
22
from drf_writable_nested import WritableNestedModelSerializer
33
from rest_framework import serializers
44

55
from api.serializers.submission_leaderboard import SubmissionLeaderBoardSerializer
66

77
from competitions.models import Submission, Phase
8-
from leaderboards.models import Leaderboard, Column
8+
from leaderboards.models import Leaderboard, Column, SubmissionScore
99

1010
from .fields import CharacterSeparatedField
1111
from .tasks import PhaseTaskInstanceSerializer
@@ -98,38 +98,56 @@ class Meta:
9898
)
9999

100100
def get_submissions(self, instance):
101-
# desc == -colname
102-
# asc == colname
103101
primary_col = instance.columns.get(index=instance.primary_index)
104-
# Order first by primary column. Then order by other columns after for tie breakers.
102+
105103
ordering = [
106104
F('primary_col').desc(nulls_last=True)
107105
if primary_col.sorting == 'desc'
108106
else F('primary_col').asc(nulls_last=True)
109107
]
110-
submissions = (
108+
109+
submissions_qs = (
111110
Submission.objects.filter(
112111
leaderboard=instance,
113112
is_specific_task_re_run=False
114113
)
115-
.select_related('owner')
116-
.prefetch_related('scores')
117-
.annotate(primary_col=Sum('scores__score', filter=Q(scores__column=primary_col)))
114+
.select_related(
115+
'owner',
116+
'organization',
117+
'queue',
118+
'parent',
119+
'phase',
120+
'phase__competition',
121+
'phase__competition__queue',
122+
)
123+
.prefetch_related(
124+
Prefetch(
125+
'scores',
126+
queryset=SubmissionScore.objects.select_related(
127+
'column',
128+
'column__leaderboard',
129+
),
130+
)
131+
)
132+
.annotate(primary_col=Sum(
133+
'scores__score',
134+
filter=Q(scores__column=primary_col)
135+
))
118136
)
137+
119138
for column in instance.columns.exclude(id=primary_col.id).order_by('index'):
120139
col_name = f'col{column.index}'
121140
ordering.append(
122141
F(col_name).desc(nulls_last=True)
123142
if column.sorting == 'desc'
124143
else F(col_name).asc(nulls_last=True)
125144
)
126-
kwargs = {
145+
submissions_qs = submissions_qs.annotate(**{
127146
col_name: Sum('scores__score', filter=Q(scores__column__index=column.index))
128-
}
129-
submissions = submissions.annotate(**kwargs)
147+
})
130148

131-
submissions = submissions.order_by(*ordering, 'created_when')
132-
return SubmissionLeaderBoardSerializer(submissions, many=True).data
149+
submissions_qs = submissions_qs.order_by(*ordering, 'created_when')
150+
return SubmissionLeaderBoardSerializer(submissions_qs, many=True).data
133151

134152

135153
class LeaderboardPhaseSerializer(serializers.ModelSerializer):

src/apps/api/serializers/submission_leaderboard.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,43 @@ class SubmissionLeaderBoardSerializer(serializers.ModelSerializer):
2626
slug_url = serializers.CharField(source='owner.slug_url')
2727
organization = SimpleOrganizationSerializer(allow_null=True)
2828
created_when = serializers.DateTimeField()
29+
queue_id = serializers.SerializerMethodField()
30+
queue_name = serializers.SerializerMethodField()
31+
32+
def _get_effective_queue(self, obj):
33+
if obj.queue:
34+
return obj.queue
35+
36+
if obj.parent and obj.parent.queue:
37+
return obj.parent.queue
38+
39+
if obj.phase and obj.phase.competition and obj.phase.competition.queue:
40+
return obj.phase.competition.queue
41+
42+
return None
43+
44+
def _get_display_queue_name(self, obj):
45+
queue = self._get_effective_queue(obj)
46+
if not queue:
47+
return None
48+
49+
raw_name = queue.name or ""
50+
group_name = raw_name.rsplit("__", 1)[-1] # comp10__CLB -> CLB, APHP -> APHP
51+
submission_parent_id = obj.parent_id or obj.id
52+
53+
return f"{submission_parent_id}_{group_name}"
54+
55+
def get_queue_name(self, obj):
56+
return self._get_display_queue_name(obj)
57+
58+
def get_queue_id(self, obj):
59+
queue = self._get_effective_queue(obj)
60+
return queue.id if queue else None
2961

3062
class Meta:
3163
model = Submission
3264
fields = (
3365
'id', 'parent', 'owner', 'leaderboard_id', 'fact_sheet_answers',
3466
'task', 'scores', 'display_name', 'slug_url', 'organization',
35-
'detailed_result', 'created_when'
67+
'detailed_result', 'created_when', 'queue_name', 'queue_id',
3668
)

src/apps/api/views/competitions.py

Lines changed: 108 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import zipfile
22
import json
33
import csv
4-
from collections import OrderedDict
4+
from collections import Counter, OrderedDict
55
from io import StringIO
66
from django.http import HttpResponse
77
from tempfile import SpooledTemporaryFile
@@ -424,26 +424,43 @@ def collect_leaderboard_data(self, competition, phase_pk=None):
424424
phase_id = phases[0].id
425425

426426
leaderboard = Leaderboard.objects.prefetch_related('columns').get(phases=phase_id)
427-
leaderboard_titles = {phase['id']: f'{leaderboard.title} - {phase["name"]}({phase["id"]})' for phase in submission_query}
427+
leaderboard_titles = {
428+
phase['id']: f'{leaderboard.title} - {phase["name"]}({phase["id"]})'
429+
for phase in submission_query
430+
}
428431
leaderboard_data = {title: {} for title in leaderboard_titles.values()}
429432

430433
for phase in submission_query:
431434
generated_columns = OrderedDict()
432435
for task in phase['tasks']:
433436
for col in leaderboard.columns.all():
434-
generated_columns.update({f'{col.key}-{task["id"]}': f'{task["name"]}({task["id"]})-{col.title}'})
437+
generated_columns.update({
438+
f'{col.key}-{task["id"]}': f'{task["name"]}({task["id"]})-{col.title}'
439+
})
440+
435441
for submission in phase['submissions']:
436-
submission_key = f'{submission["owner"]}-{submission["parent"] or submission["id"]}'
437-
if submission_key not in leaderboard_data[leaderboard_titles[phase['id']]].keys():
438-
leaderboard_data[leaderboard_titles[phase['id']]].update({submission_key: OrderedDict()})
439-
if 'fact_sheet_answers' in submission.keys() and submission['fact_sheet_answers']:
440-
leaderboard_data[leaderboard_titles[phase['id']]][submission_key]\
441-
.update({'fact_sheet_answers': submission['fact_sheet_answers']})
442+
queue_name = submission.get('queue_name') or ''
443+
submission_key = f'{submission["owner"]}-{submission["id"]}'
444+
if queue_name:
445+
submission_key = f'{submission_key}-{queue_name}'
446+
447+
if submission_key not in leaderboard_data[leaderboard_titles[phase['id']]]:
448+
leaderboard_data[leaderboard_titles[phase['id']]][submission_key] = OrderedDict()
449+
450+
if submission.get('fact_sheet_answers'):
451+
leaderboard_data[leaderboard_titles[phase['id']]][submission_key].update({
452+
'fact_sheet_answers': submission['fact_sheet_answers']
453+
})
454+
442455
for col_title in generated_columns.values():
443456
leaderboard_data[leaderboard_titles[phase['id']]][submission_key].update({col_title: ""})
457+
444458
for score in submission['scores']:
445459
score_column = generated_columns[f'{score["column_key"]}-{submission["task"]}']
446-
leaderboard_data[leaderboard_titles[phase['id']]][submission_key].update({score_column: score['score']})
460+
leaderboard_data[leaderboard_titles[phase['id']]][submission_key].update({
461+
score_column: score['score']
462+
})
463+
447464
return leaderboard_data
448465

449466
@action(detail=True, methods=['GET'], renderer_classes=[JSONRenderer, CSVRenderer, ZipRenderer])
@@ -773,6 +790,23 @@ def rerun_submissions(self, request, pk):
773790
@action(detail=True, methods=['GET'], permission_classes=[AllowAny])
774791
def get_leaderboard(self, request, pk):
775792
phase = self.get_object()
793+
794+
def _clean_group_label(raw_name, submission_parent_id=None):
795+
if not raw_name:
796+
return None
797+
798+
label = str(raw_name)
799+
800+
if submission_parent_id is not None:
801+
prefix = f"{submission_parent_id}_"
802+
if label.startswith(prefix):
803+
label = label[len(prefix):]
804+
805+
if "__" in label:
806+
label = label.rsplit("__", 1)[1]
807+
808+
return label or None
809+
776810
if phase.competition.fact_sheet:
777811
fact_sheet_keys = [
778812
(
@@ -792,21 +826,73 @@ def get_leaderboard(self, request, pk):
792826
'submissions': [],
793827
'tasks': [],
794828
'fact_sheet_keys': fact_sheet_keys or None,
795-
'primary_index': query['leaderboard']['primary_index']
829+
'primary_index': query['leaderboard']['primary_index'],
830+
'has_group_queues': False,
796831
}
797832

798-
columns = [col for col in query['columns']]
833+
columns = list(query['columns'])
799834
submissions_keys = {}
800835
submission_detailed_results = {}
801836

837+
group_name_by_user_queue = {}
838+
for group in phase.competition.participant_groups.filter(
839+
queue__isnull=False
840+
).select_related('queue').prefetch_related('user_set'):
841+
cleaned_group_name = _clean_group_label(group.name)
842+
for user in group.user_set.all():
843+
group_name_by_user_queue[(user.username, group.queue_id)] = cleaned_group_name
844+
845+
parent_ids = {
846+
s['parent']
847+
for s in query['submissions']
848+
if s['parent'] is not None
849+
}
850+
parent_task_counts = Counter(
851+
(s['parent'], s['task'])
852+
for s in query['submissions']
853+
if s['parent'] is not None
854+
)
855+
802856
for submission in query['submissions']:
803-
submission_key = f"{submission['owner']}{submission['parent'] or submission['id']}"
857+
if submission['id'] in parent_ids:
858+
continue
859+
860+
submission_parent_id = submission.get('parent') or submission.get('id')
861+
raw_queue_name = submission.get('queue_name') or ''
862+
queue_id = submission.get('queue_id')
863+
864+
group_name = group_name_by_user_queue.get(
865+
(submission['owner'], queue_id)
866+
) if queue_id else None
867+
868+
group_label = _clean_group_label(
869+
group_name or raw_queue_name,
870+
submission_parent_id=submission_parent_id
871+
)
872+
873+
display_group = (
874+
f"{submission_parent_id}_{group_label}"
875+
if group_label
876+
else None
877+
)
878+
879+
parent_id = submission['parent']
880+
task_id = submission.get('task')
881+
882+
# Cas particulier: plusieurs submissions d'un même parent sans queue explicite
883+
is_multi_group_null_queue = (
884+
parent_id is not None
885+
and not queue_id
886+
and parent_task_counts.get((parent_id, task_id), 0) > 1
887+
)
888+
889+
if is_multi_group_null_queue:
890+
submission_key = f"{submission['owner']}{parent_id}_{submission['id']}"
891+
else:
892+
submission_key = f"{submission['owner']}{submission_parent_id}_{group_label or ''}"
893+
804894
# gather detailed result from submissions for each task
805-
# detailed_results are gathered based on submission key
806-
# `id` is used to fetch the right detailed result in detailed results page
807-
# `detailed_result` url is not needed
808895
submission_detailed_results.setdefault(submission_key, []).append({
809-
# 'detailed_result': submission['detailed_result'],
810896
'task': submission['task'],
811897
'id': submission['id']
812898
})
@@ -821,23 +907,18 @@ def get_leaderboard(self, request, pk):
821907
'fact_sheet_answers': submission['fact_sheet_answers'],
822908
'slug_url': submission['slug_url'],
823909
'organization': submission['organization'],
824-
'created_when': submission['created_when']
910+
'created_when': submission['created_when'],
911+
'queue_name': display_group,
825912
})
826913

827-
for score in submission['scores']:
914+
if queue_id or is_multi_group_null_queue:
915+
response['has_group_queues'] = True
828916

829-
# to check if a column is found
830-
# this is useful because of `hidden` field
831-
# if a column is hidden it will not be shown here so
832-
# we will not return that score to the front-end
917+
for score in submission['scores']:
833918
column_found = False
834-
# default precision is set to 2
835919
precision = 2
836-
# default hidden is set to false
837920
hidden = False
838921

839-
# loop over columns to find a column with the same index
840-
# replace default precision with column precision
841922
for col in columns:
842923
if col["index"] == score["index"]:
843924
precision = col["precision"]
@@ -847,13 +928,8 @@ def get_leaderboard(self, request, pk):
847928

848929
tempScore = score
849930
tempScore['task_id'] = submission['task']
850-
# round the score to 'precision' decimal points
851931
tempScore['score'] = str(round(float(tempScore["score"]), precision))
852932

853-
# only add scores to the scores list
854-
# if this column is found
855-
# and
856-
# column is not hidden
857933
if column_found and not hidden:
858934
response['submissions'][submissions_keys[submission_key]]['scores'].append(tempScore)
859935

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

879955
for task in query['tasks']:
880-
# This can be used to rendered variable columns on each task
881956
tempTask = {
882957
'name': task['name'],
883958
'id': task['id'],

0 commit comments

Comments
 (0)