Skip to content

Commit fab0649

Browse files
authored
Merge pull request #2245 from codalab/feature/pagination_for_submissions
Submission pagination backend fixed
2 parents b94529c + 669461d commit fab0649

File tree

3 files changed

+229
-16
lines changed

3 files changed

+229
-16
lines changed

src/apps/api/pagination.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,60 @@ def get_paginated_response(self, data):
2222
'page_size': self.page_size,
2323
'results': data
2424
})
25+
26+
27+
class DynamicChoicePagination(PageNumberPagination):
28+
"""
29+
Dynamic pagination :
30+
- default : 50 objects.
31+
- predetermined values : 50, 100, 500, all
32+
- if page_size=all => fetch all objects, capped by lax_page_size
33+
"""
34+
page_size = 50
35+
page_size_query_param = 'page_size'
36+
max_page_size = 1000
37+
_allowed_sizes = (50, 100, 500, 'all')
38+
39+
def get_page_size(self, request):
40+
raw = request.query_params.get(self.page_size_query_param)
41+
if raw is None:
42+
return self.page_size
43+
44+
raw_lower = str(raw).lower()
45+
if raw_lower == 'all':
46+
return self.max_page_size
47+
48+
try:
49+
val = int(raw)
50+
except (TypeError, ValueError):
51+
return self.page_size
52+
53+
if val in (50, 100, 500):
54+
return min(val, self.max_page_size)
55+
return self.page_size
56+
57+
def paginate_queryset(self, queryset, request, view=None):
58+
raw = request.query_params.get(self.page_size_query_param)
59+
self.requested_page_size = str(raw).lower() if raw is not None else str(self.page_size)
60+
61+
page_size = self.get_page_size(request)
62+
if isinstance(page_size, int) and page_size > 0:
63+
self.page_size = min(page_size, self.max_page_size)
64+
else:
65+
self.page_size = self.page_size
66+
67+
return super().paginate_queryset(queryset, request, view)
68+
69+
def get_paginated_response(self, data):
70+
page_size_value = getattr(self, 'requested_page_size', None)
71+
if page_size_value is None:
72+
page_size_value = self.page_size
73+
74+
return Response({
75+
'next': self.get_next_link(),
76+
'previous': self.get_previous_link(),
77+
'count': self.page.paginator.count,
78+
'page_size': page_size_value,
79+
'results': data,
80+
'allowed_page_sizes': [50, 100, 500, 'all'],
81+
})

src/apps/api/views/submissions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from django.core.files.base import ContentFile
1717

1818
from profiles.models import Organization, Membership
19+
from api.pagination import DynamicChoicePagination
1920
from tasks.models import Task
2021
from api.serializers.submissions import SubmissionCreationSerializer, SubmissionSerializer, SubmissionFilesSerializer, SubmissionDetailSerializer
2122
from competitions.models import Submission, SubmissionDetails, Phase, CompetitionParticipant
@@ -32,6 +33,7 @@ class SubmissionViewSet(ModelViewSet):
3233
filterset_fields = ('phase__competition', 'phase', 'status', 'is_soft_deleted')
3334
search_fields = ('data__data_file', 'description', 'name', 'owner__username')
3435
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [renderers.CSVRenderer]
36+
pagination_class = DynamicChoicePagination
3537

3638
def check_object_permissions(self, request, obj):
3739
if self.action in ['submission_leaderboard_connection']:

src/static/riot/competitions/detail/submission_manager.tag

Lines changed: 170 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,37 @@
187187
</tbody>
188188
</table>
189189

190+
<div class="ui pagination menu" style="display:flex; align-items:center; justify-content:space-between; margin-top: 12px;">
191+
<div style="display:flex; align-items:center; gap:8px;">
192+
<button class="ui button" onclick="{ go_to_page.bind(this, page - 1) }" disabled="{ page <= 1 }">
193+
<i class="icon chevron left"></i> Previous
194+
</button>
195+
196+
<div style="display:flex; align-items:center; gap:6px;">
197+
<span>Page</span>
198+
<input type="number" min="1" value="{ page }" onkeydown="{ handle_page_enter }" style="width:70px; text-align:center;" />
199+
<span> / { total_pages || 1 }</span>
200+
</div>
201+
202+
<button class="ui button" onclick="{ go_to_page.bind(this, page + 1) }" disabled="{ !next }">
203+
Next <i class="icon chevron right"></i>
204+
</button>
205+
</div>
206+
207+
<div style="display:flex; align-items:center; gap:8px;">
208+
<label>Per page</label>
209+
<select class="ui dropdown" value="{ page_size }" onchange="{ change_page_size.bind(this) }">
210+
<option value="50">50</option>
211+
<option value="100">100</option>
212+
<option value="500">500</option>
213+
<option value="all">1000</option>
214+
</select>
215+
216+
<div style="margin-right: 10px; color: #8c8c8c;">
217+
<small>{ total_count || 0 } total</small>
218+
</div>
219+
</div>
220+
190221
<div class="ui large modal" ref="modal">
191222
<div class="content">
192223
<div if="{!!selected_submission && !_.get(selected_submission, 'has_children', false)}">
@@ -203,7 +234,6 @@
203234

