From de7ee2190787b01a0661d2daa92a5dc2eb7124f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Ho=C3=A0ng=20Th=C3=A1i?= Date: Thu, 9 Apr 2026 18:09:22 +0700 Subject: [PATCH 1/6] Test ordering feature --- dmoj/urls.py | 1 + judge/models/contest.py | 2 ++ judge/views/contests.py | 33 +++++++++++++++++++++--- templates/contest/reorder.html | 46 ++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 templates/contest/reorder.html diff --git a/dmoj/urls.py b/dmoj/urls.py index 7d16871fc..e1f9ddfe3 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -229,6 +229,7 @@ def paged_list_view(view, name): path('contests.ics', contests.ContestICal.as_view(), name='contest_ical'), path('contests///', 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'), re_path(r'^contests/tag/(?P[a-z-]+)', include([ path('', paged_list_view(contests.ContestTagList, 'contest_tag_list')), ])), diff --git a/judge/models/contest.py b/judge/models/contest.py index 4ecfeed4d..529e94eab 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -77,6 +77,7 @@ class Contest(models.Model): (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) key = models.CharField(max_length=32, verbose_name=_('contest id'), unique=True, validators=[RegexValidator('^[a-z0-9_]+$', _('Contest id must be ^[a-z0-9_]+$'))]) name = models.CharField(max_length=100, verbose_name=_('contest name'), db_index=True) @@ -553,6 +554,7 @@ class Meta: ) verbose_name = _('contest') verbose_name_plural = _('contests') + ordering = ('sort_order', 'key') class ContestAnnouncement(models.Model): diff --git a/judge/views/contests.py b/judge/views/contests.py index f8a8707be..15d4f860a 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -54,6 +54,8 @@ from judge.utils.stats import get_bar_chart, get_pie_chart, get_stacked_bar_chart from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, SingleObjectFormView, TitleMixin, \ add_file_response, generic_message +from django.contrib.admin.views.decorators import staff_member_required +from django.utils.decorators import method_decorator __all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar', 'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', @@ -114,7 +116,7 @@ def _get_queryset(self): 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) if 'search' in self.request.GET: self.search_query = search_query = ' '.join(self.request.GET.getlist('search')).strip() if search_query: @@ -147,9 +149,13 @@ def get_context_data(self, **kwargs): active.append(participation) present.remove(participation.contest) - active.sort(key=attrgetter('end_time', 'key')) - present.sort(key=attrgetter('end_time', 'key')) - future.sort(key=attrgetter('start_time')) + active.sort(key=lambda p: (p.contest.sort_order, p.contest.key)) + present.sort(key=lambda c: (c.sort_order, c.key)) + future.sort(key=lambda c: (c.sort_order, c.key)) + + # active.sort(key=attrgetter('end_time', 'key')) + # present.sort(key=attrgetter('end_time', 'key')) + # future.sort(key=attrgetter('start_time')) context['active_participations'] = active context['current_contests'] = present context['future_contests'] = future @@ -1744,3 +1750,22 @@ def get(self, request, *args, **kwargs): response['Content-Type'] = 'application/zip' response['Content-Disposition'] = 'attachment; filename=%s-data.zip' % self.object.key return response + + +@method_decorator(staff_member_required, name='dispatch') +class ContestReorder(View): + def get(self, request): + now = timezone.now() + contests = Contest.objects.order_by('sort_order', 'key') + return render(request, 'contest/reorder.html', { + 'title': _('Reorder Contests'), + 'future_contests': contests.filter(start_time__gt=now), + 'current_contests': contests.filter(start_time__lte=now, end_time__gte=now), + 'past_contests': contests.filter(end_time__lt=now), + }) + + def post(self, request): + order = json.loads(request.body).get('order', []) + for idx, contest_key in enumerate(order): + Contest.objects.filter(key=contest_key).update(sort_order=idx) + return JsonResponse({'status': 'ok'}) diff --git a/templates/contest/reorder.html b/templates/contest/reorder.html new file mode 100644 index 000000000..9104e0103 --- /dev/null +++ b/templates/contest/reorder.html @@ -0,0 +1,46 @@ +{% extends "common-content.html" %} + +{% macro sortable_section(title, contests, section_id) %} +

{{ title }}

+
    + {% for contest in contests %} +
  • + ⠿ {{ contest.name }} {{ contest.start_time }} +
  • + {% endfor %} +
+{% endmacro %} + +{% block body %} +{{ sortable_section('Ongoing', current_contests, 'present-sortable') }} +{{ sortable_section('Upcoming', future_contests, 'future-sortable') }} +{{ sortable_section('Past', past_contests, 'past-sortable') }} + + + + + + +{% endblock %} \ No newline at end of file From f0fe09764ffe6c1bbabe86e18b947c3d28d3c207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Ho=C3=A0ng=20Th=C3=A1i?= Date: Thu, 9 Apr 2026 21:04:23 +0700 Subject: [PATCH 2/6] Fetch 2 commits from 'ThisIsNotNam/oj' --- judge/views/contests.py | 42 ++++-- resources/Sortable.min.js | 2 + templates/contest/contest-list-tabs.html | 1 + templates/contest/reorder.html | 168 ++++++++++++++++++----- 4 files changed, 169 insertions(+), 44 deletions(-) create mode 100644 resources/Sortable.min.js diff --git a/judge/views/contests.py b/judge/views/contests.py index 15d4f860a..bf576696f 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -225,9 +225,13 @@ def get_context_data(self, **kwargs): active.append(participation) present.remove(participation.contest) - active.sort(key=attrgetter('end_time', 'key')) - present.sort(key=attrgetter('end_time', 'key')) - future.sort(key=attrgetter('start_time')) + active.sort(key=lambda p: (p.contest.sort_order, p.contest.key)) + present.sort(key=lambda c: (c.sort_order, c.key)) + future.sort(key=lambda c: (c.sort_order, c.key)) + + # active.sort(key=attrgetter('end_time', 'key')) + # present.sort(key=attrgetter('end_time', 'key')) + # future.sort(key=attrgetter('start_time')) context['title'] = _('Các kỳ thi thuộc mục "%s"') % self.tag.display_name context['active_participations'] = active context['current_contests'] = present @@ -1756,16 +1760,38 @@ def get(self, request, *args, **kwargs): class ContestReorder(View): def get(self, request): now = timezone.now() - contests = Contest.objects.order_by('sort_order', 'key') + contests = Contest.objects.prefetch_related('tags', 'organizations').order_by('sort_order', 'key') + + year = request.GET.get('year') + tag_key = request.GET.get('tag') + + if year: + contests = contests.filter(start_time__year=year) + if tag_key: + contests = contests.filter(tags__key=tag_key) + + all_years = Contest.objects.dates('start_time', 'year', order='DESC') + all_tags = ContestTag.objects.all().order_by('key') return render(request, 'contest/reorder.html', { - 'title': _('Reorder Contests'), + 'title': _('Reorder contests'), 'future_contests': contests.filter(start_time__gt=now), 'current_contests': contests.filter(start_time__lte=now, end_time__gte=now), 'past_contests': contests.filter(end_time__lt=now), + 'all_years': [d.year for d in all_years], + 'all_tags': all_tags, + 'current_year': year, + 'current_tag': tag_key, }) def post(self, request): - order = json.loads(request.body).get('order', []) - for idx, contest_key in enumerate(order): - Contest.objects.filter(key=contest_key).update(sort_order=idx) + data = json.loads(request.body) + for section in ('future', 'present', 'past'): + keys = data.get(section, []) + if not keys: + continue + current_orders = sorted( + Contest.objects.filter(key__in=keys).values_list('sort_order', flat=True) + ) + for key, sort_val in zip(keys, current_orders): + Contest.objects.filter(key=key).update(sort_order=sort_val) return JsonResponse({'status': 'ok'}) diff --git a/resources/Sortable.min.js b/resources/Sortable.min.js new file mode 100644 index 000000000..4d6c09744 --- /dev/null +++ b/resources/Sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.15.0 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function M(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function N(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&p(t,e)||o&&t===n)return t}while(t!==n&&(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode))}var i;return null}var g,m=/\s+/g;function I(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(m," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(m," ")))}function P(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=P(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function b(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[j]._onDragOver(o)}}var i,r,a}function Yt(t){q&&q.parentNode[j]._isOutsideThisEl(t.target)}function Bt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[j]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return It(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Bt.supportPointer&&"PointerEvent"in window&&!u,emptyInsertThreshold:5};for(n in K.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Pt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Mt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),Et.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,x())}function Ft(t,e,n,o,i,r,a,l){var s,c,u=t[j],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||k(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function jt(t){t.draggable=!1}function Ht(){Ct=!1}function Lt(t){return setTimeout(t,0)}function Kt(t){return clearTimeout(t)}Bt.prototype={constructor:Bt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(gt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,q):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Tt.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Tt.push(o)}}(o),!q&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=N(l,t.draggable,o,!1))&&l.animated||J===l)){if(nt=B(l),it=B(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return U({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),z("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=N(s,t.trim(),o,!1))return U({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),z("filter",n,{evt:e}),!0}))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!N(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!q&&n.parentNode===r&&(o=k(n),$=r,V=(q=n).parentNode,Q=q.nextSibling,J=n,at=a.group,st={target:Bt.dragged=q,clientX:(e||t).clientX,clientY:(e||t).clientY},ht=st.clientX-o.left,ft=st.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,q.style["will-change"]="all",o=function(){z("delayEnded",i,{evt:t}),Bt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(q.draggable=!0),i._triggerDragStart(t,e),U({sortable:i,name:"choose",originalEvent:t}),I(q,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){b(q,t.trim(),jt)}),h(l,"dragover",Xt),h(l,"mousemove",Xt),h(l,"touchmove",Xt),h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,q.draggable=!0),z("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():Bt.eventCanceled?this._onDrop():(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){q&&jt(q),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;f(t,"mouseup",this._disableDelayedDrag),f(t,"touchend",this._disableDelayedDrag),f(t,"touchcancel",this._disableDelayedDrag),f(t,"mousemove",this._delayedDragTouchMoveHandler),f(t,"touchmove",this._delayedDragTouchMoveHandler),f(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(q,"dragend",this),h($,"dragstart",this._onDragStart));try{document.selection?Lt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;yt=!1,$&&q?(z("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Yt),n=this.options,t||I(q,n.dragClass,!1),I(q,n.ghostClass,!0),Bt.active=this,t&&this._appendGhost(),U({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(ct){this._lastX=ct.clientX,this._lastY=ct.clientY,kt();for(var t=document.elementFromPoint(ct.clientX,ct.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(ct.clientX,ct.clientY))!==e;)e=t;if(q.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j])if(e[j]._onDragOver({clientX:ct.clientX,clientY:ct.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=(t=e).parentNode);Rt()}},_onTouchMove:function(t){if(st){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Z&&v(Z,!0),a=Z&&r&&r.a,l=Z&&r&&r.d,e=Ot&&bt&&E(bt),a=(i.clientX-st.clientX+o.x)/(a||1)+(e?e[0]-_t[0]:0)/(a||1),l=(i.clientY-st.clientY+o.y)/(l||1)+(e?e[1]-_t[1]:0)/(l||1);if(!Bt.active&&!yt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))n.right+10||t.clientX<=n.right&&t.clientY>n.bottom&&t.clientX>=n.left:t.clientX>n.right&&t.clientY>n.top||t.clientX<=n.right&&t.clientY>n.bottom+10}(n,r,this)&&!g.animated){if(g===q)return O(!1);if((l=g&&a===n.target?g:l)&&(w=k(l)),!1!==Ft($,a,q,o,l,w,n,!!l))return x(),g&&g.nextSibling?a.insertBefore(q,g.nextSibling):a.appendChild(q),V=a,A(),O(!0)}else if(g&&function(t,e,n){n=k(X(n.el,0,n.options,!0));return e?t.clientX{{ title }} -
    - {% for contest in contests %} -
  • - ⠿ {{ contest.name }} {{ contest.start_time }} -
  • - {% endfor %} -
