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..bf576696f 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 @@ -219,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 @@ -1744,3 +1754,44 @@ 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.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'), + '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): + 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/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index d32af3cff..6a7e99554 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -7656,3 +7656,69 @@ 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" + +#: 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" + +#: 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/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{{ _('Reorder contests') }} +{% endblock %} + +{% block content_js_media %} + +{% endblock %} + +{% block body %} + + +
+
+
+ + +
+ +
+ + +
+ + {% 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