Skip to content

Feature PR: Thêm tính năng sắp xếp lại contest (có filter) cho phép admin reorder lại contest quan trọng#84

Merged
ngtlowk merged 17 commits into
CHT-OJ:masterfrom
hoangthaiminh:final-project
Apr 19, 2026
Merged

Feature PR: Thêm tính năng sắp xếp lại contest (có filter) cho phép admin reorder lại contest quan trọng#84
ngtlowk merged 17 commits into
CHT-OJ:masterfrom
hoangthaiminh:final-project

Conversation

@hoangthaiminh

Copy link
Copy Markdown
Contributor

Mô tả

  • Đây là feature PR theo yêu cầu: Thêm tính năng cho phép admin sắp xếp lại các kỳ thi quan trọng, bao gồm các kiểu lọc sắp xếp như lọc theo Năm tổ chứcTag kỳ thi
  • Truy cập bằng link có đuôi /contests/reorder hoặc click trực tiếp vào tab Sắp xếp lại các cuộc thi trong mục Các kỳ thi
  • Tuỳ chọn: Sử dụng tham số "?tag=<tag_ky_thi>" để reorder các kỳ thi có trong mục "tag_ky_thi"

Tệp đã thay đổi

Updated

  • viewsurls: Thêm path "/contests/reorder" để truy cập trang re-order, và class "ContestReorder" để sắp xếp lại các contest bên phía client
  • reorder.htmlcontest.scss: Front-end sắp xếp contest cho admin
  • django.po: Thêm dịch thuật cho reorder template

Added

  • Sortable.min.js: GUI cho phép kéo-thả để reorder

Lưu ý

PR này bao gồm những contributor là ThisIsNotNam từ ThisIsNotNam/oj hoangthaiminh từ hoangthaiminh/chtOJ

@hoangthaiminh hoangthaiminh changed the title Feature PR: Thêm tính năng sắp xếp lại contest (có filter theo năm và tag) cho phép admin reorder lại contest quan trọng Feature PR: Thêm tính năng sắp xếp lại contest (có filter) cho phép admin reorder lại contest quan trọng Apr 10, 2026
@ngtlowk ngtlowk requested a review from Copilot April 18, 2026 01:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

PR bổ sung trang admin để kéo-thả sắp xếp lại (reorder) các contest theo bộ lọc (năm tổ chức / tag), đồng thời đưa khái niệm sort_order vào model Contest để điều khiển thứ tự hiển thị.

Changes:

  • Thêm trang /contests/reorder/ (UI kéo-thả + lưu thứ tự qua POST JSON) và tab “Reorder contests” cho staff có quyền.
  • Thêm trường Contest.sort_order và thay đổi logic sắp xếp contest list để ưu tiên sort_order.
  • Bổ sung asset SortableJS, CSS cho form filter, và chuỗi dịch cho template reorder.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
judge/views/contests.py Thêm view ContestReorder; thay đổi thứ tự sắp xếp contest list/tag list để dùng sort_order.
judge/models/contest.py Thêm sort_order và đặt default ordering theo sort_order.
dmoj/urls.py Đăng ký route /contests/reorder/.
templates/contest/reorder.html Template UI reorder (filter + table kéo-thả + lưu).
templates/contest/contest-list-tabs.html Thêm tab “Reorder contests” (kèm query tag).
resources/contest.scss CSS cho form filter.
resources/Sortable.min.js Thêm SortableJS phục vụ kéo-thả.
locale/vi/LC_MESSAGES/django.po Thêm translation cho reorder template (và một số entry khác).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread judge/views/contests.py
def get_queryset(self):
self.search_query = None
query_set = self._get_queryset().order_by(self.order, 'key').filter(end_time__lt=self._now)
query_set = self._get_queryset().order_by('sort_order', self.order, 'key').filter(end_time__lt=self._now)

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

order_by('sort_order', self.order, 'key') makes sort_order the primary sort key, so user-selected sorting (order query param: name/user_count/start_time) will not behave as expected (it becomes only a tie-breaker). If the intent is to preserve existing sortable columns, keep self.order first and use sort_order only as a secondary key (or only apply sort_order when using the default ordering).

Suggested change
query_set = self._get_queryset().order_by('sort_order', self.order, 'key').filter(end_time__lt=self._now)
query_set = self._get_queryset().order_by(self.order, 'sort_order', 'key').filter(end_time__lt=self._now)

Copilot uses AI. Check for mistakes.
Comment thread judge/views/contests.py
Comment on lines +156 to +158
# active.sort(key=attrgetter('end_time', 'key'))
# present.sort(key=attrgetter('end_time', 'key'))
# future.sort(key=attrgetter('start_time'))

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old sorting logic is left commented out. Since git history already preserves previous implementations, keeping these commented lines adds noise and makes future maintenance harder; please remove the commented-out sort calls.

Suggested change
# active.sort(key=attrgetter('end_time', 'key'))
# present.sort(key=attrgetter('end_time', 'key'))
# future.sort(key=attrgetter('start_time'))

Copilot uses AI. Check for mistakes.
Comment thread judge/views/contests.py
Comment on lines +232 to +234
# active.sort(key=attrgetter('end_time', 'key'))
# present.sort(key=attrgetter('end_time', 'key'))
# future.sort(key=attrgetter('start_time'))

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old sorting logic is left commented out here as well. Please remove these commented-out lines to avoid accumulating dead code in the view.