-{% endmacro %} +{% block title_ruler %}{% endblock %} + +{% block title_row %} +

{{ _('Reorder contests') }}

+{% endblock %} + +{% block content_js_media %} + +{% endblock %} {% block body %} -{{ sortable_section('Ongoing', current_contests, 'present-sortable') }} -{{ sortable_section('Upcoming', future_contests, 'future-sortable') }} -{{ sortable_section('Past', past_contests, 'past-sortable') }} +
+
+ + + {% if current_year or current_tag %} + {{ _('Clear filters') }} + {% endif %} +
+ + {% macro sortable_section(title, contests, section_id) %} +

{{ title }}

+ + + + + + + + + + {% for contest in contests %} + + + + + + {% else %} + + {% endfor %} + +
{{ _('Contest') }}{{ _('Users') }}
+
+ + {{ contest.name }} + + + {% if not contest.is_visible %} + + {{ _('hidden') }} + + {% endif %} + {% if contest.is_organization_private %} + {% for org in contest.organizations.all() %} + + + {{ org.name }} + + + {% endfor %} + {% elif contest.is_private %} + + {{ _('private') }} + + {% endif %} + {% if contest.is_rated %} + + {{ _('rated') }} + + {% endif %} + {% for tag in contest.tags.all() %} + + + {{ tag.display_name }} + + + {% endfor %} + +
+
+ {{ contest.start_time|date(_("M j, Y, G:i")) }} + {% if contest.time_limit %} + - {{ contest.end_time|date(_("M j, Y, G:i")) }} + {% endif %} +
+
+
{{ contest.user_count }}
{{ _('No contests.') }}
+
+ {% endmacro %} - - + {{ sortable_section(_('Ongoing contests'), current_contests, 'present') }} + {{ sortable_section(_('Upcoming contests'), future_contests, 'future') }} + {{ sortable_section(_('Past contests'), past_contests, 'past') }} + + + + +
- {% endblock %} \ No newline at end of file From 89e3498887108afbfbfa688fcef2de488b05b1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Ho=C3=A0ng=20Th=C3=A1i?= Date: Thu, 9 Apr 2026 22:09:31 +0700 Subject: [PATCH 3/6] Fix frontend --- locale/vi/LC_MESSAGES/django.po | 16 +++++++ resources/contest.scss | 76 +++++++++++++++++++++++++++++++++ templates/contest/reorder.html | 25 +++++++---- 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 21661f8f9..5ead7693c 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -7686,3 +7686,19 @@ msgstr "" #: judge/admin/profile.py:62 msgid "This user is not allowed to use this logo" msgstr "Người dùng này không có quyền sử dụng logo này!" + +#: judge/view/contest.py:1776 +msgid "Reorder contests" +msgstr "Sắp xếp lại các cuộc thi" + +#: templates/contest/reorder.html +msgid "Contest year" +msgstr "Năm tổ chức" + +#: templates/contest/reorder.html +msgid "Contest tag" +msgstr "Thẻ kỳ thi" + +#: templates/contest/reorder.html +msgid "Clear filters" +msgstr "Xoá bộ lọc" diff --git a/resources/contest.scss b/resources/contest.scss index b2ec0655e..4f3164241 100644 --- a/resources/contest.scss +++ b/resources/contest.scss @@ -298,3 +298,79 @@ form.contest-join-pseudotab { width: 260px !important; } } + +form.contest-filter-form { + display: grid; + row-gap: 7px; + width: max-content; + margin: 8px 0px 16px 0px; + background: transparent; +} + +form.contest-filter-form .contest-filter-row { + display: grid; + grid-template-columns: 105px auto; + align-items: center; + column-gap: 12px; +} + +form.contest-filter-form .contest-filter-row label { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 500; + line-height: 1; + white-space: nowrap; +} + +form.contest-filter-form .contest-filter-row label i { + font-size: 14px; +} + +form.contest-filter-form .contest-filter-row select { + appearance: auto; + -webkit-appearance: auto; + -moz-appearance: auto; + + min-width: 92px; + padding: 4px 8px; + border: 1px solid #b8b8b8; + border-radius: 4px; + background: #fff; + color: #111; + font-size: 14px; + outline: none; + cursor: pointer; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +form.contest-filter-form .contest-filter-row select:hover { + border-color: #7a7a7a; +} + +form.contest-filter-form .contest-filter-row select:focus { + border-color: #1a73e8; + box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.12); +} + +form.contest-filter-form .contest-filter-row .button { + justify-self: start; +} + +@media (max-width: 640px) { + form.contest-filter-row { + grid-template-columns: 1fr; + row-gap: 4px; + } + + form.contest-filter-form .contest-filter-row label { + font-size: 13px; + } + + form.contest-filter-form .contest-filter-row select { + min-width: 84px; + font-size: 13px; + padding: 4px 7px; + } +} diff --git a/templates/contest/reorder.html b/templates/contest/reorder.html index 0d5612810..ec02d4843 100644 --- a/templates/contest/reorder.html +++ b/templates/contest/reorder.html @@ -12,25 +12,34 @@

