From 8ad23ca32fd6e82486d02f740018765ec9bd5643 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/8] 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 1490519736c091925b452be0fdd9c11fff5b5286 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/8] 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 67d5c99fd5d28dbe233efab9b7c9a623f887780f 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/8] Fix frontend --- locale/vi/LC_MESSAGES/django.po | 46 ++++++++++++++++++++ resources/contest.scss | 76 +++++++++++++++++++++++++++++++++ templates/contest/reorder.html | 25 +++++++---- 3 files changed, 139 insertions(+), 8 deletions(-) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index d32af3cff..f3211b16e 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -7656,3 +7656,49 @@ msgstr "File logo" #: judge/models/contest.py: 203 msgid "rank display options" msgstr "Hiển thị đại diện trong rank" + +#: 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" + +#: judge/models/profile.py:178 +msgid "If private, only these organizations may choose the logo" +msgstr "Nếu cho private, chỉ những tổ chức trên mới có thể chọn logo" + +#: judge/forms.py:93 +#, python-format +msgid "You are not allowed to use this logo" +msgstr "" +"Bạn không có quyền sử dụng logo này!" + +#: 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 400859da4ed97a4a5304db5984c2836db35a1ef9 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/8] 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 f3211b16e..25775c3a1 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 e51e53366b11363eae351a57a538ad5940fbeb13 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/8] 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 ef2ef29dd5908eab12d28e9eac68d153a6f1a238 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/8] 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 25775c3a1..6a7e99554 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 From 5246a6b80a616bd3d113256ee7e4cbe73d187603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Ho=C3=A0ng=20Th=C3=A1i?= Date: Fri, 10 Apr 2026 21:46:59 +0700 Subject: [PATCH 7/8] Fix to pass the 'lint' tests --- judge/views/contests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/judge/views/contests.py b/judge/views/contests.py index bf576696f..a2e3a620b 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -9,6 +9,7 @@ from bs4 import BeautifulSoup from django import forms from django.conf import settings +from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.context_processors import PermWrapper from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.contrib.auth.models import User as UserModel @@ -24,6 +25,7 @@ from django.template.loader import get_template from django.urls import reverse from django.utils import timezone +from django.utils.decorators import method_decorator from django.utils.functional import cached_property from django.utils.html import escape, format_html from django.utils.safestring import mark_safe @@ -54,8 +56,6 @@ 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', @@ -1790,7 +1790,7 @@ def post(self, request): if not keys: continue current_orders = sorted( - Contest.objects.filter(key__in=keys).values_list('sort_order', flat=True) + 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) From a8799cda1426241aff80ee0ebf8bcbda9aff9136 Mon Sep 17 00:00:00 2001 From: ThisIsNotNam Date: Fri, 10 Apr 2026 15:53:50 +0000 Subject: [PATCH 8/8] automatically add tag filter for contets reordering --- judge/views/contests.py | 1 + templates/contest/contest-list-tabs.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/judge/views/contests.py b/judge/views/contests.py index a2e3a620b..b1a971581 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -241,6 +241,7 @@ def get_context_data(self, **kwargs): context['first_page_href'] = '.' context['page_suffix'] = '#past-contests' context['search_query'] = self.search_query + context['current_tag_key'] = self.tag.key if self.tag else None context.update(self.get_sort_context()) context.update(self.get_sort_paginate_context()) return context diff --git a/templates/contest/contest-list-tabs.html b/templates/contest/contest-list-tabs.html index 2178d72a8..12f522fa1 100644 --- a/templates/contest/contest-list-tabs.html +++ b/templates/contest/contest-list-tabs.html @@ -31,7 +31,7 @@ {{ make_tab('list', 'fa-list', url('contest_list'), _('List')) }} {{ make_tab('calendar', 'fa-calendar', url('contest_calendar', now.year, now.month), _('Calendar')) }} {% if request.user.is_staff and (perms.judge.edit_all_contest or perms.judge.edit_own_contest) %} - {{ make_tab('reorder', 'fa-sort', url('contest_reorder'), _('Reorder contests')) }} + {{ make_tab('reorder', 'fa-sort', url('contest_reorder') + ('?tag=' + current_tag_key if current_tag_key else ''), _('Reorder contests')) }} {{ make_tab('admin', 'fa-edit', url('admin:judge_contest_changelist'), _('Admin')) }} {% endif %} {% endblock %}