Suggested change
# active.sort(key=attrgetter('end_time', 'key'))
# present.sort(key=attrgetter('end_time', 'key'))
# future.sort(key=attrgetter('start_time'))

Copilot uses AI. Check for mistakes.
Comment thread judge/views/contests.py
Comment on lines +1767 to +1769
tag_key = request.GET.get('tag')

if year:

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

year is taken directly from the query string and passed to start_time__year=year. If a non-numeric value is provided (e.g. year=abc), some DB backends can error, causing a 500. Please validate/coerce year to an int (and ignore/400 on failure) before applying the filter.

Suggested change
tag_key = request.GET.get('tag')
if year:
if year:
try:
year = int(year)
except (TypeError, ValueError):
year = None
tag_key = request.GET.get('tag')
if year is not None:

Copilot uses AI. Check for mistakes.
Comment thread judge/views/contests.py
})

def post(self, request):
data = json.loads(request.body)

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.loads(request.body) can raise JSONDecodeError (or UnicodeDecodeError) on malformed requests, returning a 500. Please handle invalid JSON and return a 400 with a small error payload.

Suggested change
data = json.loads(request.body)
try:
data = json.loads(request.body)
except (json.JSONDecodeError, UnicodeDecodeError):
return JsonResponse({'error': 'invalid_json'}, status=400)

Copilot uses AI. Check for mistakes.
Comment thread resources/contest.scss
}

@media (max-width: 640px) {
form.contest-filter-row {

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The responsive selector in the media query appears wrong: form.contest-filter-row targets a <form> with class contest-filter-row, but the markup uses a <div class="contest-filter-row"> inside <form class="contest-filter-form">. As written, the mobile grid override likely never applies; adjust the selector to match the actual .contest-filter-row element.

Suggested change
form.contest-filter-row {
form.contest-filter-form .contest-filter-row {

Copilot uses AI. Check for mistakes.
Comment thread judge/views/contests.py


@method_decorator(staff_member_required, name='dispatch')
class ContestReorder(View):

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

staff_member_required allows any is_staff user to access this endpoint, but the UI tab is shown only when the user has judge.edit_all_contest or judge.edit_own_contest. This creates a permission bypass where staff without contest edit permissions can still reorder contests (and see potentially private contests). Consider enforcing the same permissions in dispatch (e.g., PermissionRequiredMixin or explicit has_perm checks) instead of (or in addition to) staff_member_required.

Suggested change
class ContestReorder(View):
class ContestReorder(PermissionRequiredMixin, View):
permission_required = ('judge.edit_all_contest', 'judge.edit_own_contest')
raise_exception = True
def has_permission(self):
return (
self.request.user.has_perm('judge.edit_all_contest') or
self.request.user.has_perm('judge.edit_own_contest')
)

Copilot uses AI. Check for mistakes.
Comment thread judge/models/contest.py
(SCOREBOARD_AFTER_CONTEST, _('Hidden for duration of contest')),
(SCOREBOARD_AFTER_PARTICIPATION, _('Hidden for duration of participation')),
)
sort_order = models.IntegerField(verbose_name=_('sort order'), default=0, db_index=True)

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With sort_order defaulting to 0, all existing contests will initially share the same value. That makes the new ordering unstable (falls back to key order) and can make the reorder operation ineffective unless sort_order is backfilled/uniqued. Please add a backfill step (migration/data script/management command) to initialize sort_order for existing rows to preserve the current contest ordering semantics.

Copilot uses AI. Check for mistakes.
Comment on lines +7660 to +7674
#: judge/models/profile.py:170
msgid "unpublic logo"
msgstr "Không công khai logo"

#: judge/models/profile.py:171
msgid "If enabled, only selected users or users within the organization selected below will be able to use the logo. Leave the fields below blank so no one can see this logo"
msgstr "Nếu bật, chỉ những người dùng được chọn hoặc người dùng trong tổ chức được chọn bên dưới mới có thể sử dụng logo. Để trống những trường bên dưới để ẩn đi logo này"

#: judge/models/profile.py:175
msgid "users are permitted"
msgstr "Người dùng được phép"

#: judge/models/profile.py:176
msgid "If private, only these users may choose the logo"
msgstr "Nếu cho private, chỉ những người dùng trên mới được chọn logo"

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diff adds several new Vietnamese translations (e.g. "unpublic logo", "users are permitted", "You are not allowed to use this logo") that don't appear to exist anywhere in the current codebase (no references found). This looks unrelated to the contest reorder feature and may be accidental translation churn; please remove these unrelated entries or include the corresponding code changes that introduce the msgids.

Copilot uses AI. Check for mistakes.
Comment thread dmoj/urls.py
path('contests.ics', contests.ContestICal.as_view(), name='contest_ical'),
path('contests/<int:year>/<int:month>/', contests.ContestCalendar.as_view(), name='contest_calendar'),
path('contests/new', contests.CreateContest.as_view(), name='contest_new'),
path('contests/reorder/', contests.ContestReorder.as_view(), name='contest_reorder'),

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions accessing the feature via /contests/reorder (no trailing slash), but the new route is registered as contests/reorder/. If APPEND_SLASH is disabled in some environments, the documented URL will 404; consider aligning the path/docs (or adding a redirect/alternative path).

Copilot uses AI. Check for mistakes.
@ngtlowk ngtlowk merged commit ee6ce4e into CHT-OJ:master Apr 19, 2026
6 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants