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
61 changes: 61 additions & 0 deletions src/apps/api/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,64 @@ def get_paginated_response(self, data):
'page_size': self.page_size,
'results': data
})


class DynamicChoicePagination(PageNumberPagination):
"""
Pagination dynamique pour l'UI :
- défaut : 50
- valeurs autorisées côté client : 50, 100, 500, all
- si page_size=all => on renvoie tous les objets (dans la limite de max_page_size)
"""
page_size = 50
page_size_query_param = 'page_size'
max_page_size = 1000
_allowed_sizes = (50, 100, 500, 'all')

def get_page_size(self, request):
raw = request.query_params.get(self.page_size_query_param)
if raw is None:
return self.page_size

raw_lower = str(raw).lower()
if raw_lower == 'all':
return None

try:
val = int(raw)
except (TypeError, ValueError):
return self.page_size

if val in (50, 100, 500):
return val
return self.page_size

def paginate_queryset(self, queryset, request, view=None):
raw = request.query_params.get(self.page_size_query_param)
self.requested_page_size = raw if raw is not None else str(self.page_size)

if raw is not None and str(raw).lower() == 'all':
try:
total = queryset.count()
except Exception:
total = self.max_page_size
self.page_size = total if (isinstance(total, int) and total > 0) else 1
else:
page_size = self.get_page_size(request)
if page_size is None:
page_size = self.page_size
self.page_size = page_size

return super().paginate_queryset(queryset, request, view)

def get_paginated_response(self, data):
page_size_value = self.requested_page_size if getattr(self, 'requested_page_size', None) is not None else self.page_size

return Response({
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'count': self.page.paginator.count,
'page_size': page_size_value,
'results': data,
'allowed_page_sizes': [50, 100, 500, 'all'],
})
2 changes: 2 additions & 0 deletions src/apps/api/views/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from django.core.files.base import ContentFile

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

def check_object_permissions(self, request, obj):
if self.action in ['submission_leaderboard_connection']:
Expand Down
140 changes: 128 additions & 12 deletions src/static/riot/competitions/detail/submission_manager.tag
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,37 @@
</tbody>
</table>

<div class="ui pagination menu" style="display:flex; align-items:center; justify-content:space-between; margin-top: 12px;">
<div style="display:flex; align-items:center; gap:8px;">
<button class="ui button" onclick="{ go_to_page.bind(this, page - 1) }" disabled="{ page <= 1 }">
<i class="icon chevron left"></i> Previous
</button>

<div style="display:flex; align-items:center; gap:6px;">
<span>Page</span>
<input type="number" min="1" value="{ page }" onkeydown="{ handle_page_enter }" style="width:70px; text-align:center;" />
<span> / { total_pages || 1 }</span>
</div>

<button class="ui button" onclick="{ go_to_page.bind(this, page + 1) }" disabled="{ !next }">
Next <i class="icon chevron right"></i>
</button>
</div>

<div style="display:flex; align-items:center; gap:8px;">
<label>Per page</label>
<select class="ui dropdown" value="{ page_size }" onchange="{ change_page_size.bind(this) }">
<option value="50">50</option>
<option value="100">100</option>
<option value="500">500</option>
<option value="all">all</option>
</select>

<div style="margin-right: 10px; color: #8c8c8c;">
<small>{ total_count || 0 } total</small>
</div>
</div>

<div class="ui large modal" ref="modal">
<div class="content">
<div if="{!!selected_submission && !_.get(selected_submission, 'has_children', false)}">
Expand All @@ -203,7 +234,6 @@

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

<!-- Sometimes submissions end up in a bad state with no children.. -->
<div class="item" if="{_.get(selected_submission, 'children').length === 0}">
<i style="padding: 5px;">ERROR: Submission is a parent, but has no children. There was an error
during creation.</i>
Expand All @@ -216,6 +246,8 @@
show_visualization="{opts.competition.enable_detailed_results}"
submission="{child}"></submission-modal>
</div>


<div class="ui tab" style="height: 565px; overflow: auto;" data-tab="admin" if="{is_admin()}">
<submission-scores leaderboards="{leaderboards}"></submission-scores>
</div>
Expand All @@ -234,6 +266,13 @@
self.checked_submissions = []
self.show_is_soft_deleted = false