{{ _('Reorder contests') }}

{% block body %}
-
- - +
+ {% if current_year or current_tag %} - {{ _('Clear filters') }} + + {{ _('Clear filters') }} {% endif %} From 092bf9ca881ee447a53637dbad4c913e92e365cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Ho=C3=A0ng=20Th=C3=A1i?= Date: Fri, 10 Apr 2026 00:25:12 +0700 Subject: [PATCH 4/6] Fix drag --- locale/vi/LC_MESSAGES/django.po | 16 ++ templates/contest/reorder.html | 313 +++++++++++++++++++++++--------- 2 files changed, 248 insertions(+), 81 deletions(-) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 5ead7693c..f27035546 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -7702,3 +7702,19 @@ msgstr "Thẻ kỳ thi" #: templates/contest/reorder.html msgid "Clear filters" msgstr "Xoá bộ lọc" + +#: templates/contest/reorder.html +msgid "Save order" +msgstr "Lưu thứ tự này" + +#: templates/contest/reorder.html +msgid "No contests" +msgstr "Không có kỳ thi" + +#: templates/contest/reorder.html +msgid "Order saved" +msgstr "Đã cập nhật thứ tự này" + +#: templates/contest/reorder.html +msgid "Error while saving order" +msgstr "Lỗi khi cập nhật thứ tự mới" diff --git a/templates/contest/reorder.html b/templates/contest/reorder.html index ec02d4843..57f82f1e6 100644 --- a/templates/contest/reorder.html +++ b/templates/contest/reorder.html @@ -11,6 +11,77 @@