204235
<div if="{is_admin()}" data-tab="admin" class="parent-modal item">Admin</div>
205236

206-
<!-- Sometimes submissions end up in a bad state with no children.. -->
207237
<div class="item" if="{_.get(selected_submission, 'children').length === 0}">
208238
<i style="padding: 5px;">ERROR: Submission is a parent, but has no children. There was an error
209239
during creation.</i>
@@ -216,6 +246,8 @@
216246
show_visualization="{opts.competition.enable_detailed_results}"
217247
submission="{child}"></submission-modal>
218248
</div>
249+
250+
219251
<div class="ui tab" style="height: 565px; overflow: auto;" data-tab="admin" if="{is_admin()}">
220252
<submission-scores leaderboards="{leaderboards}"></submission-scores>
221253
</div>
@@ -234,6 +266,13 @@
234266
self.checked_submissions = []
235267
self.show_is_soft_deleted = false
236268

269+
self.page = 1
270+
self.page_size = 50
271+
self.total_count = 0
272+
self.total_pages = 1
273+
self.next = null
274+
self.previous = null
275+
237276
self.on("mount", function () {
238277
$(self.refs.search).dropdown()
239278
$(self.refs.status).dropdown()
@@ -269,37 +308,119 @@
269308
self.update()
270309
}
271310

311+
272312
self.update_submissions = function (filters) {
273313
self.loading = true
274314
self.update()
275-
if (opts.admin) {
276-
filters = filters || { phase__competition: opts.competition.id }
277-
filters.show_is_soft_deleted = self.show_is_soft_deleted
315+
316+
if (!filters) {
317+
if (opts.admin) {
318+
filters = { phase__competition: opts.competition.id }
319+
filters.show_is_soft_deleted = self.show_is_soft_deleted
320+
} else {
321+
filters = { phase: self.selected_phase ? self.selected_phase.id : undefined }
322+
}
323+
}
324+
325+
filters.page = self.page
326+
if (String(self.page_size).toLowerCase() === 'all') {
327+
filters.page_size = 'all'
278328
} else {
279-
filters = filters || { phase: self.selected_phase.id }
329+
filters.page_size = self.page_size
280330
}
281-
filters = filters || { phase: self.selected_phase.id }
331+
282332
CODALAB.api.get_submissions(filters)
283-
.done(function (submissions) {
284-
// TODO: should be able to do this with a serializer?
333+
.done(function (response) {
334+
let data = response
335+
let results = response
336+
if (response && typeof response === 'object' && response.hasOwnProperty('results')) {
337+
results = response.results || []
338+
self.next = response.next || null
339+
self.previous = response.previous || null
340+
self.total_count = response.count || 0
341+
342+
var effectivePageSize = 1
343+
var serverPageSize = undefined
344+
var totalCount = (typeof response.count === 'number') ? response.count : self.total_count
345+
var isLastPage = (response.next === null)
346+
347+
if (response && typeof response.page_size !== 'undefined') {
348+
serverPageSize = response.page_size
349+
}
350+
351+
if (typeof serverPageSize !== 'undefined') {
352+
if (String(serverPageSize).toLowerCase() === 'all') {
353+
self.page_size = 'all'
354+
} else {
355+
var parsedServerPS = Number(serverPageSize)
356+
if (!isNaN(parsedServerPS) && parsedServerPS > 0) {
357+
self.page_size = parsedServerPS
358+
}
359+
}
360+
}
361+
362+
if (String(self.page_size).toLowerCase() === 'all') {
363+
if (typeof response.page_size_numeric === 'number' && response.page_size_numeric > 0) {
364+
effectivePageSize = response.page_size_numeric
365+
} else if (typeof response.effective_page_size === 'number' && response.effective_page_size > 0) {
366+
effectivePageSize = response.effective_page_size
367+
} else {
368+
if (!isLastPage && Array.isArray(results) && results.length > 0) {
369+
effectivePageSize = results.length
370+
} else if (isLastPage && self.page > 1 && typeof totalCount === 'number' && totalCount > 0) {
371+
var itemsOnLastPage = Array.isArray(results) ? results.length : 0
372+
var pagesBefore = self.page - 1
373+
var calc = Math.floor((totalCount - itemsOnLastPage) / pagesBefore)
374+
if (calc > 0) {
375+
effectivePageSize = calc
376+
} else {
377+
effectivePageSize = 50
378+
}
379+
} else if (Array.isArray(results) && results.length > 0) {
380+
effectivePageSize = results.length
381+
} else {
382+
effectivePageSize = 50
383+
}
384+
}
385+
} else if (typeof self.page_size === 'number' && self.page_size > 0) {
386+
effectivePageSize = self.page_size
387+
} else {
388+
effectivePageSize = 50
389+
}
390+
391+
if (typeof totalCount !== 'number' || totalCount < 0) {
392+
self.total_pages = 1
393+
} else {
394+
self.total_pages = Math.max(1, Math.ceil(totalCount / effectivePageSize))
395+
}
396+
} else {
397+
results = response || []
398+
self.next = null
399+
self.previous = null
400+
self.total_count = results.length
401+
self.total_pages = Math.max(1, Math.ceil(self.total_count / self.page_size))
402+
}
403+
285404
if (opts.admin) {
286-
self.submissions = submissions.map((item) => {
405+
self.submissions = results.map((item) => {
287406
item.phase = opts.competition.phases.filter((phase) => {
288407
return phase.id === item.phase
289408
})[0]
290409
return item
291410
})
292411
} else {
293-
self.submissions = _.filter(submissions, sub => sub.owner === CODALAB.state.user.username)
412+
self.submissions = _.filter(results, sub => sub.owner === CODALAB.state.user.username)
294413
}
414+
295415
if (!opts.admin) {
296416
CODALAB.events.trigger('submissions_loaded', self.submissions)
297417
}
418+
298419
self.csv_link = CODALAB.api.get_submission_csv_URL(filters)
420+
299421
self.update()
300422
self.submission_checked()
301423

302-
// Timeout here so loader doesn't flicker
303424
_.delay(() => {
304425
self.loading = false
305426
self.update()
@@ -310,6 +431,43 @@
310431
})
311432
}
312433

434+
435+
self.go_to_page = function (p) {
436+
let newPage = parseInt(p, 10)
437+
if (isNaN(newPage) || newPage < 1) newPage = 1
438+
if (self.total_pages && newPage > self.total_pages) newPage = self.total_pages
439+
if (newPage === self.page) return
440+
self.page = newPage
441+
self.update_submissions()
442+
}
443+
444+
self.change_page_size = function (e) {
445+
const raw = (e && e.target && typeof e.target.value !== 'undefined') ? String(e.target.value).toLowerCase() : String(self.page_size).toLowerCase()
446+
447+
if (raw === 'all') {
448+
self.page_size = 'all'
449+
} else {
450+
const val = parseInt(raw, 10)
451+
if (isNaN(val) || val <= 0) return
452+
// n'autorise que 50,100,500
453+
if (![50, 100, 500].includes(val)) return
454+
self.page_size = val
455+
}
456+
457+
self.page = 1 // reset to first page when page size changes
458+
self.update_submissions()
459+
}
460+
461+
self.handle_page_enter = function (ev) {
462+
if (ev.key === 'Enter') {
463+
// value du champ input
464+
let v = ev.target.value
465+
let requested = parseInt(v, 10)
466+
if (isNaN(requested)) return
467+
self.go_to_page(requested)
468+
}
469+
}
470+
313471
self.add_to_leaderboard = function (submission) {
314472
CODALAB.api.add_submission_to_leaderboard(submission.id)
315473
.done(function (data) {
@@ -365,6 +523,7 @@
365523
filters['phase__competition'] = opts.competition.id
366524
}
367525
}
526+
self.page = 1
368527
self.update_submissions(filters)
369528
}, 100)
370529
}
@@ -524,16 +683,11 @@
524683
// Set checkboxes to be equal to Select_All checkbox
525684
check_boxes.prop('checked', check_boxes.first().is(':checked'))
526685

527-
528686
let inputs = $(self.refs.submission_table).find('input')
529687
let checked_boxes = inputs.not(':first').filter('input:checked')
530688
self.checked_submissions = checked_boxes.serializeArray().map((x) => { return x.name })
531689
}
532690

533-
534-
535-
536-
537691
self.submission_clicked = function (submission) {
538692
// stupid workaround to not modify the original submission object
539693
submission = _.defaultsDeep({}, submission)

0 commit comments

Comments
 (0)