self.page = 1
self.page_size = 50
self.total_count = 0
self.total_pages = 1
self.next = null
self.previous = null

self.on("mount", function () {
$(self.refs.search).dropdown()
$(self.refs.status).dropdown()
Expand Down Expand Up @@ -269,37 +308,76 @@
self.update()
}


self.update_submissions = function (filters) {
self.loading = true
self.update()
if (opts.admin) {
filters = filters || { phase__competition: opts.competition.id }
filters.show_is_soft_deleted = self.show_is_soft_deleted
} else {
filters = filters || { phase: self.selected_phase.id }

if (!filters) {
if (opts.admin) {
filters = { phase__competition: opts.competition.id }
filters.show_is_soft_deleted = self.show_is_soft_deleted
} else {
filters = { phase: self.selected_phase ? self.selected_phase.id : undefined }
}
}
filters = filters || { phase: self.selected_phase.id }

filters.page = self.page
filters.page_size = self.page_size

CODALAB.api.get_submissions(filters)
.done(function (submissions) {
// TODO: should be able to do this with a serializer?
.done(function (response) {
let data = response
let results = response
if (response && typeof response === 'object' && response.hasOwnProperty('results')) {
results = response.results || []
self.next = response.next || null
self.previous = response.previous || null
self.total_count = response.count || 0
if (response && typeof response.page_size !== 'undefined') {
const rsp = String(response.page_size).toLowerCase()
if (rsp === 'all') {
self.page_size = 'all'
} else {
const n = Number(response.page_size)
self.page_size = isNaN(n) ? self.page_size : n
}
}

if (String(self.page_size).toLowerCase() === 'all') {
self.total_pages = 1
} else {
const ps = Number(self.page_size) || 1
self.total_pages = Math.max(1, Math.ceil(self.total_count / ps))
}
} else {
results = response || []
self.next = null
self.previous = null
self.total_count = results.length
self.total_pages = Math.max(1, Math.ceil(self.total_count / self.page_size))
}

if (opts.admin) {
self.submissions = submissions.map((item) => {
self.submissions = results.map((item) => {
item.phase = opts.competition.phases.filter((phase) => {
return phase.id === item.phase
})[0]
return item
})
} else {
self.submissions = _.filter(submissions, sub => sub.owner === CODALAB.state.user.username)
self.submissions = _.filter(results, sub => sub.owner === CODALAB.state.user.username)
}

if (!opts.admin) {
CODALAB.events.trigger('submissions_loaded', self.submissions)
}

self.csv_link = CODALAB.api.get_submission_csv_URL(filters)

self.update()
self.submission_checked()

// Timeout here so loader doesn't flicker
_.delay(() => {
self.loading = false
self.update()
Expand All @@ -310,6 +388,43 @@
})
}


self.go_to_page = function (p) {
let newPage = parseInt(p, 10)
if (isNaN(newPage) || newPage < 1) newPage = 1
if (self.total_pages && newPage > self.total_pages) newPage = self.total_pages
if (newPage === self.page) return
self.page = newPage
self.update_submissions()
}

self.change_page_size = function (e) {
const raw = (e && e.target && typeof e.target.value !== 'undefined') ? String(e.target.value).toLowerCase() : String(self.page_size).toLowerCase()

if (raw === 'all') {
self.page_size = 'all'
} else {
const val = parseInt(raw, 10)
if (isNaN(val) || val <= 0) return
// n'autorise que 50,100,500
if (![50, 100, 500].includes(val)) return
self.page_size = val
}

self.page = 1 // reset to first page when page size changes
self.update_submissions()
}

self.handle_page_enter = function (ev) {
if (ev.key === 'Enter') {
// value du champ input
let v = ev.target.value
let requested = parseInt(v, 10)
if (isNaN(requested)) return
self.go_to_page(requested)
}
}

self.add_to_leaderboard = function (submission) {
CODALAB.api.add_submission_to_leaderboard(submission.id)
.done(function (data) {
Expand Down Expand Up @@ -365,6 +480,7 @@
filters['phase__competition'] = opts.competition.id
}
}
self.page = 1
self.update_submissions(filters)
}, 100)
}
Expand Down