{{ _('Reorder contests') }}

{% endblock %} {% block body %} + +
@@ -45,96 +116,171 @@

{{ _('Reorder contests') }}

{% macro sortable_section(title, contests, section_id) %}

{{ title }}

- - - - - - - - - - {% for contest in contests %} - - - + + + {% else %} + + {% endfor %} + +
{{ _('Contest') }}{{ _('Users') }}
- {{ contest.user_count }}
{{ _('No contests') }}
+
{# /.sortable-scroll-wrapper #}
{% endmacro %} - {{ sortable_section(_('Ongoing contests'), current_contests, 'present') }} - {{ sortable_section(_('Upcoming contests'), future_contests, 'future') }} - {{ sortable_section(_('Past contests'), past_contests, 'past') }} - - - + {{ sortable_section(_('Ongoing contests'), current_contests, 'present') }} + {{ sortable_section(_('Upcoming contests'), future_contests, 'future') }} + {{ sortable_section(_('Past contests'), past_contests, 'past') }} +
{% endblock %} \ No newline at end of file From 6d5cea33147154f3017f077d0a700bfb56423461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Ho=C3=A0ng=20Th=C3=A1i?= Date: Fri, 10 Apr 2026 00:42:07 +0700 Subject: [PATCH 5/6] Fix drag-scroll speed & No-dragable for 'No contest' list --- templates/contest/reorder.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/contest/reorder.html b/templates/contest/reorder.html index 57f82f1e6..cc7a0d7ce 100644 --- a/templates/contest/reorder.html +++ b/templates/contest/reorder.html @@ -178,11 +178,11 @@

{{ title }}

{{ contest.user_count }} {% else %} - {{ _('No contests') }} + {{ _('No contests') }} {% endfor %} - {# /.sortable-scroll-wrapper #} +
{% endmacro %} @@ -199,7 +199,7 @@

{{ title }}

var scrollRaf = null; var pointerY = 0; var ZONE = 43; /* px – vùng nhạy gần mép trên/dưới wrapper */ - var MAX_SPEED = 20; /* px/frame – tốc độ cuộn tối đa */ + var MAX_SPEED = 26; /* px/frame – tốc độ cuộn tối đa */ document.addEventListener('pointermove', function (e) { pointerY = e.clientY; @@ -226,6 +226,8 @@

{{ title }}

} document.querySelectorAll('.sortable-list').forEach(function (el) { + var chk = el.querySelectorAll('.no-contest-nc'); + if (chk.length > 0) return; var wrapper = el.closest('.sortable-scroll-wrapper'); Sortable.create(el, { From 000141fb7fb7cd98fba9b63a1e115cea10b15bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Ho=C3=A0ng=20Th=C3=A1i?= Date: Fri, 10 Apr 2026 21:16:38 +0700 Subject: [PATCH 6/6] Fix some bug about 'drag & drop' function and add confirm box for reorder function when leaving --- locale/vi/LC_MESSAGES/django.po | 4 + templates/contest/reorder.html | 141 ++++++++++++++++++++++++-------- 2 files changed, 111 insertions(+), 34 deletions(-) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index f27035546..302d24299 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -7718,3 +7718,7 @@ msgstr "Đã cập nhật thứ tự này" #: templates/contest/reorder.html msgid "Error while saving order" msgstr "Lỗi khi cập nhật thứ tự mới" + +#: templates/contest/reorder.html +msgid "Your changes may not have been saved yet. Do you want to leave the page" +msgstr "Các thay đổi của bạn có thể chưa được lưu. Bạn có muốn rời khỏi trang không" diff --git a/templates/contest/reorder.html b/templates/contest/reorder.html index cc7a0d7ce..3e667e0b0 100644 --- a/templates/contest/reorder.html +++ b/templates/contest/reorder.html @@ -88,7 +88,7 @@

{{ _('Reorder contests') }}

- {% for year in all_years %} @@ -100,7 +100,7 @@

{{ _('Reorder contests') }}

- {% for tag in all_tags %} @@ -195,64 +195,113 @@

{{ title }}

{% endblock %} \ No newline at end of file