From c4a91ccf0f490a82e096ae6a95ba306594dbbaaf Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sun, 20 Feb 2022 19:33:35 -0600 Subject: [PATCH 001/959] Initial IDE --- dmoj/urls.py | 7 +- judge/views/ide.py | 29 ++ resources/download.js | 132 +++++++ resources/ide.css | 166 ++++++++ resources/ide.js | 868 ++++++++++++++++++++++++++++++++++++++++++ templates/ide.html | 227 +++++++++++ 6 files changed, 1428 insertions(+), 1 deletion(-) create mode 100644 judge/views/ide.py create mode 100644 resources/download.js create mode 100644 resources/ide.css create mode 100644 resources/ide.js create mode 100644 templates/ide.html diff --git a/dmoj/urls.py b/dmoj/urls.py index ecc17b9..1fad04d 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -18,7 +18,7 @@ from judge.feed import AtomBlogFeed, AtomCommentFeed, AtomProblemFeed, BlogFeed, from judge.forms import CustomAuthenticationForm from judge.sitemap import BlogPostSitemap, ContestSitemap, HomePageSitemap, OrganizationSitemap, ProblemSitemap, \ SolutionSitemap, UrlSitemap, UserSitemap -from judge.views import TitledTemplateView, about, api, blog, comment, contests, language, license, mailgun, \ +from judge.views import TitledTemplateView, about, api, blog, comment, contests, ide, language, license, mailgun, \ notification, organization, preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tasks, \ ticket, totp, user, widgets from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ @@ -399,6 +399,11 @@ urlpatterns = [ url(r'submit/$', user.import_users_submit, name='import_users_submit'), url(r'sample/$', user.sample_import_users, name='import_users_sample') ])), + + url(r'^ide/', include([ + url(r'^$', login_required(TitledTemplateView.as_view(template_name='ide.html', title=_('IDE'))), name='ide'), + url(r'api', ide.api, name='ide_api'), + ])), ] favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png', diff --git a/judge/views/ide.py b/judge/views/ide.py new file mode 100644 index 0000000..cbdba47 --- /dev/null +++ b/judge/views/ide.py @@ -0,0 +1,29 @@ +from django.utils.translation import gettext as _ +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse, Http404, JsonResponse +from django.views.decorators.csrf import csrf_exempt + +import requests, json, http + +PREFIX_URL = 'ide/api' + +@login_required +@csrf_exempt +def api(request): + url = 'http://localhost:2358' + request.get_full_path()[len(PREFIX_URL) + 1:] + headers = {'X-Judge0-Token': 'cuom1999'} + r = None + if request.method == 'POST': + r = requests.post(url, data=json.loads(request.body.decode('utf-8')), headers=headers) + elif request.method == 'GET': + r = requests.get(url, headers=headers) + else: + return Http404() + + res = r.content.decode('utf-8') + try: + res = json.loads(r.content.decode('utf-8')) + return JsonResponse(res, status=r.status_code, safe=False) + except Exception: + return HttpResponse(res) + \ No newline at end of file diff --git a/resources/download.js b/resources/download.js new file mode 100644 index 0000000..9312aec --- /dev/null +++ b/resources/download.js @@ -0,0 +1,132 @@ +//download.js v3.0, by dandavis; 2008-2014. [CCBY2] see http://danml.com/download.html for tests/usage +// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime +// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs +// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support + +// data can be a string, Blob, File, or dataURL + + + + +function download(data, strFileName, strMimeType) { + + var self = window, // this script is only for browsers anyway... + u = "application/octet-stream", // this default mime also triggers iframe downloads + m = strMimeType || u, + x = data, + D = document, + a = D.createElement("a"), + z = function(a){return String(a);}, + + + B = self.Blob || self.MozBlob || self.WebKitBlob || z, + BB = self.MSBlobBuilder || self.WebKitBlobBuilder || self.BlobBuilder, + fn = strFileName || "download", + blob, + b, + ua, + fr; + + //if(typeof B.bind === 'function' ){ B=B.bind(self); } + + if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback + x=[x, m]; + m=x[0]; + x=x[1]; + } + + + + //go ahead and download dataURLs right away + if(String(x).match(/^data\:[\w+\-]+\/[\w+\-]+[,;]/)){ + return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs: + navigator.msSaveBlob(d2b(x), fn) : + saver(x) ; // everyone else can save dataURLs un-processed + }//end if dataURL passed? + + try{ + + blob = x instanceof B ? + x : + new B([x], {type: m}) ; + }catch(y){ + if(BB){ + b = new BB(); + b.append([x]); + blob = b.getBlob(m); // the blob + } + + } + + + + function d2b(u) { + var p= u.split(/[:;,]/), + t= p[1], + dec= p[2] == "base64" ? atob : decodeURIComponent, + bin= dec(p.pop()), + mx= bin.length, + i= 0, + uia= new Uint8Array(mx); + + for(i;i.dropdown { + border-radius: 0 .28571429rem .28571429rem 0; +} + +.ui.selection.dropdown { + width: 205px; +} + +.lm_header .lm_tab { + padding-bottom: 3px; + height: 16px; + font-family: "Droid Sans Mono", monospace, monospace, "Droid Sans Fallback"; + font-size: 14px; +} + +.lm_header .lm_tab.lm_active { + box-shadow: none; + padding-bottom: 3px; +} + +#site-navigation { + border-radius: 0; + margin: 0; + background: #1e1e1e; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + height: 45px; +} + +#navigation-message { + font-size: 1.2em; + white-space: nowrap; + overflow: hidden; +} + +@keyframes slide { + 0% { transform: translateX(570px); } + 100% { transform: translateX(-100%); } +} +@-webkit-keyframes slide { + 0% { transform: translateX(570px); } + 100% { transform: translateX(-100%); } +} + +.navigation-message-text { + white-space: nowrap; + -moz-animation: slide 60s linear infinite; + -webkit-animation: slide 60s linear infinite; + animation: slide 60s linear infinite; +} + +.navigation-message-text:hover { + -moz-animation-play-state: paused; + -webkit-animation-play-state: paused; + animation-play-state: paused; +} + +#navigation-message a { + color: #41a5f1; + font-weight: bold; +} + +#site-icon { + height: 40px; + vertical-align: middle; +} + +#site-header { + padding-top: 0; + padding-bottom: 0; +} + +#site-header h2 { + display: inline; + vertical-align: middle; + font-family: 'Exo 2', sans-serif; +} + +#site-content { + height: calc(100% - 45px - 19px); +} + +#site-modal .header { + color: #db2828; +} + +#site-footer { + background-color: darkslategrey; + bottom: 0; + font-family: monospace, monospace, "Droid Sans Fallback"; + font-size: 13px; + height: 19px; + padding-left: 16px; + padding-right: 16px; + position: fixed; +} + +#site-footer { + color: gray; + left: 0; + width: 100%; +} + +#editor-status-line { + background: transparent; + color: #fff; + font-family: monospace; + height: 19px; + padding: 0px 16px; + width: inherit; +} + +#editor-status-line input[type=text] { + background: transparent; + border: none; + color: #fff; + font-family: monospace; + outline: none; +} + +#editor-status-line input[type=text]::selection { + background-color: #cce2ff; + color: #fff; +} + +.blink { + animation: blinker 1s linear infinite; +} + +@keyframes blinker { + 50% { + background: #FFD700; + } +} + +.dot { + background: #0E6EB8; + position: absolute; + border-radius: 50px; + width: 5px; + height: 5px; + right: 7.5px; + top: 7.5px; +} + +.no-left-padding { + padding-left: 0 !important; +} + +input[type="text"] { + width: 205px !important; +} \ No newline at end of file diff --git a/resources/ide.js b/resources/ide.js new file mode 100644 index 0000000..08020c9 --- /dev/null +++ b/resources/ide.js @@ -0,0 +1,868 @@ +var wait = true; +var check_timeout = 300; + +var editorMode = localStorageGetItem("editorMode") || "normal"; +var redirectStderrToStdout = ((localStorageGetItem("redirectStderrToStdout") || "false") === "true"); +var editorModeObject = null; + +var fontSize = 14; + +var MonacoVim; +var MonacoEmacs; + +var layout; + +var sourceEditor; +var stdinEditor; +var stdoutEditor; +var stderrEditor; + +var isEditorDirty = false; +var currentLanguageId; + +var $selectLanguage +var $selectTheme +var $insertTemplateBtn; +var $runBtn; + +var timeStart; +var timeEnd; + +var layoutConfig = { + settings: { + showPopoutIcon: false, + reorderEnabled: true + }, + dimensions: { + borderWidth: 3, + headerHeight: 22 + }, + content: [{ + type: "row", + content: [{ + type: "component", + componentName: "source", + title: "SOURCE", + isClosable: false, + componentState: { + readOnly: false + }, + width: 60 + }, { + type: "column", + content: [{ + type: "stack", + content: [{ + type: "component", + componentName: "stdin", + title: "STDIN", + isClosable: false, + componentState: { + readOnly: false + } + }] + }, { + type: "stack", + content: [{ + type: "component", + componentName: "stdout", + title: "STDOUT", + isClosable: false, + componentState: { + readOnly: true + } + }, { + type: "component", + componentName: "stderr", + title: "STDERR", + isClosable: false, + componentState: { + readOnly: true + } + }, + ] + }] + }] + }] +}; + +function encode(str) { + return btoa(unescape(encodeURIComponent(str || ""))); +} + +function decode(bytes) { + var escaped = escape(atob(bytes || "")); + try { + return decodeURIComponent(escaped); + } catch { + return unescape(escaped); + } +} + +function localStorageSetItem(key, value) { + try { + localStorage.setItem(key, value); + } catch (ignorable) { + } +} + +function localStorageGetItem(key) { + try { + return localStorage.getItem(key); + } catch (ignorable) { + return null; + } +} + + +function showError(title, content) { + $("#site-modal #title").html(title); + $("#site-modal .content").html(content); + $("#site-modal").modal("show"); +} + +function handleError(jqXHR, textStatus, errorThrown) { + showError(`${jqXHR.statusText} (${jqXHR.status})`, `
${JSON.stringify(jqXHR, null, 4)}
`); +} + +function handleRunError(jqXHR, textStatus, errorThrown) { + handleError(jqXHR, textStatus, errorThrown); + $runBtn.removeClass("loading"); +} + +function handleResult(data) { + timeEnd = performance.now(); + + var status = data.status; + var stdout = decode(data.stdout); + var stderr = decode(data.stderr); + var compile_output = decode(data.compile_output); + var time = (data.time === null ? "-" : data.time + "s"); + var memory = (data.memory === null ? "-" : data.memory + "KB"); + + stdoutEditor.setValue(compile_output + stdout + `\n[${status.description}, ${time}, ${memory}]`); + stderrEditor.setValue(stderr); + + if (stdout !== "") { + var dot = document.getElementById("stdout-dot"); + if (!dot.parentElement.classList.contains("lm_active")) { + dot.hidden = false; + } + } + if (stderr !== "") { + var dot = document.getElementById("stderr-dot"); + if (!dot.parentElement.classList.contains("lm_active")) { + dot.hidden = false; + } + } + + $runBtn.removeClass("loading"); +} + +function getIdFromURI() { + var uri = location.search.substr(1).trim(); + return uri.split("&")[0]; +} + +function downloadSource() { + var value = parseInt($selectLanguage.val()); + download(sourceEditor.getValue(), fileNames[value], "text/plain"); +} + +function loadSavedSource() { + snippet_id = getIdFromURI(); + + if (snippet_id.length == 36) { + $.ajax({ + url: apiUrl + "/submissions/" + snippet_id + "?fields=source_code,language_id,stdin,stdout,stderr,compile_output,message,time,memory,status&base64_encoded=true", + type: "GET", + success: function(data, textStatus, jqXHR) { + sourceEditor.setValue(decode(data["source_code"])); + $selectLanguage.dropdown("set selected", data["language_id"]); + stdinEditor.setValue(decode(data["stdin"])); + stderrEditor.setValue(decode(data["stderr"])); + var time = (data.time === null ? "-" : data.time + "s"); + var memory = (data.memory === null ? "-" : data.memory + "KB"); + stdoutEditor.setValue(decode(data["compile_output"]) + decode(data["stdout"]) + `\n[${data.status.description}, ${time}, ${memory}]`); + changeEditorLanguage(); + }, + error: handleRunError + }); + } else { + loadRandomLanguage(); + } +} + +function run() { + if (sourceEditor.getValue().trim() === "") { + showError("Error", "Source code can't be empty!"); + return; + } else { + $runBtn.addClass("loading"); + } + + document.getElementById("stdout-dot").hidden = true; + document.getElementById("stderr-dot").hidden = true; + + stdoutEditor.setValue(""); + stderrEditor.setValue(""); + + var sourceValue = encode(sourceEditor.getValue()); + var stdinValue = encode(stdinEditor.getValue()); + var languageId = resolveLanguageId($selectLanguage.val()); + + if (parseInt(languageId) === 44) { + sourceValue = sourceEditor.getValue(); + } + + var data = { + source_code: sourceValue, + language_id: languageId, + stdin: stdinValue, + redirect_stderr_to_stdout: redirectStderrToStdout + }; + + var sendRequest = function(data) { + timeStart = performance.now(); + $.ajax({ + url: apiUrl + `/submissions?base64_encoded=true&wait=${wait}`, + type: "POST", + async: true, + contentType: "application/json", + data: JSON.stringify(data), + xhrFields: { + withCredentials: apiUrl.indexOf("/secure") != -1 ? true : false + }, + success: function (data, textStatus, jqXHR) { + console.log(data.token); + if (wait == true) { + handleResult(data); + } else { + setTimeout(fetchSubmission.bind(null, data.token), check_timeout); + } + }, + error: handleRunError + }); + } + + sendRequest(data); +} + +function fetchSubmission(submission_token) { + $.ajax({ + url: apiUrl + "/submissions/" + submission_token + "?base64_encoded=true", + type: "GET", + async: true, + success: function (data, textStatus, jqXHR) { + if (data.status.id <= 2) { // In Queue or Processing + setTimeout(fetchSubmission.bind(null, submission_token), check_timeout); + return; + } + handleResult(data); + }, + error: handleRunError + }); +} + +function changeEditorLanguage() { + monaco.editor.setModelLanguage(sourceEditor.getModel(), $selectLanguage.find(":selected").attr("mode")); + currentLanguageId = parseInt($selectLanguage.val()); + $(".lm_title")[0].innerText = fileNames[currentLanguageId]; + apiUrl = resolveApiUrl($selectLanguage.val()); +} + +function insertTemplate() { + currentLanguageId = parseInt($selectLanguage.val()); + sourceEditor.setValue(sources[currentLanguageId]); + changeEditorLanguage(); +} + +function loadRandomLanguage() { + var values = []; + for (var i = 0; i < $selectLanguage[0].options.length; ++i) { + values.push($selectLanguage[0].options[i].value); + } + // $selectLanguage.dropdown("set selected", values[Math.floor(Math.random() * $selectLanguage[0].length)]); + $selectLanguage.dropdown("set selected", values[9]); + apiUrl = resolveApiUrl($selectLanguage.val()); + insertTemplate(); +} + +function resizeEditor(layoutInfo) { + if (editorMode != "normal") { + var statusLineHeight = $("#editor-status-line").height(); + layoutInfo.height -= statusLineHeight; + layoutInfo.contentHeight -= statusLineHeight; + } +} + +function disposeEditorModeObject() { + try { + editorModeObject.dispose(); + editorModeObject = null; + } catch(ignorable) { + } +} + +function changeEditorMode() { + disposeEditorModeObject(); + + if (editorMode == "vim") { + editorModeObject = MonacoVim.initVimMode(sourceEditor, $("#editor-status-line")[0]); + } else if (editorMode == "emacs") { + var statusNode = $("#editor-status-line")[0]; + editorModeObject = new MonacoEmacs.EmacsExtension(sourceEditor); + editorModeObject.onDidMarkChange(function(e) { + statusNode.textContent = e ? "Mark Set!" : "Mark Unset"; + }); + editorModeObject.onDidChangeKey(function(str) { + statusNode.textContent = str; + }); + editorModeObject.start(); + } +} + +function resolveLanguageId(id) { + id = parseInt(id); + return id; +} + +function resolveApiUrl(id) { + id = parseInt(id); + return apiUrl; +} + +function editorsUpdateFontSize(fontSize) { + sourceEditor.updateOptions({fontSize: fontSize}); + stdinEditor.updateOptions({fontSize: fontSize}); + stdoutEditor.updateOptions({fontSize: fontSize}); + stderrEditor.updateOptions({fontSize: fontSize}); +} + +function getDefaultTheme() { + return localStorageGetItem('editor-theme') || 'vs-dark'; +} + +function editorsUpdateTheme(isInit) { + var theme = $selectTheme.val(); + if (isInit) { + theme = getDefaultTheme(); + $selectTheme.val(theme).change(); + } + else localStorageSetItem('editor-theme', theme); + + var $siteNavigation = $("#site-navigation"); + if (theme == 'vs') { + $siteNavigation.removeClass('inverted'); + $siteNavigation.css('background', 'white'); + } + else { + $("#site-navigation").addClass('inverted'); + $siteNavigation.css('background', '#1e1e1e'); + } + sourceEditor.updateOptions({theme: theme}); + stdinEditor.updateOptions({theme: theme}); + stdoutEditor.updateOptions({theme: theme}); + stderrEditor.updateOptions({theme: theme}); +} + +function updateScreenElements() { + var display = window.innerWidth <= 1200 ? "none" : ""; + $(".wide.screen.only").each(function(index) { + $(this).css("display", display); + }); +} + +$(window).resize(function() { + layout.updateSize(); + updateScreenElements(); +}); + +$(document).ready(function () { + updateScreenElements(); + + $selectLanguage = $("#select-language"); + $selectLanguage.change(function (e) { + if (!isEditorDirty) { + insertTemplate(); + } else { + changeEditorLanguage(); + } + }); + + $selectTheme = $("#select-theme"); + $selectTheme.change(function(e) { + editorsUpdateTheme(false); + }); + + $insertTemplateBtn = $("#insert-template-btn"); + $insertTemplateBtn.click(function (e) { + if (isEditorDirty && confirm("Are you sure? Your current changes will be lost.")) { + insertTemplate(); + } + }); + + $runBtn = $("#run-btn"); + $runBtn.click(function (e) { + run(); + }); + + $(`input[name="editor-mode"][value="${editorMode}"]`).prop("checked", true); + $("input[name=\"editor-mode\"]").on("change", function(e) { + editorMode = e.target.value; + localStorageSetItem("editorMode", editorMode); + + resizeEditor(sourceEditor.getLayoutInfo()); + changeEditorMode(); + + sourceEditor.focus(); + }); + + $("input[name=\"redirect-output\"]").prop("checked", redirectStderrToStdout) + $("input[name=\"redirect-output\"]").on("change", function(e) { + redirectStderrToStdout = e.target.checked; + localStorageSetItem("redirectStderrToStdout", redirectStderrToStdout); + }); + + $("body").keydown(function (e) { + var keyCode = e.keyCode || e.which; + if (keyCode == 120 || (event.ctrlKey && keyCode == 66)) { // F9 || ctrl B + e.preventDefault(); + run(); + } else if (event.ctrlKey && (keyCode == 107 || keyCode == 187)) { // Ctrl++ + e.preventDefault(); + fontSize += 1; + editorsUpdateFontSize(fontSize); + } else if (event.ctrlKey && (keyCode == 107 || keyCode == 189)) { // Ctrl+- + e.preventDefault(); + fontSize -= 1; + editorsUpdateFontSize(fontSize); + } + }); + + $("select.dropdown").dropdown(); + $(".ui.dropdown").dropdown(); + $(".ui.dropdown.site-links").dropdown({action: "hide", on: "hover"}); + $(".ui.checkbox").checkbox(); + $(".message .close").on("click", function () { + $(this).closest(".message").transition("fade"); + }); + + require(["vs/editor/editor.main", "monaco-vim", "monaco-emacs"], function (ignorable, MVim, MEmacs) { + layout = new GoldenLayout(layoutConfig, $("#site-content")); + + MonacoVim = MVim; + MonacoEmacs = MEmacs; + + layout.registerComponent("source", function (container, state) { + sourceEditor = monaco.editor.create(container.getElement()[0], { + automaticLayout: true, + theme: getDefaultTheme(), + scrollBeyondLastLine: true, + readOnly: state.readOnly, + language: "cpp", + minimap: { + enabled: false + }, + matchBrackets: false, + }); + + changeEditorMode(); + + sourceEditor.getModel().onDidChangeContent(function (e) { + currentLanguageId = parseInt($selectLanguage.val()); + isEditorDirty = sourceEditor.getValue() != sources[currentLanguageId]; + }); + + sourceEditor.onDidLayoutChange(resizeEditor); + + sourceEditor.onDidChangeCursorPosition(function(e) { + var line = sourceEditor.getPosition().lineNumber; + var col = sourceEditor.getPosition().column; + $('#cursor-position').html(`Line ${line}, Column ${col}`) + }) + }); + + layout.registerComponent("stdin", function (container, state) { + stdinEditor = monaco.editor.create(container.getElement()[0], { + automaticLayout: true, + theme: getDefaultTheme(), + scrollBeyondLastLine: false, + readOnly: state.readOnly, + language: "plaintext", + minimap: { + enabled: false + }, + wordWrap: "on", + }); + }); + + layout.registerComponent("stdout", function (container, state) { + stdoutEditor = monaco.editor.create(container.getElement()[0], { + automaticLayout: true, + theme: getDefaultTheme(), + scrollBeyondLastLine: false, + readOnly: state.readOnly, + language: "plaintext", + minimap: { + enabled: false + }, + wordWrap: "on", + }); + + container.on("tab", function(tab) { + tab.element.append(""); + tab.element.on("mousedown", function(e) { + e.target.closest(".lm_tab").children[3].hidden = true; + }); + }); + }); + + layout.registerComponent("stderr", function (container, state) { + stderrEditor = monaco.editor.create(container.getElement()[0], { + automaticLayout: true, + theme: getDefaultTheme(), + scrollBeyondLastLine: false, + readOnly: state.readOnly, + language: "plaintext", + minimap: { + enabled: false + }, + wordWrap: "on", + }); + + container.on("tab", function(tab) { + tab.element.append(""); + tab.element.on("mousedown", function(e) { + e.target.closest(".lm_tab").children[3].hidden = true; + }); + }); + }); + + layout.on("initialised", function () { + $(".monaco-editor")[0].appendChild($("#editor-status-line")[0]); + if (getIdFromURI()) { + loadSavedSource(); + } else { + loadRandomLanguage(); + } + $("#site-navigation").css("border-bottom", "1px solid black"); + sourceEditor.focus(); + editorsUpdateFontSize(fontSize); + editorsUpdateTheme(true); + }); + + layout.init(); + }); +}); + +// Template Sources +var assemblySource = "\ +section .text\n\ + global _start\n\ +\n\ +_start:\n\ +\n\ + xor eax, eax\n\ + lea edx, [rax+len]\n\ + mov al, 1\n\ + mov esi, msg\n\ + mov edi, eax\n\ + syscall\n\ +\n\ + xor edi, edi\n\ + lea eax, [rdi+60]\n\ + syscall\n\ +\n\ +section .rodata\n\ +\n\ +msg db 'hello, world', 0xa\n\ +len equ $ - msg\n\ +"; + +var bashSource = "echo \"hello, world\""; + +var basicSource = "PRINT \"hello, world\""; + +var cSource = "\ +// Powered by Judge0\n\ +#include \n\ +\n\ +int main(void) {\n\ + printf(\"Hello Judge0!\\n\");\n\ + return 0;\n\ +}\n\ +"; + +var csharpSource = "\ +public class Hello {\n\ + public static void Main() {\n\ + System.Console.WriteLine(\"hello, world\");\n\ + }\n\ +}\n\ +"; + +var cppSource = "\ +#include \n\ +\n\ +int main() {\n\ + std::cout << \"hello, world\" << std::endl;\n\ + return 0;\n\ +}\n\ +"; + +var clojureSource = "(println \"hello, world\")\n"; + +var cobolSource = "\ +IDENTIFICATION DIVISION.\n\ +PROGRAM-ID. MAIN.\n\ +PROCEDURE DIVISION.\n\ +DISPLAY \"hello, world\".\n\ +STOP RUN.\n\ +"; + +var lispSource = "(write-line \"hello, world\")"; + +var dSource = "\ +import std.stdio;\n\ +\n\ +void main()\n\ +{\n\ + writeln(\"hello, world\");\n\ +}\n\ +"; + +var elixirSource = "IO.puts \"hello, world\""; + +var erlangSource = "\ +main(_) ->\n\ + io:fwrite(\"hello, world\\n\").\n\ +"; + +var executableSource = "\ +Judge0 IDE assumes that content of executable is Base64 encoded.\n\ +\n\ +This means that you should Base64 encode content of your binary,\n\ +paste it here and click \"Run\".\n\ +\n\ +Here is an example of compiled \"hello, world\" NASM program.\n\ +Content of compiled binary is Base64 encoded and used as source code.\n\ +\n\ +https://ide.judge0.com/?kS_f\n\ +"; + +var fsharpSource = "printfn \"hello, world\"\n"; + +var fortranSource = "\ +program main\n\ + print *, \"hello, world\"\n\ +end\n\ +"; + +var goSource = "\ +package main\n\ +\n\ +import \"fmt\"\n\ +\n\ +func main() {\n\ + fmt.Println(\"hello, world\")\n\ +}\n\ +"; + +var groovySource = "println \"hello, world\"\n"; + +var haskellSource = "main = putStrLn \"hello, world\""; + +var javaSource = "\ +public class Main {\n\ + public static void main(String[] args) {\n\ + System.out.println(\"hello, world\");\n\ + }\n\ +}\n\ +"; + +var javaScriptSource = "console.log(\"hello, world\");"; + +var kotlinSource = "\ +fun main() {\n\ + println(\"hello, world\")\n\ +}\n\ +"; + +var luaSource = "print(\"hello, world\")"; + +var objectiveCSource = "\ +#import \n\ +\n\ +int main() {\n\ + @autoreleasepool {\n\ + char name[10];\n\ + scanf(\"%s\", name);\n\ + NSString *message = [NSString stringWithFormat:@\"hello, %s\\n\", name];\n\ + printf(\"%s\", message.UTF8String);\n\ + }\n\ + return 0;\n\ +}\n\ +"; + +var ocamlSource = "print_endline \"hello, world\""; + +var octaveSource = "printf(\"hello, world\\n\");"; + +var pascalSource = "\ +program Hello;\n\ +begin\n\ + writeln ('hello, world')\n\ +end.\n\ +"; + +var perlSource = "\ +my $name = ;\n\ +print \"hello, $name\";\n\ +"; + +var phpSource = "\ +\n\ +"; + +var plainTextSource = "hello, world\n"; + +var prologSource = "\ +:- initialization(main).\n\ +main :- write('hello, world\\n').\n\ +"; + +var pythonSource = "print(\"hello, world\")"; + +var rSource = "cat(\"hello, world\\n\")"; + +var rubySource = "puts \"hello, world\""; + +var rustSource = "\ +fn main() {\n\ + println!(\"hello, world\");\n\ +}\n\ +"; + +var scalaSource = "\ +object Main {\n\ + def main(args: Array[String]) = {\n\ + val name = scala.io.StdIn.readLine()\n\ + println(\"hello, \"+ name)\n\ + }\n\ +}\n\ +"; + +var swiftSource = "\ +import Foundation\n\ +let name = readLine()\n\ +print(\"hello, \\(name!)\")\n\ +"; + +var typescriptSource = "console.log(\"hello, world\");"; + +var vbSource = "\ +Public Module Program\n\ + Public Sub Main()\n\ + Console.WriteLine(\"hello, world\")\n\ + End Sub\n\ +End Module\n\ +"; + +var sources = { + 45: assemblySource, + 46: bashSource, + 47: basicSource, + 48: cSource, + 49: cSource, + 50: cSource, + 51: csharpSource, + 52: cppSource, + 53: cppSource, + 54: cppSource, + 55: lispSource, + 56: dSource, + 57: elixirSource, + 58: erlangSource, + 44: executableSource, + 59: fortranSource, + 60: goSource, + 61: haskellSource, + 62: javaSource, + 63: javaScriptSource, + 64: luaSource, + 65: ocamlSource, + 66: octaveSource, + 67: pascalSource, + 68: phpSource, + 43: plainTextSource, + 69: prologSource, + 70: pythonSource, + 71: pythonSource, + 72: rubySource, + 73: rustSource, + 74: typescriptSource, + 75: cSource, + 76: cppSource, + 77: cobolSource, + 78: kotlinSource, + 79: objectiveCSource, + 80: rSource, + 81: scalaSource, + 83: swiftSource, + 84: vbSource, + 85: perlSource, + 86: clojureSource, + 87: fsharpSource, + 88: groovySource, +}; + +var fileNames = { + 45: "main.asm", + 46: "script.sh", + 47: "main.bas", + 48: "main.c", + 49: "main.c", + 50: "main.c", + 51: "Main.cs", + 52: "main.cpp", + 53: "main.cpp", + 54: "main.cpp", + 55: "script.lisp", + 56: "main.d", + 57: "script.exs", + 58: "main.erl", + 44: "a.out", + 59: "main.f90", + 60: "main.go", + 61: "main.hs", + 62: "Main.java", + 63: "script.js", + 64: "script.lua", + 65: "main.ml", + 66: "script.m", + 67: "main.pas", + 68: "script.php", + 43: "text.txt", + 69: "main.pro", + 70: "script.py", + 71: "script.py", + 72: "script.rb", + 73: "main.rs", + 74: "script.ts", + 75: "main.c", + 76: "main.cpp", + 77: "main.cob", + 78: "Main.kt", + 79: "main.m", + 80: "script.r", + 81: "Main.scala", + 83: "Main.swift", + 84: "Main.vb", + 85: "script.pl", + 86: "main.clj", + 87: "script.fsx", + 88: "script.groovy", +}; \ No newline at end of file diff --git a/templates/ide.html b/templates/ide.html new file mode 100644 index 0000000..81f8460 --- /dev/null +++ b/templates/ide.html @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + {# Chrome 39 for Android colour #} + + {% if og_image %} + + {% endif %} + {% block og_title %}{% endblock %} + + + {% if meta_description %} + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + IDE + + + + + + + + + +
+ + + + + + + + + From 68c6f13926d29b922e6b631a7c4f065f606839d2 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 5 Mar 2022 16:13:01 -0600 Subject: [PATCH 002/959] Change hide_problem_tags default to True --- judge/migrations/0119_auto_20220306_0512.py | 18 ++++++++++++++++++ judge/models/contest.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 judge/migrations/0119_auto_20220306_0512.py diff --git a/judge/migrations/0119_auto_20220306_0512.py b/judge/migrations/0119_auto_20220306_0512.py new file mode 100644 index 0000000..c85f792 --- /dev/null +++ b/judge/migrations/0119_auto_20220306_0512.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.25 on 2022-03-05 22:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0118_rating'), + ] + + operations = [ + migrations.AlterField( + model_name='contest', + name='hide_problem_tags', + field=models.BooleanField(default=True, help_text='Whether problem tags should be hidden by default.', verbose_name='hide problem tags'), + ), + ] diff --git a/judge/models/contest.py b/judge/models/contest.py index 8d75ee3..ce70953 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -101,7 +101,7 @@ class Contest(models.Model): related_name='private_contestants+') hide_problem_tags = models.BooleanField(verbose_name=_('hide problem tags'), help_text=_('Whether problem tags should be hidden by default.'), - default=False) + default=True) run_pretests_only = models.BooleanField(verbose_name=_('run pretests only'), help_text=_('Whether judges should grade pretests only, versus all ' 'testcases. Commonly set during a contest, then unset ' From 2e3a45168e7bec372ca5fe3938f970046a493e3b Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 9 Mar 2022 23:38:29 -0600 Subject: [PATCH 003/959] Add problem vote --- dmoj/settings.py | 1 + dmoj/urls.py | 2 + judge/admin/__init__.py | 5 +- judge/admin/problem.py | 14 + judge/admin/profile.py | 2 +- judge/forms.py | 10 +- judge/migrations/0120_auto_20220306_1124.py | 34 ++ judge/models/__init__.py | 2 +- judge/models/problem.py | 45 +++ judge/models/profile.py | 5 + judge/views/problem.py | 65 +++- locale/vi/LC_MESSAGES/django.po | 297 +++++++++++------- .../admin/judge/problem/change_form.html | 5 + templates/problem/problem.html | 8 +- templates/problem/voting-controls.html | 73 +++++ templates/problem/voting-form.html | 93 ++++++ templates/problem/voting-stats.html | 146 +++++++++ 17 files changed, 685 insertions(+), 122 deletions(-) create mode 100644 judge/migrations/0120_auto_20220306_1124.py create mode 100644 templates/problem/voting-controls.html create mode 100644 templates/problem/voting-form.html create mode 100644 templates/problem/voting-stats.html diff --git a/dmoj/settings.py b/dmoj/settings.py index 86377a7..c01407d 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -162,6 +162,7 @@ else: 'children': [ 'judge.ProblemGroup', 'judge.ProblemType', + 'judge.ProblemPointsVote', ], }, { diff --git a/dmoj/urls.py b/dmoj/urls.py index ecc17b9..4869ab6 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -139,6 +139,8 @@ urlpatterns = [ url(r'^/tickets$', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'), url(r'^/tickets/new$', ticket.NewProblemTicketView.as_view(), name='new_problem_ticket'), + url(r'^/vote$', problem.Vote.as_view(), name='vote'), + url(r'^/manage/submission', include([ url('^$', problem_manage.ManageProblemSubmissionView.as_view(), name='problem_manage_submissions'), url('^/action$', problem_manage.ActionSubmissionsView.as_view(), name='problem_submissions_action'), diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index 51a7173..f079f32 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -5,7 +5,7 @@ from judge.admin.comments import CommentAdmin from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin -from judge.admin.problem import ProblemAdmin +from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin from judge.admin.profile import ProfileAdmin from judge.admin.runtime import JudgeAdmin, LanguageAdmin from judge.admin.submission import SubmissionAdmin @@ -13,7 +13,7 @@ from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.ticket import TicketAdmin from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \ ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \ - OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Ticket + OrganizationRequest, Problem, ProblemGroup, ProblemPointsVote, ProblemType, Profile, Submission, Ticket admin.site.register(BlogPost, BlogPostAdmin) admin.site.register(Comment, CommentAdmin) @@ -31,6 +31,7 @@ admin.site.register(Organization, OrganizationAdmin) admin.site.register(OrganizationRequest, OrganizationRequestAdmin) admin.site.register(Problem, ProblemAdmin) admin.site.register(ProblemGroup, ProblemGroupAdmin) +admin.site.register(ProblemPointsVote, ProblemPointsVoteAdmin) admin.site.register(ProblemType, ProblemTypeAdmin) admin.site.register(Profile, ProfileAdmin) admin.site.register(Submission, SubmissionAdmin) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 164caa1..3e92e74 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -236,3 +236,17 @@ class ProblemAdmin(VersionAdmin): if form.cleaned_data.get('change_message'): return form.cleaned_data['change_message'] return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs) + + +class ProblemPointsVoteAdmin(admin.ModelAdmin): + list_display = ('points', 'voter', 'problem', 'vote_time') + search_fields = ('voter', 'problem') + readonly_fields = ('voter', 'problem', 'vote_time') + + def has_change_permission(self, request, obj=None): + if obj is None: + return request.user.has_perm('judge.edit_own_problem') + return obj.problem.is_editable_by(request.user) + + def lookup_allowed(self, key, value): + return super().lookup_allowed(key, value) or key in ('problem__code',) \ No newline at end of file diff --git a/judge/admin/profile.py b/judge/admin/profile.py index e5fbf46..02a1f71 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -45,7 +45,7 @@ class TimezoneFilter(admin.SimpleListFilter): class ProfileAdmin(VersionAdmin): fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme', - 'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'notes', 'is_totp_enabled', 'user_script', + 'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'is_banned_problem_voting', 'notes', 'is_totp_enabled', 'user_script', 'current_contest') readonly_fields = ('user',) list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full', diff --git a/judge/forms.py b/judge/forms.py index 67ccd12..d704fc5 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -12,7 +12,7 @@ from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django_ace import AceWidget -from judge.models import Contest, Language, Organization, PrivateMessage, Problem, Profile, Submission +from judge.models import Contest, Language, Organization, PrivateMessage, Problem, ProblemPointsVote, Profile, Submission from judge.utils.subscription import newsletter_id from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \ Select2Widget @@ -161,4 +161,10 @@ class ContestCloneForm(Form): key = self.cleaned_data['key'] if Contest.objects.filter(key=key).exists(): raise ValidationError(_('Contest with key already exists.')) - return key \ No newline at end of file + return key + + +class ProblemPointsVoteForm(ModelForm): + class Meta: + model = ProblemPointsVote + fields = ['points'] \ No newline at end of file diff --git a/judge/migrations/0120_auto_20220306_1124.py b/judge/migrations/0120_auto_20220306_1124.py new file mode 100644 index 0000000..849461c --- /dev/null +++ b/judge/migrations/0120_auto_20220306_1124.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.25 on 2022-03-06 04:24 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0119_auto_20220306_0512'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='is_banned_problem_voting', + field=models.BooleanField(default=False, help_text="User will not be able to vote on problems' point values.", verbose_name='banned from voting'), + ), + migrations.CreateModel( + name='ProblemPointsVote', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('points', models.IntegerField(help_text='The amount of points you think this problem deserves.', validators=[django.core.validators.MinValueValidator(100), django.core.validators.MaxValueValidator(600)], verbose_name='proposed point value')), + ('vote_time', models.DateTimeField(auto_now_add=True, verbose_name='The time this vote was cast')), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problem_points_votes', to='judge.Problem')), + ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problem_points_votes', to='judge.Profile')), + ], + options={ + 'verbose_name': 'vote', + 'verbose_name_plural': 'votes', + }, + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index 23e0734..a875103 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -7,7 +7,7 @@ from judge.models.contest import Contest, ContestMoss, ContestParticipation, Con from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex from judge.models.message import PrivateMessage, PrivateMessageThread from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \ - ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet + ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet, ProblemPointsVote from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \ problem_directory_file from judge.models.profile import Organization, OrganizationRequest, Profile, Friend diff --git a/judge/models/problem.py b/judge/models/problem.py index 4f7e545..28dd102 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -375,6 +375,25 @@ class Problem(models.Model): save.alters_data = True + def can_vote(self, user): + if not user.is_authenticated: + return False + + # If the user is in contest, nothing should be shown. + if user.profile.current_contest: + return False + + # If the user is not allowed to vote + if user.profile.is_unlisted or user.profile.is_banned_problem_voting: + return False + + # If the user is banned from submitting to the problem. + if self.banned_users.filter(pk=user.pk).exists(): + return False + + # If the user has a full AC submission to the problem (solved the problem). + return self.submission_set.filter(user=user.profile, result='AC', points=F('problem__points')).exists() + class Meta: permissions = ( ('see_private_problem', 'See hidden problems'), @@ -448,3 +467,29 @@ class Solution(models.Model): ) verbose_name = _('solution') verbose_name_plural = _('solutions') + + +class ProblemPointsVote(models.Model): + points = models.IntegerField( + verbose_name=_('proposed point value'), + help_text=_('The amount of points you think this problem deserves.'), + validators=[ + MinValueValidator(100), + MaxValueValidator(600), + ], + ) + + voter = models.ForeignKey(Profile, related_name='problem_points_votes', on_delete=CASCADE, db_index=True) + problem = models.ForeignKey(Problem, related_name='problem_points_votes', on_delete=CASCADE, db_index=True) + vote_time = models.DateTimeField( + verbose_name=_('The time this vote was cast'), + auto_now_add=True, + blank=True, + ) + + class Meta: + verbose_name = _('vote') + verbose_name_plural = _('votes') + + def __str__(self): + return f'{self.voter}: {self.points} for {self.problem.code}' \ No newline at end of file diff --git a/judge/models/profile.py b/judge/models/profile.py index 7e2dc7f..b00dc50 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -98,6 +98,11 @@ class Profile(models.Model): default=False) is_unlisted = models.BooleanField(verbose_name=_('unlisted user'), help_text=_('User will not be ranked.'), default=False) + is_banned_problem_voting = models.BooleanField( + verbose_name=_('banned from voting'), + help_text=_("User will not be able to vote on problems' point values."), + default=False, + ) rating = models.IntegerField(null=True, default=None) user_script = models.TextField(verbose_name=_('user script'), default='', blank=True, max_length=65536, help_text=_('User-defined JavaScript for site customization.')) diff --git a/judge/views/problem.py b/judge/views/problem.py index e7f2763..78d538f 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -12,7 +12,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.db.models import Count, F, Prefetch, Q, Sum, Case, When, IntegerField from django.db.utils import ProgrammingError -from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect +from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render from django.template.loader import get_template from django.urls import reverse @@ -26,10 +26,10 @@ from django.views.generic.base import TemplateResponseMixin from django.views.generic.detail import SingleObjectMixin from judge.comments import CommentedDetailView -from judge.forms import ProblemCloneForm, ProblemSubmitForm +from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemClarification, \ - ProblemGroup, ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource, \ - TranslatedProblemForeignKeyQuerySet, Organization + ProblemGroup, ProblemTranslation, ProblemType, ProblemPointsVote, RuntimeVersion, Solution, Submission, SubmissionSource, \ + TranslatedProblemForeignKeyQuerySet, Organization from judge.pdf_problems import DefaultPdfMaker, HAS_PDF from judge.utils.diggpaginator import DiggPaginator from judge.utils.opengraph import generate_opengraph @@ -216,8 +216,65 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, DetailView): context['description'], 'problem') context['meta_description'] = self.object.summary or metadata[0] context['og_image'] = self.object.og_image or metadata[1] + + context['can_vote'] = self.object.can_vote(user) + if context['can_vote']: + try: + context['vote'] = ProblemPointsVote.objects.get(voter=user.profile, problem=self.object) + except ObjectDoesNotExist: + context['vote'] = None + else: + context['vote'] = None + + all_votes = list(self.object.problem_points_votes.order_by('points').values_list('points', flat=True)) + + context['has_votes'] = len(all_votes) > 0 + + # If the user is not currently in contest. + if not user.is_authenticated or user.profile.current_contest is None: + context['all_votes'] = all_votes + + context['max_possible_vote'] = 600 + context['min_possible_vote'] = 100 return context +class DeleteVote(ProblemMixin, SingleObjectMixin, View): + def get(self, request, *args, **kwargs): + return HttpResponseForbidden(status=405, content_type='text/plain') + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + if not request.user.is_authenticated: + return HttpResponseForbidden('Not signed in.', content_type='text/plain') + elif self.object.can_vote(request.user): + ProblemPointsVote.objects.filter(voter=request.profile, problem=self.object).delete() + return HttpResponse('success', content_type='text/plain') + else: + return HttpResponseForbidden('Not allowed to delete votes on this problem.', content_type='text/plain') + + +class Vote(ProblemMixin, SingleObjectMixin, View): + def get(self, request, *args, **kwargs): + return HttpResponseForbidden(status=405, content_type='text/plain') + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + if not self.object.can_vote(request.user): # Not allowed to vote for some reason. + return HttpResponseForbidden('Not allowed to vote on this problem.', content_type='text/plain') + + form = ProblemPointsVoteForm(request.POST) + if form.is_valid(): + with transaction.atomic(): + # Delete any pre existing votes. + ProblemPointsVote.objects.filter(voter=request.profile, problem=self.object).delete() + vote = form.save(commit=False) + vote.voter = request.profile + vote.problem = self.object + vote.save() + return JsonResponse({'points': vote.points}) + else: + return JsonResponse(form.errors, status=400) + class ProblemComments(ProblemMixin, TitleMixin, CommentedDetailView): context_object_name = 'problem' diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index b9d0339..36f657d 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-01-10 18:14+0700\n" +"POT-Creation-Date: 2022-03-10 12:30+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -20,7 +20,7 @@ msgstr "" #: chat_box/models.py:23 chat_box/models.py:41 chat_box/models.py:47 #: judge/admin/interface.py:110 judge/models/contest.py:403 -#: judge/models/contest.py:528 judge/models/profile.py:215 +#: judge/models/contest.py:528 judge/models/profile.py:220 msgid "user" msgstr "người dùng" @@ -36,71 +36,71 @@ msgstr "nội dung bình luận" msgid "last seen" msgstr "xem lần cuối" -#: chat_box/views.py:29 templates/chat/chat.html:4 templates/chat/chat.html:541 +#: chat_box/views.py:29 templates/chat/chat.html:4 templates/chat/chat.html:548 msgid "Chat Box" msgstr "Chat Box" -#: dmoj/settings.py:358 +#: dmoj/settings.py:359 msgid "German" msgstr "" -#: dmoj/settings.py:359 +#: dmoj/settings.py:360 msgid "English" msgstr "" -#: dmoj/settings.py:360 +#: dmoj/settings.py:361 msgid "Spanish" msgstr "" -#: dmoj/settings.py:361 +#: dmoj/settings.py:362 msgid "French" msgstr "" -#: dmoj/settings.py:362 +#: dmoj/settings.py:363 msgid "Croatian" msgstr "" -#: dmoj/settings.py:363 +#: dmoj/settings.py:364 msgid "Hungarian" msgstr "" -#: dmoj/settings.py:364 +#: dmoj/settings.py:365 msgid "Japanese" msgstr "" -#: dmoj/settings.py:365 +#: dmoj/settings.py:366 msgid "Korean" msgstr "" -#: dmoj/settings.py:366 +#: dmoj/settings.py:367 msgid "Brazilian Portuguese" msgstr "" -#: dmoj/settings.py:367 +#: dmoj/settings.py:368 msgid "Romanian" msgstr "" -#: dmoj/settings.py:368 +#: dmoj/settings.py:369 msgid "Russian" msgstr "" -#: dmoj/settings.py:369 +#: dmoj/settings.py:370 msgid "Serbian (Latin)" msgstr "" -#: dmoj/settings.py:370 +#: dmoj/settings.py:371 msgid "Turkish" msgstr "" -#: dmoj/settings.py:371 +#: dmoj/settings.py:372 msgid "Vietnamese" msgstr "Tiếng Việt" -#: dmoj/settings.py:372 +#: dmoj/settings.py:373 msgid "Simplified Chinese" msgstr "" -#: dmoj/settings.py:373 +#: dmoj/settings.py:374 msgid "Traditional Chinese" msgstr "" @@ -108,7 +108,7 @@ msgstr "" msgid "Login" msgstr "Đăng nhập" -#: dmoj/urls.py:106 templates/base.html:249 +#: dmoj/urls.py:106 templates/base.html:251 msgid "Home" msgstr "Trang chủ" @@ -217,7 +217,7 @@ msgstr "Tính toán lại kết quả" msgid "username" msgstr "tên đăng nhập" -#: judge/admin/contest.py:320 templates/base.html:337 +#: judge/admin/contest.py:320 templates/base.html:339 msgid "virtual" msgstr "ảo" @@ -587,9 +587,9 @@ msgstr "người bình luận" msgid "associated page" msgstr "trang tương ứng" -#: judge/models/comment.py:46 +#: judge/models/comment.py:46 judge/models/problem.py:492 msgid "votes" -msgstr "" +msgstr "bình chọn" #: judge/models/comment.py:48 msgid "hide the comment" @@ -607,7 +607,7 @@ msgstr "bình luận" msgid "comments" msgstr "" -#: judge/models/comment.py:137 judge/models/problem.py:443 +#: judge/models/comment.py:137 judge/models/problem.py:462 #, python-format msgid "Editorial for %s" msgstr "" @@ -725,7 +725,7 @@ msgstr "" msgid "description" msgstr "mô tả" -#: judge/models/contest.py:72 judge/models/problem.py:390 +#: judge/models/contest.py:72 judge/models/problem.py:409 #: judge/models/runtime.py:138 msgid "problems" msgstr "bài tập" @@ -739,7 +739,7 @@ msgid "end time" msgstr "thời gian kết thúc" #: judge/models/contest.py:75 judge/models/problem.py:118 -#: judge/models/problem.py:414 +#: judge/models/problem.py:433 msgid "time limit" msgstr "giới hạn thời gian" @@ -1037,8 +1037,8 @@ msgid "contest participations" msgstr "lần tham gia kỳ thi" #: judge/models/contest.py:491 judge/models/contest.py:513 -#: judge/models/contest.py:554 judge/models/problem.py:389 -#: judge/models/problem.py:394 judge/models/problem.py:412 +#: judge/models/contest.py:554 judge/models/problem.py:408 +#: judge/models/problem.py:413 judge/models/problem.py:431 #: judge/models/problem_data.py:40 msgid "problem" msgstr "bài tập" @@ -1176,7 +1176,7 @@ msgstr "mục cha" msgid "post title" msgstr "tiêu đề bài đăng" -#: judge/models/interface.py:67 judge/models/problem.py:432 +#: judge/models/interface.py:67 judge/models/problem.py:451 msgid "authors" msgstr "tác giả" @@ -1184,7 +1184,7 @@ msgstr "tác giả" msgid "slug" msgstr "slug" -#: judge/models/interface.py:69 judge/models/problem.py:430 +#: judge/models/interface.py:69 judge/models/problem.py:449 msgid "public visibility" msgstr "khả năng hiển thị công khai" @@ -1380,7 +1380,7 @@ msgid "" "are supported." msgstr "" -#: judge/models/problem.py:123 judge/models/problem.py:417 +#: judge/models/problem.py:123 judge/models/problem.py:436 msgid "memory limit" msgstr "" @@ -1453,67 +1453,85 @@ msgstr "" msgid "If private, only these organizations may see the problem." msgstr "" -#: judge/models/problem.py:395 judge/models/problem.py:413 +#: judge/models/problem.py:414 judge/models/problem.py:432 #: judge/models/runtime.py:111 msgid "language" msgstr "" -#: judge/models/problem.py:396 +#: judge/models/problem.py:415 msgid "translated name" msgstr "" -#: judge/models/problem.py:397 +#: judge/models/problem.py:416 msgid "translated description" msgstr "" -#: judge/models/problem.py:401 +#: judge/models/problem.py:420 msgid "problem translation" msgstr "" -#: judge/models/problem.py:402 +#: judge/models/problem.py:421 msgid "problem translations" msgstr "" -#: judge/models/problem.py:406 +#: judge/models/problem.py:425 msgid "clarified problem" msgstr "" -#: judge/models/problem.py:407 +#: judge/models/problem.py:426 msgid "clarification body" msgstr "" -#: judge/models/problem.py:408 +#: judge/models/problem.py:427 msgid "clarification timestamp" msgstr "" -#: judge/models/problem.py:423 +#: judge/models/problem.py:442 msgid "language-specific resource limit" msgstr "" -#: judge/models/problem.py:424 +#: judge/models/problem.py:443 msgid "language-specific resource limits" msgstr "" -#: judge/models/problem.py:428 +#: judge/models/problem.py:447 msgid "associated problem" msgstr "" -#: judge/models/problem.py:431 +#: judge/models/problem.py:450 msgid "publish date" msgstr "" -#: judge/models/problem.py:433 +#: judge/models/problem.py:452 msgid "editorial content" msgstr "nội dung lời giải" -#: judge/models/problem.py:449 +#: judge/models/problem.py:468 msgid "solution" msgstr "lời giải" -#: judge/models/problem.py:450 +#: judge/models/problem.py:469 msgid "solutions" msgstr "lời giải" +#: judge/models/problem.py:474 +#, fuzzy +#| msgid "point value" +msgid "proposed point value" +msgstr "điểm" + +#: judge/models/problem.py:475 +msgid "The amount of points you think this problem deserves." +msgstr "" + +#: judge/models/problem.py:485 +msgid "The time this vote was cast" +msgstr "" + +#: judge/models/problem.py:491 +msgid "vote" +msgstr "" + #: judge/models/problem_data.py:26 msgid "Standard" msgstr "Tiêu chuẩn" @@ -1707,7 +1725,7 @@ msgid "" msgstr "Ảnh này sẽ thay thế logo mặc định khi ở trong tổ chức." #: judge/models/profile.py:76 judge/models/profile.py:93 -#: judge/models/profile.py:216 +#: judge/models/profile.py:221 msgid "organization" msgstr "" @@ -1756,78 +1774,88 @@ msgid "User will not be ranked." msgstr "" #: judge/models/profile.py:102 +#, fuzzy +#| msgid "Banned from joining" +msgid "banned from voting" +msgstr "Bị cấm tham gia" + +#: judge/models/profile.py:103 +msgid "User will not be able to vote on problems' point values." +msgstr "" + +#: judge/models/profile.py:107 msgid "user script" msgstr "" -#: judge/models/profile.py:103 +#: judge/models/profile.py:108 msgid "User-defined JavaScript for site customization." msgstr "" -#: judge/models/profile.py:104 +#: judge/models/profile.py:109 msgid "current contest" msgstr "kỳ thi hiện tại" -#: judge/models/profile.py:106 +#: judge/models/profile.py:111 msgid "math engine" msgstr "" -#: judge/models/profile.py:108 +#: judge/models/profile.py:113 msgid "the rendering engine used to render math" msgstr "" -#: judge/models/profile.py:109 +#: judge/models/profile.py:114 msgid "2FA enabled" msgstr "" -#: judge/models/profile.py:110 +#: judge/models/profile.py:115 msgid "check to enable TOTP-based two factor authentication" msgstr "đánh dấu để sử dụng TOTP-based two factor authentication" -#: judge/models/profile.py:111 +#: judge/models/profile.py:116 msgid "TOTP key" msgstr "mã TOTP" -#: judge/models/profile.py:112 +#: judge/models/profile.py:117 msgid "32 character base32-encoded key for TOTP" msgstr "" -#: judge/models/profile.py:114 +#: judge/models/profile.py:119 msgid "TOTP key must be empty or base32" msgstr "" -#: judge/models/profile.py:115 +#: judge/models/profile.py:120 msgid "internal notes" msgstr "ghi chú nội bộ" -#: judge/models/profile.py:116 +#: judge/models/profile.py:121 msgid "Notes for administrators regarding this user." msgstr "Ghi chú riêng cho quản trị viên." -#: judge/models/profile.py:210 +#: judge/models/profile.py:215 msgid "user profile" msgstr "thông tin người dùng" -#: judge/models/profile.py:211 +#: judge/models/profile.py:216 msgid "user profiles" msgstr "thông tin người dùng" -#: judge/models/profile.py:218 +#: judge/models/profile.py:223 msgid "request time" msgstr "thời gian đăng ký" -#: judge/models/profile.py:219 +#: judge/models/profile.py:224 msgid "state" msgstr "trạng thái" -#: judge/models/profile.py:224 +#: judge/models/profile.py:229 msgid "reason" msgstr "lý do" -#: judge/models/profile.py:227 +#: judge/models/profile.py:232 msgid "organization join request" msgstr "đơn đăng ký tham gia" -#: judge/models/profile.py:228 +#: judge/models/profile.py:233 msgid "organization join requests" msgstr "đơn đăng ký tham gia" @@ -2666,46 +2694,46 @@ msgstr "Hướng dẫn cho {0}" msgid "Editorial for {0}" msgstr "Hướng dẫn cho {0}" -#: judge/views/problem.py:227 +#: judge/views/problem.py:284 #, python-brace-format msgid "Disscuss {0}" msgstr "" -#: judge/views/problem.py:230 +#: judge/views/problem.py:287 #, python-brace-format msgid "Discuss {0}" msgstr "Thảo luận {0}" -#: judge/views/problem.py:298 templates/contest/contest.html:79 +#: judge/views/problem.py:355 templates/contest/contest.html:79 #: templates/user/user-about.html:28 templates/user/user-tabs.html:5 #: templates/user/users-table.html:29 msgid "Problems" msgstr "Bài tập" -#: judge/views/problem.py:598 +#: judge/views/problem.py:655 msgid "Banned from submitting" msgstr "Bị cấm nộp bài" -#: judge/views/problem.py:599 +#: judge/views/problem.py:656 msgid "" "You have been declared persona non grata for this problem. You are " "permanently barred from submitting this problem." msgstr "Bạn đã bị cấm nộp bài này." -#: judge/views/problem.py:613 +#: judge/views/problem.py:670 msgid "Too many submissions" msgstr "Quá nhiều lần nộp" -#: judge/views/problem.py:614 +#: judge/views/problem.py:671 msgid "You have exceeded the submission limit for this problem." msgstr "Bạn đã vượt quá số lần nộp cho bài này." -#: judge/views/problem.py:674 judge/views/problem.py:677 +#: judge/views/problem.py:731 judge/views/problem.py:734 #, python-format msgid "Submit to %(problem)s" msgstr "Nộp bài cho %(problem)s" -#: judge/views/problem.py:692 +#: judge/views/problem.py:749 msgid "Clone Problem" msgstr "Nhân bản bài tập" @@ -2992,7 +3020,7 @@ msgid "Updated on site" msgstr "Được cập nhật trên web" #: judge/views/user.py:326 templates/admin/auth/user/change_form.html:14 -#: templates/admin/auth/user/change_form.html:17 templates/base.html:297 +#: templates/admin/auth/user/change_form.html:17 templates/base.html:299 #: templates/user/user-tabs.html:10 msgid "Edit profile" msgstr "Chỉnh sửa thông tin" @@ -3177,15 +3205,22 @@ msgstr "Ngắt kết nối" msgid "Terminate" msgstr "Dừng" -#: templates/admin/judge/problem/change_form.html:14 +#: templates/admin/judge/problem/change_form.html:15 msgid "View Submissions" msgstr "Xem Bài Nộp" -#: templates/admin/judge/problem/change_form.html:17 +#: templates/admin/judge/problem/change_form.html:18 #: templates/user/user-base.html:112 msgid "View submissions" msgstr "Xem bài nộp" +#: templates/admin/judge/problem/change_form.html:19 +#: templates/admin/judge/problem/change_form.html:22 +#, fuzzy +#| msgid "View on site" +msgid "View votes" +msgstr "Xem trên trang" + #: templates/admin/judge/profile/change_form.html:14 #: templates/admin/judge/profile/change_form.html:17 msgid "Edit user" @@ -3197,20 +3232,20 @@ msgstr "Chỉnh sửa thông tin" msgid "Rejudge" msgstr "Chấm lại" -#: templates/base.html:266 templates/chat/chat.html:566 +#: templates/base.html:268 templates/chat/chat.html:573 msgid "Chat" msgstr "Chat" -#: templates/base.html:272 +#: templates/base.html:274 msgid "Notification" msgstr "Thông báo" -#: templates/base.html:289 +#: templates/base.html:291 #, python-format msgid "Hello, %(username)s." msgstr "Xin chào, %(username)s." -#: templates/base.html:295 templates/chat/chat.html:20 +#: templates/base.html:297 templates/chat/chat.html:20 #: templates/comments/list.html:89 templates/contest/contest-list-tabs.html:24 #: templates/contest/ranking-table.html:53 #: templates/problem/problem-list-tabs.html:6 @@ -3219,36 +3254,36 @@ msgstr "Xin chào, %(username)s." msgid "Admin" msgstr "" -#: templates/base.html:304 +#: templates/base.html:306 msgid "Log out" msgstr "Đăng xuất" -#: templates/base.html:313 +#: templates/base.html:315 #: templates/registration/password_reset_complete.html:4 msgid "Log in" msgstr "Đăng nhập" -#: templates/base.html:314 templates/registration/registration_form.html:177 +#: templates/base.html:316 templates/registration/registration_form.html:177 msgid "or" msgstr "hoặc" -#: templates/base.html:315 +#: templates/base.html:317 msgid "Sign up" msgstr "Đăng ký" -#: templates/base.html:331 +#: templates/base.html:333 msgid "spectating" msgstr "đang theo dõi" -#: templates/base.html:343 +#: templates/base.html:345 msgid "Compete" msgstr "Thi" -#: templates/base.html:345 +#: templates/base.html:347 msgid "General" msgstr "Chung" -#: templates/base.html:352 +#: templates/base.html:354 msgid "This site works best with JavaScript enabled." msgstr "" @@ -3292,7 +3327,7 @@ msgid "News" msgstr "Tin tức" #: templates/blog/list.html:115 templates/problem/list.html:347 -#: templates/problem/problem.html:364 +#: templates/problem/problem.html:370 msgid "Clarifications" msgstr "Thông báo" @@ -3301,7 +3336,7 @@ msgid "Add" msgstr "Thêm mới" #: templates/blog/list.html:140 templates/problem/list.html:369 -#: templates/problem/problem.html:375 +#: templates/problem/problem.html:381 msgid "No clarifications have been made at this time." msgstr "Không có thông báo nào." @@ -3349,24 +3384,24 @@ msgstr "Thành viên khác" msgid "New message(s)" msgstr "Tin nhắn mới" -#: templates/chat/chat.html:507 templates/chat/chat.html:588 +#: templates/chat/chat.html:514 templates/chat/chat.html:595 #: templates/user/base-users.html:14 templates/user/base-users.html:80 msgid "Search by handle..." msgstr "Tìm kiếm theo tên..." -#: templates/chat/chat.html:568 templates/chat/chat.html:575 +#: templates/chat/chat.html:575 templates/chat/chat.html:582 msgid "Online Users" msgstr "Trực tuyến" -#: templates/chat/chat.html:576 +#: templates/chat/chat.html:583 msgid "Refresh" msgstr "Làm mới" -#: templates/chat/chat.html:609 +#: templates/chat/chat.html:616 msgid "Emoji" msgstr "" -#: templates/chat/chat.html:610 +#: templates/chat/chat.html:617 msgid "Enter your message" msgstr "Nhập tin nhắn" @@ -3563,8 +3598,8 @@ msgstr "Lịch" msgid "Info" msgstr "Thông tin" -#: templates/contest/contest-tabs.html:6 templates/stats/base.html:9 -#: templates/submission/list.html:339 +#: templates/contest/contest-tabs.html:6 templates/problem/voting-stats.html:26 +#: templates/stats/base.html:9 templates/submission/list.html:339 msgid "Statistics" msgstr "Thống kê" @@ -3845,27 +3880,27 @@ msgstr "Bạn có chắc muốn hủy kết quả này?" msgid "Are you sure you want to un-disqualify this participation?" msgstr "Bạn có chắc muốn khôi phục kết quả này?" -#: templates/contest/ranking.html:415 +#: templates/contest/ranking.html:446 msgid "View user participation" msgstr "Xem các lần tham gia" -#: templates/contest/ranking.html:419 +#: templates/contest/ranking.html:450 msgid "Show organizations" msgstr "Hiển thị tổ chức" -#: templates/contest/ranking.html:423 +#: templates/contest/ranking.html:454 msgid "Show full name" msgstr "Hiển thị họ tên" -#: templates/contest/ranking.html:426 +#: templates/contest/ranking.html:457 msgid "Show friends only" msgstr "Chỉ hiển thị bạn bè" -#: templates/contest/ranking.html:429 +#: templates/contest/ranking.html:460 msgid "Total score only" msgstr "Chỉ hiển thị tổng điểm" -#: templates/contest/ranking.html:431 +#: templates/contest/ranking.html:462 msgid "Show virtual participation" msgstr "Hiển thị tham gia ảo" @@ -4415,16 +4450,16 @@ msgstr[0] "Máy chấm:" msgid "none available" msgstr "không có sẵn" -#: templates/problem/problem.html:328 +#: templates/problem/problem.html:331 #, python-format msgid "This problem has %(length)s clarification(s)" msgstr "Bài này có %(length)s thông báo" -#: templates/problem/problem.html:353 +#: templates/problem/problem.html:359 msgid "Request clarification" msgstr "Yêu cầu làm rõ đề" -#: templates/problem/problem.html:355 +#: templates/problem/problem.html:361 msgid "Report an issue" msgstr "Báo cáo một vấn đề" @@ -4522,6 +4557,50 @@ msgstr "Không có máy chấm có thể chấm bài này." msgid "Submit!" msgstr "Nộp bài!" +#: templates/problem/voting-controls.html:53 +msgid "Edit difficulty" +msgstr "Thay đổi độ khó" + +#: templates/problem/voting-controls.html:61 +msgid "Vote difficulty" +msgstr "Bình chọn độ khó" + +#: templates/problem/voting-form.html:21 +msgid "How difficult is this problem?" +msgstr "Bạn thấy độ khó bài này thế nào?" + +#: templates/problem/voting-form.html:35 +msgid "This helps us improve the site" +msgstr "Bình chọn giúp admin cải thiện bài tập." + +#: templates/problem/voting-form.html:38 +msgid "Easy" +msgstr "Dễ" + +#: templates/problem/voting-form.html:39 +msgid "Hard" +msgstr "Khó" + +#: templates/problem/voting-stats.html:29 +msgid "Voting Statistics" +msgstr "Thống kê" + +#: templates/problem/voting-stats.html:32 +msgid "No Votes Available!" +msgstr "Không có bình chọn nào!" + +#: templates/problem/voting-stats.html:35 +msgid "Median:" +msgstr "Trung vị:" + +#: templates/problem/voting-stats.html:37 +msgid "Mean:" +msgstr "Trung bình:" + +#: templates/problem/voting-stats.html:39 templates/submission/list.html:345 +msgid "Total:" +msgstr "Tổng:" + #: templates/registration/activate.html:3 #, python-format msgid "%(key)s is an invalid activation key." @@ -4779,10 +4858,6 @@ msgstr "Lọc theo kết quả..." msgid "Filter by language..." msgstr "Lọc theo ngôn ngữ..." -#: templates/submission/list.html:345 -msgid "Total:" -msgstr "Tổng:" - #: templates/submission/list.html:355 msgid "You were disconnected. Refresh to show latest updates." msgstr "Bạn bị ngắt kết nối. Hãy làm mới để xem cập nhật mới nhất." diff --git a/templates/admin/judge/problem/change_form.html b/templates/admin/judge/problem/change_form.html index 4d118f0..8731e7f 100644 --- a/templates/admin/judge/problem/change_form.html +++ b/templates/admin/judge/problem/change_form.html @@ -5,6 +5,7 @@ {% endblock extrahead %} @@ -15,6 +16,10 @@ href="{% url 'admin:judge_submission_changelist' %}?problem__code={{ original.code }}"> {% trans "View submissions" %} + {% endif %} {% endblock %} diff --git a/templates/problem/problem.html b/templates/problem/problem.html index a3719a0..c37ef86 100644 --- a/templates/problem/problem.html +++ b/templates/problem/problem.html @@ -164,7 +164,7 @@ {% endif %} - + {% if not contest_problem or not contest_problem.contest.use_clarifications %}
@@ -321,6 +321,9 @@ {% endblock %} {% block description %} + {% if can_vote and not vote %} + {% include 'problem/voting-form.html' %} + {% endif %} {% if contest_problem and contest_problem.contest.use_clarifications and has_clarifications %}
@@ -346,6 +349,9 @@ {% endblock %} {% block post_description_end %} + {% if can_vote %} + {% include 'problem/voting-controls.html' %} + {% endif %} {% if request.user.is_authenticated and not request.profile.mute %} diff --git a/templates/problem/voting-controls.html b/templates/problem/voting-controls.html new file mode 100644 index 0000000..0d0d4ab --- /dev/null +++ b/templates/problem/voting-controls.html @@ -0,0 +1,73 @@ + + +{% if can_vote %} + + + + {% if request.user.is_superuser %} + - {% include 'problem/voting-stats.html' %} + {% endif %} + + + + +{% endif %} \ No newline at end of file diff --git a/templates/problem/voting-form.html b/templates/problem/voting-form.html new file mode 100644 index 0000000..0ed939e --- /dev/null +++ b/templates/problem/voting-form.html @@ -0,0 +1,93 @@ + +
+ {% csrf_token %} + + + + + + + + + + + + +
+ {{_('How difficult is this problem?')}} + +
+ + {% for i in range(1, (max_possible_vote - min_possible_vote) // 100 + 2) %} + + {% endfor %} + +
+
+ {{_('This helps us improve the site')}} + + {{_('Easy')}} + {{_('Hard')}} +
+
+ +
+
+ +
+
+ \ No newline at end of file diff --git a/templates/problem/voting-stats.html b/templates/problem/voting-stats.html new file mode 100644 index 0000000..57acd41 --- /dev/null +++ b/templates/problem/voting-stats.html @@ -0,0 +1,146 @@ + +{{ _('Statistics') }} + + + \ No newline at end of file From 573fb7a954e33478a8df7dd03ec3271d5695df05 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 10 Mar 2022 11:27:52 -0600 Subject: [PATCH 004/959] Update problem vote --- judge/admin/problem.py | 22 +++++++++++++++++----- templates/problem/voting-form.html | 4 ++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 3e92e74..3c4e3fc 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -239,14 +239,26 @@ class ProblemAdmin(VersionAdmin): class ProblemPointsVoteAdmin(admin.ModelAdmin): - list_display = ('points', 'voter', 'problem', 'vote_time') - search_fields = ('voter', 'problem') - readonly_fields = ('voter', 'problem', 'vote_time') + list_display = ('points', 'voter', 'problem', 'problem_code', 'problem_points', 'vote_time') + search_fields = ('voter__user__username', 'problem__code',) + readonly_fields = ('voter', 'problem', 'problem_code', 'problem_points', 'vote_time') def has_change_permission(self, request, obj=None): if obj is None: return request.user.has_perm('judge.edit_own_problem') return obj.problem.is_editable_by(request.user) - def lookup_allowed(self, key, value): - return super().lookup_allowed(key, value) or key in ('problem__code',) \ No newline at end of file + # def lookup_allowed(self, key, value): + # return key in ('problem') + + def problem_code(self, obj): + return obj.problem.code + + problem_code.short_description = 'Problem Code' + problem_code.admin_order_field = 'problem__code' + + def problem_points(self, obj): + return obj.problem.points + + problem_points.short_description = 'Problem Points' + problem_points.admin_order_field = 'problem__points' \ No newline at end of file diff --git a/templates/problem/voting-form.html b/templates/problem/voting-form.html index 0ed939e..735b781 100644 --- a/templates/problem/voting-form.html +++ b/templates/problem/voting-form.html @@ -35,8 +35,8 @@ {{_('This helps us improve the site')}} - {{_('Easy')}} - {{_('Hard')}} + {{min_possible_vote}} + {{max_possible_vote}} From bc40d293d67e49a33c374258bc51ab3865bdb868 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 10 Mar 2022 11:37:18 -0600 Subject: [PATCH 005/959] Update problem vote --- judge/admin/problem.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 3c4e3fc..716e9eb 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -239,8 +239,8 @@ class ProblemAdmin(VersionAdmin): class ProblemPointsVoteAdmin(admin.ModelAdmin): - list_display = ('points', 'voter', 'problem', 'problem_code', 'problem_points', 'vote_time') - search_fields = ('voter__user__username', 'problem__code',) + list_display = ('vote_points', 'voter', 'problem_name', 'problem_code', 'problem_points', 'vote_time') + search_fields = ('voter__user__username', 'problem__code', 'problem__name') readonly_fields = ('voter', 'problem', 'problem_code', 'problem_points', 'vote_time') def has_change_permission(self, request, obj=None): @@ -248,17 +248,21 @@ class ProblemPointsVoteAdmin(admin.ModelAdmin): return request.user.has_perm('judge.edit_own_problem') return obj.problem.is_editable_by(request.user) - # def lookup_allowed(self, key, value): - # return key in ('problem') - def problem_code(self, obj): return obj.problem.code - - problem_code.short_description = 'Problem Code' + problem_code.short_description = _('Problem code') problem_code.admin_order_field = 'problem__code' def problem_points(self, obj): return obj.problem.points + problem_points.short_description = _('Points') + problem_points.admin_order_field = 'problem__points' - problem_points.short_description = 'Problem Points' - problem_points.admin_order_field = 'problem__points' \ No newline at end of file + def problem_name(self, obj): + return obj.problem.name + problem_name.short_description = _('Problem name') + problem_name.admin_order_field = 'problem__name' + + def vote_points(self, obj): + return obj.points + vote_points.short_description = _('Vote') \ No newline at end of file From 3bf3754baf0e0074077b72b6696c6bf5888b443a Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 10 Mar 2022 22:30:58 -0600 Subject: [PATCH 006/959] Exclude ignored users in chat --- chat_box/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chat_box/views.py b/chat_box/views.py index f64fb15..49e5a4d 100644 --- a/chat_box/views.py +++ b/chat_box/views.py @@ -438,9 +438,12 @@ def get_unread_boxes(request): if (request.method != 'GET'): return HttpResponseBadRequest() + ignored_users = Ignore.get_ignored_users(request.profile) + mess = Message.objects.filter(room=OuterRef('room'), time__gte=OuterRef('last_seen'))\ .exclude(author=request.profile)\ + .exclude(author__in=ignored_users)\ .order_by().values('room')\ .annotate(unread_count=Count('pk')).values('unread_count') From 6db0d0dcfb48c75336443ff4efa42069808b9c0b Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 10 Mar 2022 23:48:04 -0600 Subject: [PATCH 007/959] Allow admin to see voting stat --- templates/problem/voting-controls.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/problem/voting-controls.html b/templates/problem/voting-controls.html index 0d0d4ab..1f93193 100644 --- a/templates/problem/voting-controls.html +++ b/templates/problem/voting-controls.html @@ -25,12 +25,14 @@ } -{% if can_vote %} +{% if can_vote or request.user.is_superuser %} - + {% if can_vote %} + + {% endif %} {% if request.user.is_superuser %} - {% include 'problem/voting-stats.html' %} {% endif %} From ca3133c17de62dc1b2a53ac9c9e128053c5a4bd1 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 10 Mar 2022 23:48:04 -0600 Subject: [PATCH 008/959] Allow admin to see voting stat --- templates/problem/problem.html | 2 +- templates/problem/voting-controls.html | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/problem/problem.html b/templates/problem/problem.html index c37ef86..ab89be7 100644 --- a/templates/problem/problem.html +++ b/templates/problem/problem.html @@ -349,7 +349,7 @@ {% endblock %} {% block post_description_end %} - {% if can_vote %} + {% if can_vote or request.user.is_superuser %} {% include 'problem/voting-controls.html' %} {% endif %} {% if request.user.is_authenticated and not request.profile.mute %} diff --git a/templates/problem/voting-controls.html b/templates/problem/voting-controls.html index 0d0d4ab..1f93193 100644 --- a/templates/problem/voting-controls.html +++ b/templates/problem/voting-controls.html @@ -25,12 +25,14 @@ } -{% if can_vote %} +{% if can_vote or request.user.is_superuser %} - + {% if can_vote %} + + {% endif %} {% if request.user.is_superuser %} - {% include 'problem/voting-stats.html' %} {% endif %} From cb7e4559e4103453dc0a9b50074e8e38e54f3968 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 11 Mar 2022 22:34:32 -0600 Subject: [PATCH 009/959] Update problem vote --- judge/models/problem.py | 5 +++-- judge/views/problem.py | 15 ++++++--------- templates/problem/voting-form.html | 2 +- templates/problem/voting-stats.html | 28 ++++++++++++++-------------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/judge/models/problem.py b/judge/models/problem.py index 28dd102..f9e10ec 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -375,12 +375,13 @@ class Problem(models.Model): save.alters_data = True - def can_vote(self, user): + def can_vote(self, request): + user = request.user if not user.is_authenticated: return False # If the user is in contest, nothing should be shown. - if user.profile.current_contest: + if request.in_contest_mode: return False # If the user is not allowed to vote diff --git a/judge/views/problem.py b/judge/views/problem.py index 78d538f..65ab3bf 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -217,7 +217,7 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, DetailView): context['meta_description'] = self.object.summary or metadata[0] context['og_image'] = self.object.og_image or metadata[1] - context['can_vote'] = self.object.can_vote(user) + context['can_vote'] = self.object.can_vote(self.request) if context['can_vote']: try: context['vote'] = ProblemPointsVote.objects.get(voter=user.profile, problem=self.object) @@ -226,14 +226,11 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, DetailView): else: context['vote'] = None - all_votes = list(self.object.problem_points_votes.order_by('points').values_list('points', flat=True)) - - context['has_votes'] = len(all_votes) > 0 - - # If the user is not currently in contest. - if not user.is_authenticated or user.profile.current_contest is None: + if user.is_superuser: + all_votes = list(self.object.problem_points_votes.order_by('points').values_list('points', flat=True)) context['all_votes'] = all_votes - + + context['has_votes'] = len(all_votes) > 0 context['max_possible_vote'] = 600 context['min_possible_vote'] = 100 return context @@ -259,7 +256,7 @@ class Vote(ProblemMixin, SingleObjectMixin, View): def post(self, request, *args, **kwargs): self.object = self.get_object() - if not self.object.can_vote(request.user): # Not allowed to vote for some reason. + if not self.object.can_vote(request): # Not allowed to vote for some reason. return HttpResponseForbidden('Not allowed to vote on this problem.', content_type='text/plain') form = ProblemPointsVoteForm(request.POST) diff --git a/templates/problem/voting-form.html b/templates/problem/voting-form.html index 735b781..0dc2fd3 100644 --- a/templates/problem/voting-form.html +++ b/templates/problem/voting-form.html @@ -73,7 +73,7 @@ // Forms are auto disabled to prevent resubmission, but we need to allow resubmission here. $('#id_vote_form_submit_button').removeAttr('disabled'); var current = $.featherlight.current(); - current.close(); + if (current) current.close(); }, error: function (data) { let errors = data.responseJSON; diff --git a/templates/problem/voting-stats.html b/templates/problem/voting-stats.html index 57acd41..c37896e 100644 --- a/templates/problem/voting-stats.html +++ b/templates/problem/voting-stats.html @@ -43,12 +43,12 @@ {% endblock %} @@ -90,20 +104,50 @@
- {% endif %} - - {% if own_open_tickets %} - - {% endif %} - - {% if open_tickets %} - - {% endif %} - + {% else %} +
+ {% include "comments/list.html" %} +
{% endif %} {% endblock %} diff --git a/templates/ticket/feed.html b/templates/ticket/feed.html new file mode 100644 index 0000000..a3f2af3 --- /dev/null +++ b/templates/ticket/feed.html @@ -0,0 +1,23 @@ +
+

+ + {{ ticket.title }} + +

+ {% with author=ticket.user %} + {% if author %} +
+ + {{ link_user(author) }} +
+ {% endif %} + {% endwith %} + +
+ {{ ticket.messages.last().body |markdown("ticket", MATH_ENGINE)|reference|str|safe }} +
+
\ No newline at end of file From 3d3ab23d270bfc6a409ac815b6302db9b65e5021 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 21 Mar 2022 16:09:16 -0500 Subject: [PATCH 013/959] New home UI --- dmoj/urls.py | 6 +- judge/models/comment.py | 6 +- judge/views/blog.py | 149 +++++++++----- judge/views/problem.py | 67 ++++-- locale/vi/LC_MESSAGES/django.po | 347 ++++++++++++++++++-------------- resources/base.scss | 4 +- resources/blog.scss | 100 ++++++++- templates/blog/content.html | 22 +- templates/blog/list.html | 201 +++++++++--------- templates/chat/chat_css.html | 2 +- templates/comments/feed.html | 18 ++ templates/problem/comments.html | 28 --- templates/problem/feed.html | 28 +++ templates/problem/problem.html | 51 ++++- templates/ticket/feed.html | 25 +++ 15 files changed, 676 insertions(+), 378 deletions(-) create mode 100644 templates/comments/feed.html delete mode 100644 templates/problem/comments.html create mode 100644 templates/problem/feed.html create mode 100644 templates/ticket/feed.html diff --git a/dmoj/urls.py b/dmoj/urls.py index 4869ab6..c691074 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -110,13 +110,17 @@ urlpatterns = [ url(r'^accounts/', include(register_patterns)), url(r'^', include('social_django.urls')), + url(r'^feed/', include([ + url(r'^problems/$', problem.ProblemFeed.as_view(), name='problem_feed'), + url(r'^tickets/$', blog.TicketFeed.as_view(), name='ticket_feed'), + url(r'^comments/$', blog.CommentFeed.as_view(), name='comment_feed'), + ])), url(r'^problems/$', problem.ProblemList.as_view(), name='problem_list'), url(r'^problems/random/$', problem.RandomProblem.as_view(), name='problem_random'), url(r'^problem/(?P[^/]+)', include([ url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'), url(r'^/editorial$', problem.ProblemSolution.as_view(), name='problem_editorial'), - url(r'^/comments$', problem.ProblemComments.as_view(), name='problem_comments'), url(r'^/raw$', problem.ProblemRaw.as_view(), name='problem_raw'), url(r'^/pdf$', problem.ProblemPdfView.as_view(), name='problem_pdf'), url(r'^/pdf/(?P[a-z-]+)$', problem.ProblemPdfView.as_view(), name='problem_pdf'), diff --git a/judge/models/comment.py b/judge/models/comment.py index a58ab1d..5d26500 100644 --- a/judge/models/comment.py +++ b/judge/models/comment.py @@ -65,7 +65,9 @@ class Comment(MPTTModel): problem_access = CacheDict(lambda code: Problem.objects.get(code=code).is_accessible_by(user)) contest_access = CacheDict(lambda key: Contest.objects.get(key=key).is_accessible_by(user)) blog_access = CacheDict(lambda id: BlogPost.objects.get(id=id).can_see(user)) - + + if n == -1: + n = len(queryset) if user.is_superuser: return queryset[:n] if batch is None: @@ -105,7 +107,7 @@ class Comment(MPTTModel): try: link = None if self.page.startswith('p:'): - link = reverse('problem_comments', args=(self.page[2:],)) + link = reverse('problem_detail', args=(self.page[2:],)) elif self.page.startswith('c:'): link = reverse('contest_view', args=(self.page[2:],)) elif self.page.startswith('b:'): diff --git a/judge/views/blog.py b/judge/views/blog.py index c16aa6e..b7855dd 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -17,10 +17,8 @@ from judge.utils.tickets import filter_visible_tickets from judge.utils.views import TitleMixin -class PostList(ListView): - model = BlogPost - paginate_by = 10 - context_object_name = 'posts' +# General view for all content list on home feed +class FeedView(ListView): template_name = 'blog/list.html' title = None @@ -29,6 +27,56 @@ class PostList(ListView): return DiggPaginator(queryset, per_page, body=6, padding=2, orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs) + def get_context_data(self, **kwargs): + context = super(FeedView, self).get_context_data(**kwargs) + context['has_clarifications'] = False + if self.request.user.is_authenticated: + participation = self.request.profile.current_contest + if participation: + clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all()) + context['has_clarifications'] = clarifications.count() > 0 + context['clarifications'] = clarifications.order_by('-date') + if participation.contest.is_editable_by(self.request.user): + context['can_edit_contest'] = True + + context['page_titles'] = CacheDict(lambda page: Comment.get_page_title(page)) + + context['user_count'] = lazy(Profile.objects.count, int, int) + context['problem_count'] = lazy(Problem.objects.filter(is_public=True).count, int, int) + context['submission_count'] = lazy(Submission.objects.count, int, int) + context['language_count'] = lazy(Language.objects.count, int, int) + + now = timezone.now() + + # Dashboard stuff + # if self.request.user.is_authenticated: + # user = self.request.profile + # context['recently_attempted_problems'] = (Submission.objects.filter(user=user) + # .exclude(problem__in=user_completed_ids(user)) + # .values_list('problem__code', 'problem__name', 'problem__points') + # .annotate(points=Max('points'), latest=Max('date')) + # .order_by('-latest') + # [:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT]) + + visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) \ + .order_by('start_time') + + context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now) + context['future_contests'] = visible_contests.filter(start_time__gt=now) + + visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) + + context['top_rated'] = Profile.objects.order_by('-rating')[:10] + context['top_scorer'] = Profile.objects.order_by('-performance_points')[:10] + + return context + + +class PostList(FeedView): + model = BlogPost + paginate_by = 10 + context_object_name = 'posts' + def get_queryset(self): queryset = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) \ .order_by('-sticky', '-publish_on') \ @@ -45,25 +93,7 @@ class PostList(ListView): context['title'] = self.title or _('Page %d of Posts') % context['page_obj'].number context['first_page_href'] = reverse('home') context['page_prefix'] = reverse('blog_post_list') - context['comments'] = Comment.most_recent(self.request.user, 25) - context['new_problems'] = Problem.objects.filter(is_public=True, is_organization_private=False) \ - .order_by('-date', '-id')[:settings.DMOJ_BLOG_NEW_PROBLEM_COUNT] - context['page_titles'] = CacheDict(lambda page: Comment.get_page_title(page)) - - context['has_clarifications'] = False - if self.request.user.is_authenticated: - participation = self.request.profile.current_contest - if participation: - clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all()) - context['has_clarifications'] = clarifications.count() > 0 - context['clarifications'] = clarifications.order_by('-date') - if participation.contest.is_editable_by(self.request.user): - context['can_edit_contest'] = True - context['user_count'] = lazy(Profile.objects.count, int, int) - context['problem_count'] = lazy(Problem.objects.filter(is_public=True).count, int, int) - context['submission_count'] = lazy(Submission.objects.count, int, int) - context['language_count'] = lazy(Language.objects.count, int, int) - + context['feed_type'] = 'blog' context['post_comment_counts'] = { int(page[2:]): count for page, count in Comment.objects @@ -71,40 +101,55 @@ class PostList(ListView): .values_list('page').annotate(count=Count('page')).order_by() } - now = timezone.now() + return context - # Dashboard stuff - if self.request.user.is_authenticated: - user = self.request.profile - context['recently_attempted_problems'] = (Submission.objects.filter(user=user) - .exclude(problem__in=user_completed_ids(user)) - .values_list('problem__code', 'problem__name', 'problem__points') - .annotate(points=Max('points'), latest=Max('date')) - .order_by('-latest') - [:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT]) - - visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) \ - .order_by('start_time') - context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now) - context['future_contests'] = visible_contests.filter(start_time__gt=now) +class TicketFeed(FeedView): + model = Ticket + context_object_name = 'tickets' + paginate_by = 30 - visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) - if self.request.user.is_authenticated: - profile = self.request.profile - context['own_open_tickets'] = (Ticket.objects.filter(Q(user=profile) | Q(assignees__in=[profile]), is_open=True).order_by('-id') - .prefetch_related('linked_item').select_related('user__user')) + def get_queryset(self, is_own=True): + profile = self.request.profile + if is_own: + if self.request.user.is_authenticated: + return (Ticket.objects.filter(Q(user=profile) | Q(assignees__in=[profile]), is_open=True).order_by('-id') + .prefetch_related('linked_item').select_related('user__user')) + else: + return [] else: - profile = None - context['own_open_tickets'] = [] + # Superusers better be staffs, not the spell-casting kind either. + if self.request.user.is_staff: + tickets = (Ticket.objects.order_by('-id').filter(is_open=True).prefetch_related('linked_item') + .select_related('user__user')) + return filter_visible_tickets(tickets, self.request.user, profile) + else: + return [] - # Superusers better be staffs, not the spell-casting kind either. - if self.request.user.is_staff: - tickets = (Ticket.objects.order_by('-id').filter(is_open=True).prefetch_related('linked_item') - .select_related('user__user')) - context['open_tickets'] = filter_visible_tickets(tickets, self.request.user, profile)[:10] - else: - context['open_tickets'] = [] + def get_context_data(self, **kwargs): + context = super(TicketFeed, self).get_context_data(**kwargs) + context['feed_type'] = 'ticket' + context['first_page_href'] = self.request.path + context['page_prefix'] = '?page=' + context['title'] = _('Ticket feed') + + return context + + +class CommentFeed(FeedView): + model = Comment + context_object_name = 'comments' + paginate_by = 50 + + def get_queryset(self): + return Comment.most_recent(self.request.user, 1000) + + def get_context_data(self, **kwargs): + context = super(CommentFeed, self).get_context_data(**kwargs) + context['feed_type'] = 'comment' + context['first_page_href'] = self.request.path + context['page_prefix'] = '?page=' + context['title'] = _('Comment feed') return context diff --git a/judge/views/problem.py b/judge/views/problem.py index 9149511..995c14e 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -21,7 +21,7 @@ from django.utils.functional import cached_property from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext as _, gettext_lazy -from django.views.generic import DetailView, ListView, View +from django.views.generic import ListView, View from django.views.generic.base import TemplateResponseMixin from django.views.generic.detail import SingleObjectMixin @@ -38,6 +38,7 @@ from judge.utils.problems import contest_attempted_ids, contest_completed_ids, h from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.tickets import own_ticket_filter from judge.utils.views import QueryStringSortMixin, SingleObjectFormView, TitleMixin, generic_message +from judge.views.blog import FeedView def get_contest_problem(problem, profile): @@ -155,10 +156,13 @@ class ProblemRaw(ProblemMixin, TitleMixin, TemplateResponseMixin, SingleObjectMi )) -class ProblemDetail(ProblemMixin, SolvedProblemMixin, DetailView): +class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView): context_object_name = 'problem' template_name = 'problem/problem.html' + def get_comment_page(self): + return 'p:%s' % self.object.code + def get_context_data(self, **kwargs): context = super(ProblemDetail, self).get_context_data(**kwargs) user = self.request.user @@ -235,6 +239,7 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, DetailView): context['min_possible_vote'] = 100 return context + class DeleteVote(ProblemMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): return HttpResponseForbidden(status=405, content_type='text/plain') @@ -273,21 +278,6 @@ class Vote(ProblemMixin, SingleObjectMixin, View): return JsonResponse(form.errors, status=400) -class ProblemComments(ProblemMixin, TitleMixin, CommentedDetailView): - context_object_name = 'problem' - template_name = 'problem/comments.html' - - def get_title(self): - return _('Disscuss {0}').format(self.object.name) - - def get_content_title(self): - return format_html(_(u'Discuss {0}'), self.object.name, - reverse('problem_detail', args=[self.object.code])) - - def get_comment_page(self): - return 'p:%s' % self.object.code - - class LatexError(Exception): pass @@ -592,6 +582,49 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView return HttpResponseRedirect(request.get_full_path()) +class ProblemFeed(FeedView): + model = Problem + context_object_name = 'problems' + paginate_by = 50 + title = _('Problem feed') + + @cached_property + def profile(self): + if not self.request.user.is_authenticated: + return None + return self.request.profile + + def get_unsolved_queryset(self): + filter = Q(is_public=True) + if self.profile is not None: + filter |= Q(authors=self.profile) + filter |= Q(curators=self.profile) + filter |= Q(testers=self.profile) + queryset = Problem.objects.filter(filter).select_related('group').defer('description') + if not self.request.user.has_perm('see_organization_problem'): + filter = Q(is_organization_private=False) + if self.profile is not None: + filter |= Q(organizations__in=self.profile.organizations.all()) + queryset = queryset.filter(filter) + if self.profile is not None: + queryset = queryset.exclude(id__in=Submission.objects.filter(user=self.profile, points=F('problem__points')) + .values_list('problem__id', flat=True)) + return queryset.distinct() + + def get_queryset(self): + queryset = self.get_unsolved_queryset() + return queryset.order_by('?') + + def get_context_data(self, **kwargs): + context = super(ProblemFeed, self).get_context_data(**kwargs) + context['first_page_href'] = self.request.path + context['page_prefix'] = '?page=' + context['feed_type'] = 'problem' + context['title'] = self.title + + return context + + class LanguageTemplateAjax(View): def get(self, request, *args, **kwargs): try: diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 36f657d..c77acf1 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-03-10 12:30+0700\n" +"POT-Creation-Date: 2022-03-22 04:37+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -24,7 +24,7 @@ msgstr "" msgid "user" msgstr "người dùng" -#: chat_box/models.py:24 judge/models/comment.py:43 judge/models/comment.py:191 +#: chat_box/models.py:24 judge/models/comment.py:43 judge/models/comment.py:193 msgid "posted time" msgstr "thời gian đăng" @@ -225,7 +225,7 @@ msgstr "ảo" msgid "link path" msgstr "đường dẫn" -#: judge/admin/interface.py:65 +#: judge/admin/interface.py:65 templates/blog/list.html:114 msgid "Content" msgstr "Nội dung" @@ -254,11 +254,11 @@ msgstr "Mạng Xã Hội" msgid "Taxonomy" msgstr "" -#: judge/admin/problem.py:128 templates/contest/contest.html:84 -#: templates/problem/data.html:469 templates/problem/list.html:222 -#: templates/problem/list.html:248 templates/user/base-users-table.html:10 -#: templates/user/user-about.html:36 templates/user/user-about.html:52 -#: templates/user/user-problems.html:58 +#: judge/admin/problem.py:128 judge/admin/problem.py:258 +#: templates/contest/contest.html:84 templates/problem/data.html:469 +#: templates/problem/list.html:222 templates/problem/list.html:248 +#: templates/user/base-users-table.html:10 templates/user/user-about.html:36 +#: templates/user/user-about.html:52 templates/user/user-problems.html:58 msgid "Points" msgstr "Điểm" @@ -299,6 +299,19 @@ msgstr[0] "%d bài tập đã được đánh dấu riêng tư." msgid "Mark problems as private" msgstr "Đánh dấu các bài tập là riêng tư" +#: judge/admin/problem.py:253 judge/admin/submission.py:200 +#: templates/problem/list.html:216 templates/problem/list.html:236 +msgid "Problem code" +msgstr "Mã bài" + +#: judge/admin/problem.py:263 judge/admin/submission.py:205 +msgid "Problem name" +msgstr "Tên bài" + +#: judge/admin/problem.py:268 +msgid "Vote" +msgstr "" + #: judge/admin/profile.py:34 msgid "timezone" msgstr "múi giờ" @@ -401,15 +414,6 @@ msgstr[0] "%d bài nộp đã được tính điểm lại." msgid "Rescore the selected submissions" msgstr "Tính điểm lại cái bài nộp" -#: judge/admin/submission.py:200 templates/problem/list.html:216 -#: templates/problem/list.html:236 -msgid "Problem code" -msgstr "Mã bài" - -#: judge/admin/submission.py:205 -msgid "Problem name" -msgstr "Tên bài" - #: judge/admin/submission.py:215 templates/notification/list.html:15 #: templates/organization/requests/log.html:10 #: templates/organization/requests/pending.html:13 @@ -550,7 +554,7 @@ msgstr "g:i a j b, Y" msgid "{time}" msgstr "{time}" -#: judge/jinja2/datetime.py:26 templates/blog/content.html:13 +#: judge/jinja2/datetime.py:26 templates/blog/content.html:10 #, python-brace-format msgid "on {time}" msgstr "vào {time}" @@ -583,11 +587,11 @@ msgstr "Mã trang phải có dạng ^[pcs]:[a-z0-9]+$|^b:\\d+$" msgid "commenter" msgstr "người bình luận" -#: judge/models/comment.py:44 judge/models/comment.py:177 +#: judge/models/comment.py:44 judge/models/comment.py:179 msgid "associated page" msgstr "trang tương ứng" -#: judge/models/comment.py:46 judge/models/problem.py:492 +#: judge/models/comment.py:46 judge/models/problem.py:493 msgid "votes" msgstr "bình chọn" @@ -599,7 +603,7 @@ msgstr "ẩn bình luận" msgid "parent" msgstr "" -#: judge/models/comment.py:54 judge/models/comment.py:192 +#: judge/models/comment.py:54 judge/models/comment.py:194 msgid "comment" msgstr "bình luận" @@ -607,24 +611,24 @@ msgstr "bình luận" msgid "comments" msgstr "" -#: judge/models/comment.py:137 judge/models/problem.py:462 +#: judge/models/comment.py:139 judge/models/problem.py:463 #, python-format msgid "Editorial for %s" msgstr "" -#: judge/models/comment.py:172 +#: judge/models/comment.py:174 msgid "comment vote" msgstr "" -#: judge/models/comment.py:173 +#: judge/models/comment.py:175 msgid "comment votes" msgstr "" -#: judge/models/comment.py:182 +#: judge/models/comment.py:184 msgid "Override comment lock" msgstr "" -#: judge/models/comment.py:190 +#: judge/models/comment.py:192 #: src/dmoj-wpadmin/test_project/apps/books/admin.py:24 #: src/dmoj-wpadmin/test_project/apps/books/models.py:30 #: src/dmoj-wpadmin/test_project/apps/cds/models.py:30 @@ -632,22 +636,22 @@ msgstr "" msgid "owner" msgstr "" -#: judge/models/comment.py:193 judge/models/message.py:16 +#: judge/models/comment.py:195 judge/models/message.py:16 msgid "read" msgstr "" -#: judge/models/comment.py:194 +#: judge/models/comment.py:196 #: src/dmoj-wpadmin/test_project/apps/books/models.py:28 #: src/dmoj-wpadmin/test_project/apps/cds/models.py:28 #: src/dmoj-wpadmin/test_project/apps/dvds/models.py:28 msgid "category" msgstr "" -#: judge/models/comment.py:195 +#: judge/models/comment.py:197 msgid "html link to comments, used for non-comments" msgstr "" -#: judge/models/comment.py:196 +#: judge/models/comment.py:198 msgid "who trigger, used for non-comment" msgstr "" @@ -725,7 +729,7 @@ msgstr "" msgid "description" msgstr "mô tả" -#: judge/models/contest.py:72 judge/models/problem.py:409 +#: judge/models/contest.py:72 judge/models/problem.py:410 #: judge/models/runtime.py:138 msgid "problems" msgstr "bài tập" @@ -739,7 +743,7 @@ msgid "end time" msgstr "thời gian kết thúc" #: judge/models/contest.py:75 judge/models/problem.py:118 -#: judge/models/problem.py:433 +#: judge/models/problem.py:434 msgid "time limit" msgstr "giới hạn thời gian" @@ -1037,8 +1041,8 @@ msgid "contest participations" msgstr "lần tham gia kỳ thi" #: judge/models/contest.py:491 judge/models/contest.py:513 -#: judge/models/contest.py:554 judge/models/problem.py:408 -#: judge/models/problem.py:413 judge/models/problem.py:431 +#: judge/models/contest.py:554 judge/models/problem.py:409 +#: judge/models/problem.py:414 judge/models/problem.py:432 #: judge/models/problem_data.py:40 msgid "problem" msgstr "bài tập" @@ -1176,7 +1180,7 @@ msgstr "mục cha" msgid "post title" msgstr "tiêu đề bài đăng" -#: judge/models/interface.py:67 judge/models/problem.py:451 +#: judge/models/interface.py:67 judge/models/problem.py:452 msgid "authors" msgstr "tác giả" @@ -1184,7 +1188,7 @@ msgstr "tác giả" msgid "slug" msgstr "slug" -#: judge/models/interface.py:69 judge/models/problem.py:449 +#: judge/models/interface.py:69 judge/models/problem.py:450 msgid "public visibility" msgstr "khả năng hiển thị công khai" @@ -1380,7 +1384,7 @@ msgid "" "are supported." msgstr "" -#: judge/models/problem.py:123 judge/models/problem.py:436 +#: judge/models/problem.py:123 judge/models/problem.py:437 msgid "memory limit" msgstr "" @@ -1453,82 +1457,82 @@ msgstr "" msgid "If private, only these organizations may see the problem." msgstr "" -#: judge/models/problem.py:414 judge/models/problem.py:432 +#: judge/models/problem.py:415 judge/models/problem.py:433 #: judge/models/runtime.py:111 msgid "language" msgstr "" -#: judge/models/problem.py:415 +#: judge/models/problem.py:416 msgid "translated name" msgstr "" -#: judge/models/problem.py:416 +#: judge/models/problem.py:417 msgid "translated description" msgstr "" -#: judge/models/problem.py:420 +#: judge/models/problem.py:421 msgid "problem translation" msgstr "" -#: judge/models/problem.py:421 +#: judge/models/problem.py:422 msgid "problem translations" msgstr "" -#: judge/models/problem.py:425 +#: judge/models/problem.py:426 msgid "clarified problem" msgstr "" -#: judge/models/problem.py:426 +#: judge/models/problem.py:427 msgid "clarification body" msgstr "" -#: judge/models/problem.py:427 +#: judge/models/problem.py:428 msgid "clarification timestamp" msgstr "" -#: judge/models/problem.py:442 +#: judge/models/problem.py:443 msgid "language-specific resource limit" msgstr "" -#: judge/models/problem.py:443 +#: judge/models/problem.py:444 msgid "language-specific resource limits" msgstr "" -#: judge/models/problem.py:447 +#: judge/models/problem.py:448 msgid "associated problem" msgstr "" -#: judge/models/problem.py:450 +#: judge/models/problem.py:451 msgid "publish date" msgstr "" -#: judge/models/problem.py:452 +#: judge/models/problem.py:453 msgid "editorial content" msgstr "nội dung lời giải" -#: judge/models/problem.py:468 +#: judge/models/problem.py:469 msgid "solution" msgstr "lời giải" -#: judge/models/problem.py:469 +#: judge/models/problem.py:470 msgid "solutions" msgstr "lời giải" -#: judge/models/problem.py:474 +#: judge/models/problem.py:475 #, fuzzy #| msgid "point value" msgid "proposed point value" msgstr "điểm" -#: judge/models/problem.py:475 +#: judge/models/problem.py:476 msgid "The amount of points you think this problem deserves." msgstr "" -#: judge/models/problem.py:485 +#: judge/models/problem.py:486 msgid "The time this vote was cast" msgstr "" -#: judge/models/problem.py:491 +#: judge/models/problem.py:492 msgid "vote" msgstr "" @@ -2376,11 +2380,23 @@ msgstr "Giới thiệu" msgid "Custom Checker Sample" msgstr "Hướng dẫn viết trình chấm" -#: judge/views/blog.py:45 +#: judge/views/blog.py:93 #, python-format msgid "Page %d of Posts" msgstr "Trang %d" +#: judge/views/blog.py:134 +#, fuzzy +#| msgid "Ticket title" +msgid "Ticket feed" +msgstr "Tiêu đề báo cáo" + +#: judge/views/blog.py:152 +#, fuzzy +#| msgid "Comment body" +msgid "Comment feed" +msgstr "Nội dung bình luận" + #: judge/views/comment.py:28 msgid "Messing around, are we?" msgstr "Messing around, are we?" @@ -2675,65 +2691,61 @@ msgstr "" msgid "The user you are trying to kick is not in organization: %s." msgstr "" -#: judge/views/problem.py:68 +#: judge/views/problem.py:69 msgid "No such problem" msgstr "Không có bài nào như vậy" -#: judge/views/problem.py:69 +#: judge/views/problem.py:70 #, python-format msgid "Could not find a problem with the code \"%s\"." msgstr "Không tìm thấy bài tập với mã bài \"%s\"." -#: judge/views/problem.py:113 +#: judge/views/problem.py:114 #, python-brace-format msgid "Editorial for {0}" msgstr "Hướng dẫn cho {0}" -#: judge/views/problem.py:116 +#: judge/views/problem.py:117 #, python-brace-format msgid "Editorial for {0}" msgstr "Hướng dẫn cho {0}" -#: judge/views/problem.py:284 -#, python-brace-format -msgid "Disscuss {0}" -msgstr "" - -#: judge/views/problem.py:287 -#, python-brace-format -msgid "Discuss {0}" -msgstr "Thảo luận {0}" - -#: judge/views/problem.py:355 templates/contest/contest.html:79 -#: templates/user/user-about.html:28 templates/user/user-tabs.html:5 -#: templates/user/users-table.html:29 +#: judge/views/problem.py:342 templates/blog/list.html:121 +#: templates/contest/contest.html:79 templates/user/user-about.html:28 +#: templates/user/user-tabs.html:5 templates/user/users-table.html:29 msgid "Problems" msgstr "Bài tập" -#: judge/views/problem.py:655 +#: judge/views/problem.py:589 +#, fuzzy +#| msgid "Problem code" +msgid "Problem feed" +msgstr "Mã bài" + +#: judge/views/problem.py:685 msgid "Banned from submitting" msgstr "Bị cấm nộp bài" -#: judge/views/problem.py:656 +#: judge/views/problem.py:686 msgid "" "You have been declared persona non grata for this problem. You are " "permanently barred from submitting this problem." msgstr "Bạn đã bị cấm nộp bài này." -#: judge/views/problem.py:670 +#: judge/views/problem.py:700 msgid "Too many submissions" msgstr "Quá nhiều lần nộp" -#: judge/views/problem.py:671 +#: judge/views/problem.py:701 msgid "You have exceeded the submission limit for this problem." msgstr "Bạn đã vượt quá số lần nộp cho bài này." -#: judge/views/problem.py:731 judge/views/problem.py:734 +#: judge/views/problem.py:761 judge/views/problem.py:764 #, python-format msgid "Submit to %(problem)s" msgstr "Nộp bài cho %(problem)s" -#: judge/views/problem.py:749 +#: judge/views/problem.py:779 msgid "Clone Problem" msgstr "Nhân bản bài tập" @@ -2881,7 +2893,7 @@ msgid "Submission of %(problem)s by %(user)s" msgstr "Bài nộp của %(user)s cho bài %(problem)s" #: judge/views/submission.py:244 judge/views/submission.py:245 -#: templates/problem/problem.html:165 +#: templates/problem/problem.html:194 msgid "All submissions" msgstr "Tất cả bài nộp" @@ -3299,10 +3311,6 @@ msgstr "Chỉnh sửa" msgid " posted on %(time)s" msgstr "đã đăng vào %(time)s" -#: templates/blog/content.html:13 -msgid "posted" -msgstr "đã đăng" - #: templates/blog/dashboard.html:21 #, python-format msgid "" @@ -3314,59 +3322,63 @@ msgstr "" " vào %(time)s\n" " " -#: templates/blog/list.html:93 -msgid "Blog" +#: templates/blog/list.html:107 +msgid "Feed" msgstr "" -#: templates/blog/list.html:95 +#: templates/blog/list.html:109 msgid "Events" msgstr "Sự kiện" -#: templates/blog/list.html:100 +#: templates/blog/list.html:117 msgid "News" msgstr "Tin tức" -#: templates/blog/list.html:115 templates/problem/list.html:347 -#: templates/problem/problem.html:370 +#: templates/blog/list.html:125 templates/comments/list.html:2 +msgid "Comments" +msgstr "Bình luận" + +#: templates/blog/list.html:129 +msgid "Tickets" +msgstr "Báo cáo" + +#: templates/blog/list.html:148 +msgid "You have no ticket" +msgstr "Bạn không có báo cáo" + +#: templates/blog/list.html:163 templates/problem/list.html:347 +#: templates/problem/problem.html:407 msgid "Clarifications" msgstr "Thông báo" -#: templates/blog/list.html:121 +#: templates/blog/list.html:169 msgid "Add" msgstr "Thêm mới" -#: templates/blog/list.html:140 templates/problem/list.html:369 -#: templates/problem/problem.html:381 +#: templates/blog/list.html:188 templates/problem/list.html:369 +#: templates/problem/problem.html:418 msgid "No clarifications have been made at this time." msgstr "Không có thông báo nào." -#: templates/blog/list.html:148 +#: templates/blog/list.html:196 msgid "Ongoing contests" msgstr "Kỳ thi đang diễn ra" -#: templates/blog/list.html:156 +#: templates/blog/list.html:204 msgid "Ends in" msgstr "Còn" -#: templates/blog/list.html:166 +#: templates/blog/list.html:214 msgid "Upcoming contests" msgstr "Kỳ thi sắp diễn ra" -#: templates/blog/list.html:184 -msgid "My open tickets" -msgstr "Báo cáo dành cho tôi" +#: templates/blog/list.html:230 +msgid "Top Rating" +msgstr "Top Rating" -#: templates/blog/list.html:206 -msgid "New tickets" -msgstr "Báo cáo mới" - -#: templates/blog/list.html:227 -msgid "New problems" -msgstr "Bài tập mới" - -#: templates/blog/list.html:244 -msgid "Comment stream" -msgstr "Dòng bình luận" +#: templates/blog/list.html:246 +msgid "Top Score" +msgstr "Top Score" #: templates/chat/chat.html:18 msgid "Recent" @@ -3435,10 +3447,6 @@ msgstr "Tắt thông báo" msgid "users are online" msgstr "người đang trực tuyến" -#: templates/comments/list.html:2 -msgid "Comments" -msgstr "Bình luận" - #: templates/comments/list.html:18 templates/comments/list.html:27 msgid "Please login to vote" msgstr "Đăng nhập để vote" @@ -4347,122 +4355,126 @@ msgstr "Bạn có chắc muốn tính điểm lại %(count)d bài nộp?" msgid "Rescore all submissions" msgstr "Tính điểm lại các bài nộp" -#: templates/problem/problem.html:130 +#: templates/problem/problem.html:159 msgid "View as PDF" msgstr "Xem PDF" -#: templates/problem/problem.html:139 templates/problem/problem.html:149 -#: templates/problem/problem.html:154 +#: templates/problem/problem.html:168 templates/problem/problem.html:178 +#: templates/problem/problem.html:183 msgid "Submit solution" msgstr "Nộp bài" -#: templates/problem/problem.html:142 +#: templates/problem/problem.html:171 #, python-format msgid "%(counter)s submission left" msgid_plural "%(counter)s submissions left" msgstr[0] "Còn %(counter)s lần nộp" -#: templates/problem/problem.html:150 +#: templates/problem/problem.html:179 msgid "0 submissions left" msgstr "Còn 0 lần nộp" -#: templates/problem/problem.html:162 +#: templates/problem/problem.html:191 msgid "My submissions" msgstr "Bài nộp của tôi" -#: templates/problem/problem.html:166 +#: templates/problem/problem.html:195 msgid "Best submissions" msgstr "Các bài nộp tốt nhất" -#: templates/problem/problem.html:170 -msgid "Discuss" -msgstr "Thảo luận" - -#: templates/problem/problem.html:174 +#: templates/problem/problem.html:199 msgid "Read editorial" msgstr "Xem hướng dẫn" -#: templates/problem/problem.html:179 +#: templates/problem/problem.html:204 msgid "Manage tickets" msgstr "Xử lý báo cáo" -#: templates/problem/problem.html:183 +#: templates/problem/problem.html:208 msgid "Edit problem" msgstr "Chỉnh sửa bài" -#: templates/problem/problem.html:185 +#: templates/problem/problem.html:210 msgid "Edit test data" msgstr "Chỉnh sửa test" -#: templates/problem/problem.html:190 +#: templates/problem/problem.html:215 msgid "My tickets" msgstr "Báo cáo của tôi" -#: templates/problem/problem.html:198 +#: templates/problem/problem.html:223 msgid "Manage submissions" msgstr "Quản lý bài nộp" -#: templates/problem/problem.html:204 +#: templates/problem/problem.html:229 msgid "Clone problem" msgstr "Nhân bản bài" -#: templates/problem/problem.html:211 +#: templates/problem/problem.html:236 msgid "Points:" msgstr "Điểm:" -#: templates/problem/problem.html:214 templates/problem/problem.html:216 +#: templates/problem/problem.html:239 templates/problem/problem.html:241 msgid "(partial)" msgstr "(thành phần)" -#: templates/problem/problem.html:221 +#: templates/problem/problem.html:246 msgid "Time limit:" msgstr "Thời gian:" -#: templates/problem/problem.html:233 +#: templates/problem/problem.html:258 msgid "Memory limit:" msgstr "Bộ nhớ:" -#: templates/problem/problem.html:252 +#: templates/problem/problem.html:277 msgid "Author:" msgid_plural "Authors:" msgstr[0] "Tác giả:" -#: templates/problem/problem.html:267 +#: templates/problem/problem.html:292 msgid "Problem type" msgid_plural "Problem types" msgstr[0] "Dạng bài" -#: templates/problem/problem.html:280 +#: templates/problem/problem.html:305 msgid "Allowed languages" msgstr "Ngôn ngữ cho phép" -#: templates/problem/problem.html:288 +#: templates/problem/problem.html:313 #, python-format msgid "No %(lang)s judge online" msgstr "Không có máy chấm cho %(lang)s" -#: templates/problem/problem.html:299 +#: templates/problem/problem.html:324 msgid "Judge:" msgid_plural "Judges:" msgstr[0] "Máy chấm:" -#: templates/problem/problem.html:316 +#: templates/problem/problem.html:341 msgid "none available" msgstr "không có sẵn" -#: templates/problem/problem.html:331 +#: templates/problem/problem.html:356 #, python-format msgid "This problem has %(length)s clarification(s)" msgstr "Bài này có %(length)s thông báo" -#: templates/problem/problem.html:359 +#: templates/problem/problem.html:384 msgid "Request clarification" msgstr "Yêu cầu làm rõ đề" -#: templates/problem/problem.html:361 +#: templates/problem/problem.html:386 msgid "Report an issue" msgstr "Báo cáo một vấn đề" +#: templates/problem/problem.html:395 +msgid "View comments" +msgstr "Xem bình luận" + +#: templates/problem/problem.html:397 +msgid "Be the first to comment" +msgstr "Bình luận đầu tiên" + #: templates/problem/raw.html:64 msgid "Time Limit:" msgstr "Giới hạn thời gian:" @@ -4557,11 +4569,11 @@ msgstr "Không có máy chấm có thể chấm bài này." msgid "Submit!" msgstr "Nộp bài!" -#: templates/problem/voting-controls.html:53 +#: templates/problem/voting-controls.html:55 msgid "Edit difficulty" msgstr "Thay đổi độ khó" -#: templates/problem/voting-controls.html:61 +#: templates/problem/voting-controls.html:63 msgid "Vote difficulty" msgstr "Bình chọn độ khó" @@ -4573,14 +4585,6 @@ msgstr "Bạn thấy độ khó bài này thế nào?" msgid "This helps us improve the site" msgstr "Bình chọn giúp admin cải thiện bài tập." -#: templates/problem/voting-form.html:38 -msgid "Easy" -msgstr "Dễ" - -#: templates/problem/voting-form.html:39 -msgid "Hard" -msgstr "Khó" - #: templates/problem/voting-stats.html:29 msgid "Voting Statistics" msgstr "Thống kê" @@ -4992,6 +4996,10 @@ msgstr "Tốt nhất" msgid "%(user)s's" msgstr "" +#: templates/ticket/feed.html:20 +msgid " replied" +msgstr "" + #: templates/ticket/list.html:135 templates/ticket/ticket.html:273 msgid "Reopened: " msgstr "Mở lại: " @@ -5331,3 +5339,30 @@ msgstr "Thông tin" #: templates/widgets/select_all.html:8 msgid "Check all" msgstr "Chọn tất cả" + +#~ msgid "Discuss {0}" +#~ msgstr "Thảo luận {0}" + +#~ msgid "posted" +#~ msgstr "đã đăng" + +#~ msgid "My open tickets" +#~ msgstr "Báo cáo dành cho tôi" + +#~ msgid "New tickets" +#~ msgstr "Báo cáo mới" + +#~ msgid "New problems" +#~ msgstr "Bài tập mới" + +#~ msgid "Comment stream" +#~ msgstr "Dòng bình luận" + +#~ msgid "Discuss" +#~ msgstr "Thảo luận" + +#~ msgid "Easy" +#~ msgstr "Dễ" + +#~ msgid "Hard" +#~ msgstr "Khó" diff --git a/resources/base.scss b/resources/base.scss index 369b91b..9e21b4c 100644 --- a/resources/base.scss +++ b/resources/base.scss @@ -232,7 +232,7 @@ header { } #navigation { - position: relative; + position: fixed; top: 0; left: 0; right: 0; @@ -382,7 +382,7 @@ hr { } #content { - margin: 1em auto auto; + margin: 4.5em auto 1em auto; // Header width: 90%; diff --git a/resources/blog.scss b/resources/blog.scss index cd6aeb2..8cd8693 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -2,9 +2,9 @@ .blog-content { padding-right: 0em; - flex: 73.5%; vertical-align: top; margin-right: 0; + width: 100%; .post { border: 1px dotted grey; @@ -33,7 +33,8 @@ } .blog-sidebar { - flex: 26.5%; + width: 100%; + margin-left: auto; } .blog-sidebox { @@ -88,6 +89,19 @@ color: #555; } +@media (max-width: 799px) { + .left-sidebar-header { + display: none; + } + .left-sidebar-item { + display: inline-block; + } + .blog-left-sidebar { + text-align: right; + padding-right: 1em; + margin-bottom: 1em; + } +} @media (min-width: 800px) { .blog-content, .blog-sidebar { display: block !important; @@ -104,6 +118,28 @@ #blog-container { display: flex; } + + .blog-content { + max-width: 71.5%; + margin-left: 10%; + } + + .blog-sidebar { + width: 18%; + } + + .blog-left-sidebar { + width: 8%; + margin-right: 1em; + position: fixed; + height: 100%; + margin-top: -4em; + padding-top: 4em; + } + + .feed-table { + font-size: small; + } } #mobile.tabs { @@ -135,3 +171,63 @@ } } } + +.blog-box { + border-bottom: 1px solid black; + width: 90%; + margin-bottom: 2.5em; + padding: 0.5em 1.25em; + background-color: white; + margin-left: auto; + margin-right: auto; +} + +.blog-description { + max-height: 20em; + overflow: hidden; + overflow-wrap: anywhere; + padding-bottom: 1em; +} +.problem-feed-name { + display: inline; + font-weight: bold; +} +.problem-feed-name a { + color: #0645ad; +} +.problem-feed-info-entry { + display: inline; + float: right; +} +.problem-feed-types { + color: gray; +} + +.blog-left-sidebar { + background-color: #f0f1f3; + color: #616161; +} + +.left-sidebar-item { + padding: 1em 0.5em; + text-align: center; +} +.left-sidebar-item:hover { + background-color: lightgray; + cursor: pointer; +} +.sidebar-icon { + font-size: x-large; + margin-bottom: 0.1em; + color: black; +} +.left-sidebar-header { + text-align: center; + padding-bottom: 1em; + border-bottom: 1px solid black; + color: black; + border-radius: 0; +} +.feed-table { + margin: 0; +} \ No newline at end of file diff --git a/templates/blog/content.html b/templates/blog/content.html index fd8804d..753e36e 100644 --- a/templates/blog/content.html +++ b/templates/blog/content.html @@ -1,18 +1,16 @@ -
-

- {{ post.title }} -

- +
+
- {%- if post.sticky %}{% endif -%} {% with authors=post.authors.all() %} {%- if authors -%} + {%- endif -%} {% endwith %} - {{_('posted')}} {{ relative_time(post.publish_on, abs=_('on {time}'), rel=_('{time}')) -}} + • {{ relative_time(post.publish_on, abs=_('on {time}'), rel=_('{time}')) -}} + {%- if post.sticky %} • {% endif -%} - + @@ -20,8 +18,10 @@ - -
+
+

+ {{ post.title }} +

{% if post.is_organization_private and show_organization_private_icon %}
{% for org in post.organizations.all() %} @@ -33,7 +33,7 @@ {% endfor %}
{% endif %} -
+
{% cache 86400 'post_summary' post.id %} {{ post.summary|default(post.content, true)|markdown('blog', 'svg', lazy_load=True)|reference|str|safe }} {% endcache %} diff --git a/templates/blog/list.html b/templates/blog/list.html index cd5b65d..1419703 100644 --- a/templates/blog/list.html +++ b/templates/blog/list.html @@ -16,30 +16,10 @@ clear: both; } } - .post { - margin: 0 2%; - } .time { margin-left: 0; } - .post:first-child { - margin-top: 0.6em; - } - - .own-open-tickets .title a, .open-tickets .title a { - display: block; - } - - .own-open-tickets .object, .open-tickets .object { - margin-left: 1em; - font-style: italic; - } - - .open-tickets .user { - margin-left: 1em; - } - .no-clarifications-message { font-style: italic; text-align: center; @@ -56,6 +36,11 @@ #add-clarification:hover { color: cyan; } + + #content { + width: 99%; + margin-left: 0; + } {% endblock %} @@ -81,6 +66,35 @@ $('.blog-content').hide(); $('.blog-sidebar').show(); }); + $('.blog-description').on('click', function() { + var max_height = $(this).css('max-height'); + if (max_height !== 'fit-content') { + $(this).css('max-height', 'fit-content'); + $(this).parent().css('background-image', 'inherit') + .css('padding-bottom', '0.5em'); + $(this).css('cursor', 'auto'); + } + }) + $('.blog-description').each(function() { + if ($(this).prop('scrollHeight') > $(this).height() ) { + $(this).parent().css('background-image', '-webkit-linear-gradient(bottom, lightgray, lightgray 3%, transparent 8%, transparent 100%)'); + $(this).parent().css('padding-bottom', '0'); + $(this).css('cursor', 'pointer'); + } + }); + $('.left-sidebar-item').on('click', function() { + var url = $(this).attr('data-href'); + window.location.replace(url); + }); + {% if feed_type == 'blog' %} + $('#news-icon').css('color', 'green'); + {% elif feed_type == 'problem' %} + $('#problems-icon').css('color', 'green'); + {% elif feed_type == 'ticket' %} + $('#tickets-icon').css('color', 'green'); + {% elif feed_type == 'comment' %} + $('#comments-icon').css('color', 'green'); + {% endif %} }); {% endblock %} @@ -90,20 +104,54 @@
- {% endif %} - - {% if own_open_tickets %} - - {% endif %} - - {% if open_tickets %} - - {% endif %} - + {% else %} +
+ {% include "comments/list.html" %} +
{% endif %} {% endblock %} diff --git a/templates/ticket/feed.html b/templates/ticket/feed.html new file mode 100644 index 0000000..89a2db5 --- /dev/null +++ b/templates/ticket/feed.html @@ -0,0 +1,25 @@ +
+

+ + {{ ticket.title }} + + - + + {{ ticket.linked_item|item_title }} +

+ {% with author=ticket.user %} + {% if author %} +
+ + {{ link_user(author) }} +
+ {% endif %} + {% endwith %} +
+ + {{link_user(ticket.messages.last().user)}} {{_(' replied')}} +
+
+ {{ ticket.messages.last().body |markdown("ticket", MATH_ENGINE)|reference|str|safe }} +
+
\ No newline at end of file From d0e2c7daa99f65a602b207a6c57ab9fa1e553651 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 24 Mar 2022 17:34:03 -0500 Subject: [PATCH 014/959] Fix duplicate view comment --- templates/problem/problem.html | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/templates/problem/problem.html b/templates/problem/problem.html index cd01943..30e3716 100644 --- a/templates/problem/problem.html +++ b/templates/problem/problem.html @@ -399,17 +399,6 @@
{% endif %} - {% if not (contest_problem and contest_problem.contest.use_clarifications) %} -
-
- {% if has_comments %} - {{_('View comments')}} ({{comment_list.count()}}) - {% else %} - {{_('Be the first to comment')}} - {% endif %} -
-
- {% endif %} {% endblock %} {% block comments %} From 0dfad5b0cd98a0e4ad23e0b65d7147294018e077 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 28 Mar 2022 18:42:50 -0500 Subject: [PATCH 015/959] Add manifest --- resources/icons/manifest.json | 19 +++++++++++++++++++ resources/site.webmanifest | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 resources/icons/manifest.json create mode 100644 resources/site.webmanifest diff --git a/resources/icons/manifest.json b/resources/icons/manifest.json new file mode 100644 index 0000000..de65106 --- /dev/null +++ b/resources/icons/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/resources/site.webmanifest b/resources/site.webmanifest new file mode 100644 index 0000000..de65106 --- /dev/null +++ b/resources/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} From 847fc11bc5c9f835d0b3e75b7dc2c8f4431bfe81 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 28 Mar 2022 19:29:11 -0500 Subject: [PATCH 016/959] Localize rating chart (DMOJ) --- judge/views/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/judge/views/user.py b/judge/views/user.py index 2265979..a14617a 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -169,7 +169,7 @@ class UserAboutPage(UserPage): 'ranking': rating.rank, 'link': reverse('contest_ranking', args=(rating.contest.key,)), 'timestamp': (rating.contest.end_time - EPOCH).total_seconds() * 1000, - 'date': date_format(rating.contest.end_time, _('M j, Y, G:i')), + 'date': date_format(timezone.localtime(rating.contest.end_time), _('M j, Y, G:i')), 'class': rating_class(rating.rating), 'height': '%.3fem' % rating_progress(rating.rating), } for rating in ratings])) From 68a53b8749620642f69f730cddcf33bf393b5154 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 31 Mar 2022 01:09:38 -0500 Subject: [PATCH 017/959] Remove unlisted users from home page --- judge/views/blog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/judge/views/blog.py b/judge/views/blog.py index b7855dd..f29fdac 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -66,9 +66,9 @@ class FeedView(ListView): visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) - context['top_rated'] = Profile.objects.order_by('-rating')[:10] - context['top_scorer'] = Profile.objects.order_by('-performance_points')[:10] - + context['top_rated'] = Profile.objects.filter(is_unlisted=False).order_by('-rating')[:10] + context['top_scorer'] = Profile.objects.filter(is_unlisted=False).order_by('-performance_points')[:10] + print(context['top_rated']) return context From ca2c74af7c9ed89c7047b54dc93ea119c723e2b2 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 31 Mar 2022 01:28:40 -0500 Subject: [PATCH 018/959] Add fields to problem vote admin --- judge/admin/problem.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 716e9eb..c4097d7 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -239,9 +239,9 @@ class ProblemAdmin(VersionAdmin): class ProblemPointsVoteAdmin(admin.ModelAdmin): - list_display = ('vote_points', 'voter', 'problem_name', 'problem_code', 'problem_points', 'vote_time') + list_display = ('vote_points', 'voter', 'voter_rating', 'voter_point', 'problem_name', 'problem_code', 'problem_points') search_fields = ('voter__user__username', 'problem__code', 'problem__name') - readonly_fields = ('voter', 'problem', 'problem_code', 'problem_points', 'vote_time') + readonly_fields = ('voter', 'problem', 'problem_code', 'problem_points', 'voter_rating', 'voter_point') def has_change_permission(self, request, obj=None): if obj is None: @@ -263,6 +263,16 @@ class ProblemPointsVoteAdmin(admin.ModelAdmin): problem_name.short_description = _('Problem name') problem_name.admin_order_field = 'problem__name' + def voter_rating(self, obj): + return obj.voter.rating + voter_rating.short_description = _('Voter rating') + voter_rating.admin_order_field = 'voter__rating' + + def voter_point(self, obj): + return round(obj.voter.performance_points) + voter_rating.short_description = _('Voter point') + voter_rating.admin_order_field = 'voter__performance_points' + def vote_points(self, obj): return obj.points vote_points.short_description = _('Vote') \ No newline at end of file From 329a5293ddbdfd9450ce24009c42a8dca81ebb2f Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 31 Mar 2022 01:31:10 -0500 Subject: [PATCH 019/959] Fix field problem vote admin --- judge/admin/problem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index c4097d7..1cad785 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -270,8 +270,8 @@ class ProblemPointsVoteAdmin(admin.ModelAdmin): def voter_point(self, obj): return round(obj.voter.performance_points) - voter_rating.short_description = _('Voter point') - voter_rating.admin_order_field = 'voter__performance_points' + voter_point.short_description = _('Voter point') + voter_point.admin_order_field = 'voter__performance_points' def vote_points(self, obj): return obj.points From ce77242008d6092a6c327ae122bba5fffb015db2 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 31 Mar 2022 20:33:37 -0500 Subject: [PATCH 020/959] Fix bugs --- judge/admin/comments.py | 1 + judge/views/blog.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/judge/admin/comments.py b/judge/admin/comments.py index be6780c..f71de4b 100644 --- a/judge/admin/comments.py +++ b/judge/admin/comments.py @@ -25,6 +25,7 @@ class CommentAdmin(VersionAdmin): ) list_display = ['author', 'linked_page', 'time'] search_fields = ['author__user__username', 'page', 'body'] + readonly_fields = ['score'] actions = ['hide_comment', 'unhide_comment'] list_filter = ['hidden'] actions_on_top = True diff --git a/judge/views/blog.py b/judge/views/blog.py index f29fdac..48595b9 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -68,7 +68,7 @@ class FeedView(ListView): context['top_rated'] = Profile.objects.filter(is_unlisted=False).order_by('-rating')[:10] context['top_scorer'] = Profile.objects.filter(is_unlisted=False).order_by('-performance_points')[:10] - print(context['top_rated']) + return context From 30a856dcb9d7157be82cc21fbd6759adc32d7c41 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 4 Apr 2022 17:04:40 -0500 Subject: [PATCH 021/959] add vote mean and std to problem admin --- judge/admin/problem.py | 17 +++++++++++++++-- judge/models/problem.py | 1 + locale/vi/LC_MESSAGES/django.po | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 1cad785..3d81d96 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -3,7 +3,8 @@ from operator import attrgetter from django import forms from django.contrib import admin from django.db import transaction -from django.db.models import Q +from django.db.models import Q, Avg +from django.db.models.aggregates import StdDev from django.forms import ModelForm from django.urls import reverse_lazy from django.utils.html import format_html @@ -131,7 +132,7 @@ class ProblemAdmin(VersionAdmin): (_('Justice'), {'fields': ('banned_users',)}), (_('History'), {'fields': ('change_message',)}), ) - list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public'] + list_display = ['code', 'name', 'show_authors', 'points', 'vote_mean', 'vote_std', 'is_public', 'show_public'] ordering = ['code'] search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username') inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline] @@ -198,6 +199,10 @@ class ProblemAdmin(VersionAdmin): def get_queryset(self, request): queryset = Problem.objects.prefetch_related('authors__user') + queryset = queryset.annotate( + _vote_mean=Avg('problem_points_votes__points'), + _vote_std=StdDev('problem_points_votes__points') + ) if request.user.has_perm('judge.edit_all_problem'): return queryset @@ -237,6 +242,14 @@ class ProblemAdmin(VersionAdmin): return form.cleaned_data['change_message'] return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs) + def vote_mean(self, obj): + return round(obj._vote_mean, 1) if obj._vote_mean is not None else None + vote_mean.admin_order_field = '_vote_mean' + + def vote_std(self, obj): + return round(obj._vote_std, 1) if obj._vote_std is not None else None + vote_std.admin_order_field = '_vote_std' + class ProblemPointsVoteAdmin(admin.ModelAdmin): list_display = ('vote_points', 'voter', 'voter_rating', 'voter_point', 'problem_name', 'problem_code', 'problem_points') diff --git a/judge/models/problem.py b/judge/models/problem.py index f9e10ec..1d6fc0e 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -1,4 +1,5 @@ from operator import attrgetter +from math import sqrt from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index c77acf1..463ec69 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -749,7 +749,7 @@ msgstr "giới hạn thời gian" #: judge/models/contest.py:76 judge/models/problem.py:136 msgid "publicly visible" -msgstr "hiển thị công khai" +msgstr "công khai" #: judge/models/contest.py:77 msgid "" From 891e2b0d9298a1cc008b0a44fc756f10308ddcdf Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 4 Apr 2022 17:13:23 -0500 Subject: [PATCH 022/959] Add vote count to problem admin --- judge/admin/problem.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 3d81d96..cfa954c 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -3,7 +3,7 @@ from operator import attrgetter from django import forms from django.contrib import admin from django.db import transaction -from django.db.models import Q, Avg +from django.db.models import Q, Avg, Count from django.db.models.aggregates import StdDev from django.forms import ModelForm from django.urls import reverse_lazy @@ -132,7 +132,7 @@ class ProblemAdmin(VersionAdmin): (_('Justice'), {'fields': ('banned_users',)}), (_('History'), {'fields': ('change_message',)}), ) - list_display = ['code', 'name', 'show_authors', 'points', 'vote_mean', 'vote_std', 'is_public', 'show_public'] + list_display = ['code', 'name', 'show_authors', 'points', 'vote_cnt', 'vote_mean', 'vote_std', 'is_public', 'show_public'] ordering = ['code'] search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username') inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline] @@ -201,7 +201,8 @@ class ProblemAdmin(VersionAdmin): queryset = Problem.objects.prefetch_related('authors__user') queryset = queryset.annotate( _vote_mean=Avg('problem_points_votes__points'), - _vote_std=StdDev('problem_points_votes__points') + _vote_std=StdDev('problem_points_votes__points'), + _vote_cnt=Count('problem_points_votes__points') ) if request.user.has_perm('judge.edit_all_problem'): return queryset @@ -250,6 +251,10 @@ class ProblemAdmin(VersionAdmin): return round(obj._vote_std, 1) if obj._vote_std is not None else None vote_std.admin_order_field = '_vote_std' + def vote_cnt(self, obj): + return obj._vote_cnt + vote_cnt.admin_order_field = '_vote_cnt' + class ProblemPointsVoteAdmin(admin.ModelAdmin): list_display = ('vote_points', 'voter', 'voter_rating', 'voter_point', 'problem_name', 'problem_code', 'problem_points') From 34523ab53f800a224c86de21c497f4d7663ea4b8 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 4 Apr 2022 17:43:56 -0500 Subject: [PATCH 023/959] Add median to problem admin --- judge/admin/problem.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index cfa954c..cb5bec4 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -1,4 +1,5 @@ from operator import attrgetter +import statistics from django import forms from django.contrib import admin @@ -132,7 +133,7 @@ class ProblemAdmin(VersionAdmin): (_('Justice'), {'fields': ('banned_users',)}), (_('History'), {'fields': ('change_message',)}), ) - list_display = ['code', 'name', 'show_authors', 'points', 'vote_cnt', 'vote_mean', 'vote_std', 'is_public', 'show_public'] + list_display = ['code', 'name', 'show_authors', 'points', 'vote_cnt', 'vote_mean', 'vote_median', 'vote_std', 'is_public', 'show_public'] ordering = ['code'] search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username') inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline] @@ -255,6 +256,10 @@ class ProblemAdmin(VersionAdmin): return obj._vote_cnt vote_cnt.admin_order_field = '_vote_cnt' + def vote_median(self, obj): + votes = obj.problem_points_votes.values_list('points', flat=True) + return statistics.median(votes) if votes else None + class ProblemPointsVoteAdmin(admin.ModelAdmin): list_display = ('vote_points', 'voter', 'voter_rating', 'voter_point', 'problem_name', 'problem_code', 'problem_points') @@ -266,6 +271,9 @@ class ProblemPointsVoteAdmin(admin.ModelAdmin): return request.user.has_perm('judge.edit_own_problem') return obj.problem.is_editable_by(request.user) + def lookup_allowed(self, key, value): + return True + def problem_code(self, obj): return obj.problem.code problem_code.short_description = _('Problem code') From 2fe571379ce42c6ccc8ea2c00b90c0b77b630bb4 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 11 Apr 2022 21:18:01 -0500 Subject: [PATCH 024/959] Add ML to problem feed --- judge/management/commands/addjudge.py | 1 + judge/management/commands/generate_data.py | 49 +++++++++++++++++ judge/ml/collab_filter.py | 64 ++++++++++++++++++++++ judge/utils/problems.py | 4 +- judge/views/problem.py | 38 ++++++++++++- requirements.txt | 3 +- templates/blog/list.html | 2 +- templates/problem/feed.html | 2 +- 8 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 judge/management/commands/generate_data.py create mode 100644 judge/ml/collab_filter.py diff --git a/judge/management/commands/addjudge.py b/judge/management/commands/addjudge.py index b659364..699b3ed 100644 --- a/judge/management/commands/addjudge.py +++ b/judge/management/commands/addjudge.py @@ -15,3 +15,4 @@ class Command(BaseCommand): judge.name = options['name'] judge.auth_key = options['auth_key'] judge.save() + diff --git a/judge/management/commands/generate_data.py b/judge/management/commands/generate_data.py new file mode 100644 index 0000000..f7fdbd4 --- /dev/null +++ b/judge/management/commands/generate_data.py @@ -0,0 +1,49 @@ +from django.core.management.base import BaseCommand +from judge.models import * +from collections import defaultdict +import csv +import os +from django.conf import settings + + +def gen_submissions(): + headers = ['uid', 'pid'] + with open(os.path.join(settings.ML_DATA_PATH, 'submissions.csv'), 'w') as csvfile: + f = csv.writer(csvfile) + f.writerow(headers) + + last_pid = defaultdict(int) + for u in Profile.objects.all(): + used = set() + print('Processing user', u.id) + for s in Submission.objects.filter(user=u).order_by('-date'): + if s.problem.id not in used: + used.add(s.problem.id) + f.writerow([u.id, s.problem.id]) + +def gen_users(): + headers = ['uid', 'username', 'rating', 'points'] + with open(os.path.join(settings.ML_DATA_PATH, 'profiles.csv'), 'w') as csvfile: + f = csv.writer(csvfile) + f.writerow(headers) + + for u in Profile.objects.all(): + f.writerow([u.id, u.username, u.rating, u.performance_points]) + +def gen_problems(): + headers = ['pid', 'code', 'name', 'points', 'url'] + with open(os.path.join(settings.ML_DATA_PATH, 'problems.csv'), 'w') as csvfile: + f = csv.writer(csvfile) + f.writerow(headers) + + for p in Problem.objects.all(): + f.writerow([p.id, p.code, p.name, p.points, 'lqdoj.edu.vn/problem/' + p.code]) + + +class Command(BaseCommand): + help = 'generate data for ML' + + def handle(self, *args, **options): + gen_users() + gen_problems() + gen_submissions() \ No newline at end of file diff --git a/judge/ml/collab_filter.py b/judge/ml/collab_filter.py new file mode 100644 index 0000000..76f8827 --- /dev/null +++ b/judge/ml/collab_filter.py @@ -0,0 +1,64 @@ +import numpy as np +from django.conf import settings +import os + + +class CollabFilter: + DOT = 'dot' + COSINE = 'cosine' + def __init__(self): + embeddings = np.load(os.path.join(settings.ML_OUTPUT_PATH, 'collab_filter/embeddings.npz'), + allow_pickle=True) + arr0, arr1 = embeddings.files + self.user_embeddings = embeddings[arr0] + self.problem_embeddings = embeddings[arr1] + + def compute_scores(self, query_embedding, item_embeddings, measure=DOT): + """Computes the scores of the candidates given a query. + Args: + query_embedding: a vector of shape [k], representing the query embedding. + item_embeddings: a matrix of shape [N, k], such that row i is the embedding + of item i. + measure: a string specifying the similarity measure to be used. Can be + either DOT or COSINE. + Returns: + scores: a vector of shape [N], such that scores[i] is the score of item i. + """ + u = query_embedding + V = item_embeddings + if measure == self.COSINE: + V = V / np.linalg.norm(V, axis=1, keepdims=True) + u = u / np.linalg.norm(u) + scores = u.dot(V.T) + return scores + + def user_recommendations(self, user, problems, measure=DOT, limit=None): + uid = user.id + if uid >= len(self.user_embeddings): + uid = 0 + scores = self.compute_scores( + self.user_embeddings[uid], self.problem_embeddings, measure) + + res = [] # [(score, problem)] + for problem in problems: + pid = problem.id + if pid < len(scores): + res.append((scores[pid], problem)) + + res.sort(reverse=True) + return res[:limit] + + + # return a list of pid + def problems_neighbors(self, problem, problemset, measure=DOT, limit=None): + pid = problem.id + if pid >= len(self.problem_embeddings): + return None + scores = self.compute_scores( + self.problem_embeddings[pid], self.problem_embeddings, measure) + res = [] + for p in problemset: + if p.id < len(scores): + res.append((scores[p.id], p)) + res.sort(reverse=True) + return res[:limit] \ No newline at end of file diff --git a/judge/utils/problems.py b/judge/utils/problems.py index ba846e6..aacb916 100644 --- a/judge/utils/problems.py +++ b/judge/utils/problems.py @@ -115,7 +115,7 @@ def hot_problems(duration, limit): qs = cache.get(cache_key) if qs is None: qs = Problem.get_public_problems() \ - .filter(submission__date__gt=timezone.now() - duration, points__gt=3, points__lt=25) + .filter(submission__date__gt=timezone.now() - duration) qs0 = qs.annotate(k=Count('submission__user', distinct=True)).order_by('-k').values_list('k', flat=True) if not qs0: @@ -141,7 +141,7 @@ def hot_problems(duration, limit): qs = qs.filter(unique_user_count__gt=max(mx / 3.0, 1)) qs = qs.annotate(ordering=ExpressionWrapper( - 0.5 * F('points') * (0.4 * F('ac_volume') / F('submission_volume') + 0.6 * F('ac_rate')) + + 0.02 * F('points') * (0.4 * F('ac_volume') / F('submission_volume') + 0.6 * F('ac_rate')) + 100 * e ** (F('unique_user_count') / mx), output_field=FloatField(), )).order_by('-ordering').defer('description')[:limit] diff --git a/judge/views/problem.py b/judge/views/problem.py index 995c14e..25bf526 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -39,6 +39,7 @@ from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.tickets import own_ticket_filter from judge.utils.views import QueryStringSortMixin, SingleObjectFormView, TitleMixin, generic_message from judge.views.blog import FeedView +from judge.ml.collab_filter import CollabFilter def get_contest_problem(problem, profile): @@ -611,9 +612,44 @@ class ProblemFeed(FeedView): .values_list('problem__id', flat=True)) return queryset.distinct() + # arr = [[], [], ..] + def merge_recommendation(self, arr): + idx = [0] * len(arr) + stop = False + res = [] + used_pid = set() + cnt = 0 + while not stop: + cnt += 1 + stop = True + for i in range(len(arr)): + if idx[i] < len(arr[i]): + obj = arr[i][idx[i]] + if type(obj) == tuple: + obj = obj[1] + elif cnt % 3 != 0: # hot problems appear less + continue + if obj not in used_pid: + res.append(obj) + used_pid.add(obj) + idx[i] += 1 + stop = False + return res + + def get_queryset(self): queryset = self.get_unsolved_queryset() - return queryset.order_by('?') + user = self.request.profile + if not settings.ML_OUTPUT_PATH or not user: + return queryset.order_by('?') + + cl_model = CollabFilter() + dot_rec = cl_model.user_recommendations(user, queryset, cl_model.DOT, 100) + cosine_rec = cl_model.user_recommendations(user, queryset, cl_model.COSINE, 100) + hot_problems_rec = hot_problems(timedelta(days=7), 10) + + q = self.merge_recommendation([dot_rec, cosine_rec, hot_problems_rec]) + return q def get_context_data(self, **kwargs): context = super(ProblemFeed, self).get_context_data(**kwargs) diff --git a/requirements.txt b/requirements.txt index 18eb9a3..5d84d30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,4 +35,5 @@ netaddr redis lupa websocket-client -python-memcached \ No newline at end of file +python-memcached +numpy \ No newline at end of file diff --git a/templates/blog/list.html b/templates/blog/list.html index 1419703..d8eee93 100644 --- a/templates/blog/list.html +++ b/templates/blog/list.html @@ -77,7 +77,7 @@ }) $('.blog-description').each(function() { if ($(this).prop('scrollHeight') > $(this).height() ) { - $(this).parent().css('background-image', '-webkit-linear-gradient(bottom, lightgray, lightgray 3%, transparent 8%, transparent 100%)'); + $(this).parent().css('background-image', '-webkit-linear-gradient(bottom, gray, lightgray 3%, transparent 8%, transparent 100%)'); $(this).parent().css('padding-bottom', '0'); $(this).css('cursor', 'pointer'); } diff --git a/templates/problem/feed.html b/templates/problem/feed.html index 8de3bd0..a013836 100644 --- a/templates/problem/feed.html +++ b/templates/problem/feed.html @@ -17,7 +17,7 @@ {% for type in problem.types_list %} {{ type }}{% if not loop.last %}, {% endif %} - {% endfor %} + {% endfor %}, {{problem.points | int}}
{% endif %}
From e9725d27aa9d1590d72cafc5c94cbf2002190b9e Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 11 Apr 2022 23:13:36 -0500 Subject: [PATCH 025/959] Add * point --- templates/problem/feed.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/problem/feed.html b/templates/problem/feed.html index a013836..9c7d749 100644 --- a/templates/problem/feed.html +++ b/templates/problem/feed.html @@ -17,7 +17,7 @@ {% for type in problem.types_list %} {{ type }}{% if not loop.last %}, {% endif %} - {% endfor %}, {{problem.points | int}} + {% endfor %}, *{{problem.points | int}}
{% endif %}
From 5c6391fb76d88b0d9560a62f0eee36795b9562a3 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 12 Apr 2022 11:53:39 -0500 Subject: [PATCH 026/959] Fix sort key error --- judge/ml/collab_filter.py | 4 ++-- judge/views/problem.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/judge/ml/collab_filter.py b/judge/ml/collab_filter.py index 76f8827..504f9c6 100644 --- a/judge/ml/collab_filter.py +++ b/judge/ml/collab_filter.py @@ -45,7 +45,7 @@ class CollabFilter: if pid < len(scores): res.append((scores[pid], problem)) - res.sort(reverse=True) + res.sort(reverse=True, key=lambda x: x[0]) return res[:limit] @@ -60,5 +60,5 @@ class CollabFilter: for p in problemset: if p.id < len(scores): res.append((scores[p.id], p)) - res.sort(reverse=True) + res.sort(reverse=True, key=lambda x: x[0]) return res[:limit] \ No newline at end of file diff --git a/judge/views/problem.py b/judge/views/problem.py index 25bf526..f58fd11 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -646,8 +646,9 @@ class ProblemFeed(FeedView): cl_model = CollabFilter() dot_rec = cl_model.user_recommendations(user, queryset, cl_model.DOT, 100) cosine_rec = cl_model.user_recommendations(user, queryset, cl_model.COSINE, 100) - hot_problems_rec = hot_problems(timedelta(days=7), 10) - + hot_problems_rec = [problem for problem in hot_problems(timedelta(days=7), 10) + if problem in queryset] + q = self.merge_recommendation([dot_rec, cosine_rec, hot_problems_rec]) return q From f539a906354d253a8edafc2b0664325fc541bcd2 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 13 Apr 2022 00:52:03 -0500 Subject: [PATCH 027/959] Change problem page --- dmoj/urls.py | 5 +- judge/ml/collab_filter.py | 6 +- judge/views/blog.py | 6 +- judge/views/problem.py | 87 +- locale/vi/LC_MESSAGES/django.po | 376 +- resources/blog.scss | 29 +- resources/fontawesome/fontawesome.css | 6338 +++++++++++++++++++++++++ resources/problem.scss | 37 + templates/blog/list.html | 371 +- templates/problem/feed.html | 9 +- templates/problem/list.html | 126 +- templates/problem/search-form.html | 25 +- templates/three-column-content.html | 69 + 13 files changed, 6941 insertions(+), 543 deletions(-) create mode 100755 resources/fontawesome/fontawesome.css create mode 100644 templates/three-column-content.html diff --git a/dmoj/urls.py b/dmoj/urls.py index c691074..69e7e47 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -111,13 +111,14 @@ urlpatterns = [ url(r'^', include('social_django.urls')), url(r'^feed/', include([ - url(r'^problems/$', problem.ProblemFeed.as_view(), name='problem_feed'), url(r'^tickets/$', blog.TicketFeed.as_view(), name='ticket_feed'), url(r'^comments/$', blog.CommentFeed.as_view(), name='comment_feed'), ])), url(r'^problems/$', problem.ProblemList.as_view(), name='problem_list'), url(r'^problems/random/$', problem.RandomProblem.as_view(), name='problem_random'), - + url(r'^problems/feed$', problem.ProblemFeed.as_view(feed_type='for_you'), name='problem_feed'), + url(r'^problems/feed/new$', problem.ProblemFeed.as_view(feed_type='new'), name='problem_feed_new'), + url(r'^problem/(?P[^/]+)', include([ url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'), url(r'^/editorial$', problem.ProblemSolution.as_view(), name='problem_editorial'), diff --git a/judge/ml/collab_filter.py b/judge/ml/collab_filter.py index 504f9c6..511cebc 100644 --- a/judge/ml/collab_filter.py +++ b/judge/ml/collab_filter.py @@ -6,8 +6,10 @@ import os class CollabFilter: DOT = 'dot' COSINE = 'cosine' - def __init__(self): - embeddings = np.load(os.path.join(settings.ML_OUTPUT_PATH, 'collab_filter/embeddings.npz'), + + # name = 'collab_filter' or 'collab_filter_time' + def __init__(self, name): + embeddings = np.load(os.path.join(settings.ML_OUTPUT_PATH, name + '/embeddings.npz'), allow_pickle=True) arr0, arr1 = embeddings.files self.user_embeddings = embeddings[arr0] diff --git a/judge/views/blog.py b/judge/views/blog.py index 48595b9..828f54f 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -93,7 +93,7 @@ class PostList(FeedView): context['title'] = self.title or _('Page %d of Posts') % context['page_obj'].number context['first_page_href'] = reverse('home') context['page_prefix'] = reverse('blog_post_list') - context['feed_type'] = 'blog' + context['page_type'] = 'blog' context['post_comment_counts'] = { int(page[2:]): count for page, count in Comment.objects @@ -128,7 +128,7 @@ class TicketFeed(FeedView): def get_context_data(self, **kwargs): context = super(TicketFeed, self).get_context_data(**kwargs) - context['feed_type'] = 'ticket' + context['page_type'] = 'ticket' context['first_page_href'] = self.request.path context['page_prefix'] = '?page=' context['title'] = _('Ticket feed') @@ -146,7 +146,7 @@ class CommentFeed(FeedView): def get_context_data(self, **kwargs): context = super(CommentFeed, self).get_context_data(**kwargs) - context['feed_type'] = 'comment' + context['page_type'] = 'comment' context['first_page_href'] = self.request.path context['page_prefix'] = '?page=' context['title'] = _('Comment feed') diff --git a/judge/views/problem.py b/judge/views/problem.py index f58fd11..6e0a5d4 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -38,7 +38,6 @@ from judge.utils.problems import contest_attempted_ids, contest_completed_ids, h from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.tickets import own_ticket_filter from judge.utils.views import QueryStringSortMixin, SingleObjectFormView, TitleMixin, generic_message -from judge.views.blog import FeedView from judge.ml.collab_filter import CollabFilter @@ -455,10 +454,9 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView queryset = queryset.filter(points__gte=self.point_start) if self.point_end is not None: queryset = queryset.filter(points__lte=self.point_end) - if self.show_editorial: - queryset = queryset.annotate( - has_public_editorial=Sum(Case(When(solution__is_public=True, then=1), - default=0, output_field=IntegerField()))) + queryset = queryset.annotate( + has_public_editorial=Sum(Case(When(solution__is_public=True, then=1), + default=0, output_field=IntegerField()))) return queryset.distinct() @@ -474,6 +472,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView context['show_types'] = 0 if self.in_contest else int(self.show_types) context['full_text'] = 0 if self.in_contest else int(self.full_text) context['show_editorial'] = 0 if self.in_contest else int(self.show_editorial) + context['have_editorial'] = 0 if self.in_contest else int(self.have_editorial) context['organizations'] = Organization.objects.all() context['category'] = self.category @@ -486,7 +485,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView context['search_query'] = self.search_query context['completed_problem_ids'] = self.get_completed_problems() context['attempted_problems'] = self.get_attempted_problems() - + context['page_type'] = 'list' context.update(self.get_sort_paginate_context()) if not self.in_contest: context.update(self.get_sort_context()) @@ -498,6 +497,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView context['hide_contest_scoreboard'] = self.contest.scoreboard_visibility in \ (self.contest.SCOREBOARD_AFTER_CONTEST, self.contest.SCOREBOARD_AFTER_PARTICIPATION) context['has_clarifications'] = False + if self.request.user.is_authenticated: participation = self.request.profile.current_contest if participation: @@ -537,6 +537,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView self.show_types = self.GET_with_session(request, 'show_types') self.full_text = self.GET_with_session(request, 'full_text') self.show_editorial = self.GET_with_session(request, 'show_editorial') + self.have_editorial = self.GET_with_session(request, 'have_editorial') self.search_query = None self.category = None @@ -573,44 +574,33 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView return generic_message(request, 'FTS syntax error', e.args[1], status=400) def post(self, request, *args, **kwargs): - to_update = ('hide_solved', 'show_types', 'full_text') + to_update = ('hide_solved', 'show_types', 'full_text', + 'show_editorial', 'have_editorial') for key in to_update: if key in request.GET: val = request.GET.get(key) == '1' request.session[key] = val else: - request.session.pop(key, None) + request.session[key] = False return HttpResponseRedirect(request.get_full_path()) -class ProblemFeed(FeedView): +class ProblemFeed(ProblemList): model = Problem context_object_name = 'problems' paginate_by = 50 title = _('Problem feed') + feed_type = None - @cached_property - def profile(self): - if not self.request.user.is_authenticated: - return None - return self.request.profile + def GET_with_session(self, request, key): + if not request.GET: + return request.session.get(key, key=='hide_solved') + return request.GET.get(key, None) == '1' - def get_unsolved_queryset(self): - filter = Q(is_public=True) - if self.profile is not None: - filter |= Q(authors=self.profile) - filter |= Q(curators=self.profile) - filter |= Q(testers=self.profile) - queryset = Problem.objects.filter(filter).select_related('group').defer('description') - if not self.request.user.has_perm('see_organization_problem'): - filter = Q(is_organization_private=False) - if self.profile is not None: - filter |= Q(organizations__in=self.profile.organizations.all()) - queryset = queryset.filter(filter) - if self.profile is not None: - queryset = queryset.exclude(id__in=Submission.objects.filter(user=self.profile, points=F('problem__points')) - .values_list('problem__id', flat=True)) - return queryset.distinct() + def get_paginator(self, queryset, per_page, orphans=0, + allow_empty_first_page=True, **kwargs): + return DiggPaginator(queryset, per_page, body=6, padding=2, + orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs) # arr = [[], [], ..] def merge_recommendation(self, arr): @@ -636,31 +626,50 @@ class ProblemFeed(FeedView): stop = False return res - def get_queryset(self): - queryset = self.get_unsolved_queryset() + queryset = super(ProblemFeed, self).get_queryset() + + if self.have_editorial: + queryset = queryset.filter(has_public_editorial=1) + user = self.request.profile + + if self.feed_type == 'new': + return queryset.order_by('-date') if not settings.ML_OUTPUT_PATH or not user: return queryset.order_by('?') - cl_model = CollabFilter() - dot_rec = cl_model.user_recommendations(user, queryset, cl_model.DOT, 100) - cosine_rec = cl_model.user_recommendations(user, queryset, cl_model.COSINE, 100) - hot_problems_rec = [problem for problem in hot_problems(timedelta(days=7), 10) - if problem in queryset] + cf_model = CollabFilter('collab_filter') + cf_time_model = CollabFilter('collab_filter_time') + hot_problems_recommendations = [ + problem for problem in hot_problems(timedelta(days=7), 20) + if problem in queryset + ] - q = self.merge_recommendation([dot_rec, cosine_rec, hot_problems_rec]) + q = self.merge_recommendation([ + cf_model.user_recommendations(user, queryset, cf_model.DOT, 100), + cf_model.user_recommendations(user, queryset, cf_model.COSINE, 100), + cf_time_model.user_recommendations(user, queryset, cf_time_model.COSINE, 100), + cf_time_model.user_recommendations(user, queryset, cf_time_model.DOT, 100), + hot_problems_recommendations + ]) return q def get_context_data(self, **kwargs): context = super(ProblemFeed, self).get_context_data(**kwargs) context['first_page_href'] = self.request.path context['page_prefix'] = '?page=' - context['feed_type'] = 'problem' + context['page_type'] = 'feed' context['title'] = self.title + context['feed_type'] = self.feed_type return context + def get(self, request, *args, **kwargs): + if request.in_contest_mode: + return HttpResponseRedirect(reverse('problem_list')) + return super(ProblemFeed, self).get(request, *args, **kwargs) + class LanguageTemplateAjax(View): def get(self, request, *args, **kwargs): diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 463ec69..f2d8890 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-03-22 04:37+0700\n" +"POT-Creation-Date: 2022-04-13 12:01+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -112,27 +112,27 @@ msgstr "Đăng nhập" msgid "Home" msgstr "Trang chủ" -#: judge/admin/comments.py:40 +#: judge/admin/comments.py:41 #, python-format msgid "%d comment successfully hidden." msgid_plural "%d comments successfully hidden." msgstr[0] "Đã ẩn %d bình luận." -#: judge/admin/comments.py:43 +#: judge/admin/comments.py:44 msgid "Hide comments" msgstr "Ẩn bình luận" -#: judge/admin/comments.py:47 +#: judge/admin/comments.py:48 #, python-format msgid "%d comment successfully unhidden." msgid_plural "%d comments successfully unhidden." msgstr[0] "Không ẩn được %d bình luận." -#: judge/admin/comments.py:50 +#: judge/admin/comments.py:51 msgid "Unhide comments" msgstr "Hiện bình luận" -#: judge/admin/comments.py:58 +#: judge/admin/comments.py:59 msgid "Associated page" msgstr "Trang liên kết" @@ -142,8 +142,8 @@ msgstr "" #: judge/admin/contest.py:66 templates/contest/clarification.html:42 #: templates/contest/contest.html:83 templates/contest/moss.html:43 -#: templates/problem/list.html:214 templates/problem/list.html:232 -#: templates/problem/list.html:350 templates/user/user-problems.html:56 +#: templates/problem/list.html:189 templates/problem/list.html:205 +#: templates/problem/list.html:323 templates/user/user-problems.html:56 #: templates/user/user-problems.html:98 msgid "Problem" msgstr "Bài tập" @@ -173,7 +173,7 @@ msgstr "" msgid "Access" msgstr "Truy cập" -#: judge/admin/contest.py:127 judge/admin/problem.py:131 +#: judge/admin/contest.py:127 judge/admin/problem.py:133 msgid "Justice" msgstr "Xử phạt" @@ -225,7 +225,7 @@ msgstr "ảo" msgid "link path" msgstr "đường dẫn" -#: judge/admin/interface.py:65 templates/blog/list.html:114 +#: judge/admin/interface.py:65 msgid "Content" msgstr "Nội dung" @@ -237,78 +237,90 @@ msgstr "Tổng kết" msgid "object" msgstr "" -#: judge/admin/organization.py:34 judge/admin/problem.py:171 +#: judge/admin/organization.py:34 judge/admin/problem.py:173 #: judge/admin/profile.py:80 msgid "View on site" msgstr "Xem trên trang" -#: judge/admin/problem.py:28 +#: judge/admin/problem.py:30 msgid "Describe the changes you made (optional)" msgstr "Mô tả các thay đổi (tùy chọn)" -#: judge/admin/problem.py:126 +#: judge/admin/problem.py:128 msgid "Social Media" msgstr "Mạng Xã Hội" -#: judge/admin/problem.py:127 +#: judge/admin/problem.py:129 msgid "Taxonomy" msgstr "" -#: judge/admin/problem.py:128 judge/admin/problem.py:258 +#: judge/admin/problem.py:130 judge/admin/problem.py:284 #: templates/contest/contest.html:84 templates/problem/data.html:469 -#: templates/problem/list.html:222 templates/problem/list.html:248 +#: templates/problem/list.html:195 templates/problem/list.html:221 #: templates/user/base-users-table.html:10 templates/user/user-about.html:36 #: templates/user/user-about.html:52 templates/user/user-problems.html:58 msgid "Points" msgstr "Điểm" -#: judge/admin/problem.py:129 +#: judge/admin/problem.py:131 msgid "Limits" msgstr "Giới hạn" -#: judge/admin/problem.py:130 judge/admin/submission.py:232 +#: judge/admin/problem.py:132 judge/admin/submission.py:232 #: templates/stats/base.html:14 templates/submission/list.html:322 msgid "Language" msgstr "Ngôn ngữ" -#: judge/admin/problem.py:132 +#: judge/admin/problem.py:134 msgid "History" msgstr "Lịch sử" -#: judge/admin/problem.py:168 +#: judge/admin/problem.py:170 msgid "Authors" msgstr "Các tác giả" -#: judge/admin/problem.py:183 +#: judge/admin/problem.py:185 #, python-format msgid "%d problem successfully marked as public." msgid_plural "%d problems successfully marked as public." msgstr[0] "%d bài tập đã được đánh dấu công khai." -#: judge/admin/problem.py:187 +#: judge/admin/problem.py:189 msgid "Mark problems as public" msgstr "Công khai bài tập" -#: judge/admin/problem.py:193 +#: judge/admin/problem.py:195 #, python-format msgid "%d problem successfully marked as private." msgid_plural "%d problems successfully marked as private." msgstr[0] "%d bài tập đã được đánh dấu riêng tư." -#: judge/admin/problem.py:197 +#: judge/admin/problem.py:199 msgid "Mark problems as private" msgstr "Đánh dấu các bài tập là riêng tư" -#: judge/admin/problem.py:253 judge/admin/submission.py:200 -#: templates/problem/list.html:216 templates/problem/list.html:236 +#: judge/admin/problem.py:279 judge/admin/submission.py:200 +#: templates/problem/list.html:190 templates/problem/list.html:209 msgid "Problem code" msgstr "Mã bài" -#: judge/admin/problem.py:263 judge/admin/submission.py:205 +#: judge/admin/problem.py:289 judge/admin/submission.py:205 msgid "Problem name" msgstr "Tên bài" -#: judge/admin/problem.py:268 +#: judge/admin/problem.py:294 +#, fuzzy +#| msgid "contest rating" +msgid "Voter rating" +msgstr "rating kỳ thi" + +#: judge/admin/problem.py:299 +#, fuzzy +#| msgid "Total points" +msgid "Voter point" +msgstr "Tổng điểm" + +#: judge/admin/problem.py:304 msgid "Vote" msgstr "" @@ -357,7 +369,7 @@ msgstr "Các bài tập không được cho phép" msgid "These problems are NOT allowed to be submitted in this language" msgstr "Các bài này không cho phép sử dụng ngôn ngữ này" -#: judge/admin/runtime.py:83 templates/problem/list.html:352 +#: judge/admin/runtime.py:83 templates/problem/list.html:325 msgid "Description" msgstr "Mô tả" @@ -417,7 +429,7 @@ msgstr "Tính điểm lại cái bài nộp" #: judge/admin/submission.py:215 templates/notification/list.html:15 #: templates/organization/requests/log.html:10 #: templates/organization/requests/pending.html:13 -#: templates/problem/list.html:351 +#: templates/problem/list.html:324 #: templates/submission/status-testcases.html:125 #: templates/submission/status-testcases.html:127 msgid "Time" @@ -528,7 +540,7 @@ msgstr "Two Factor Authentication phải chứa 6 chữ số." msgid "Invalid Two Factor Authentication token." msgstr "Token Two Factor Authentication không hợp lệ." -#: judge/forms.py:148 judge/models/problem.py:97 +#: judge/forms.py:148 judge/models/problem.py:98 msgid "Problem code must be ^[a-z0-9]+$" msgstr "Mã bài phải có dạng ^[a-z0-9]+$" @@ -591,7 +603,7 @@ msgstr "người bình luận" msgid "associated page" msgstr "trang tương ứng" -#: judge/models/comment.py:46 judge/models/problem.py:493 +#: judge/models/comment.py:46 judge/models/problem.py:494 msgid "votes" msgstr "bình chọn" @@ -611,7 +623,7 @@ msgstr "bình luận" msgid "comments" msgstr "" -#: judge/models/comment.py:139 judge/models/problem.py:463 +#: judge/models/comment.py:139 judge/models/problem.py:464 #, python-format msgid "Editorial for %s" msgstr "" @@ -729,7 +741,7 @@ msgstr "" msgid "description" msgstr "mô tả" -#: judge/models/contest.py:72 judge/models/problem.py:410 +#: judge/models/contest.py:72 judge/models/problem.py:411 #: judge/models/runtime.py:138 msgid "problems" msgstr "bài tập" @@ -742,12 +754,12 @@ msgstr "thời gian bắt đầu" msgid "end time" msgstr "thời gian kết thúc" -#: judge/models/contest.py:75 judge/models/problem.py:118 -#: judge/models/problem.py:434 +#: judge/models/contest.py:75 judge/models/problem.py:119 +#: judge/models/problem.py:435 msgid "time limit" msgstr "giới hạn thời gian" -#: judge/models/contest.py:76 judge/models/problem.py:136 +#: judge/models/contest.py:76 judge/models/problem.py:137 msgid "publicly visible" msgstr "công khai" @@ -846,12 +858,12 @@ msgstr "" "kỳ thi, hãy bỏ đánh dấu ô này và chấm lại tất cả các bài." #: judge/models/contest.py:110 judge/models/interface.py:77 -#: judge/models/problem.py:157 +#: judge/models/problem.py:158 msgid "private to organizations" msgstr "riêng tư với các tổ chức" #: judge/models/contest.py:111 judge/models/interface.py:75 -#: judge/models/problem.py:155 judge/models/profile.py:77 +#: judge/models/problem.py:156 judge/models/profile.py:77 msgid "organizations" msgstr "tổ chức" @@ -859,7 +871,7 @@ msgstr "tổ chức" msgid "If private, only these organizations may see the contest" msgstr "Nếu riêng tư, chỉ những tổ chức này thấy được kỳ thi" -#: judge/models/contest.py:113 judge/models/problem.py:145 +#: judge/models/contest.py:113 judge/models/problem.py:146 msgid "OpenGraph image" msgstr "Hình ảnh OpenGraph" @@ -880,7 +892,7 @@ msgstr "số lượng thí sinh thi trực tiếp" msgid "contest summary" msgstr "tổng kết kỳ thi" -#: judge/models/contest.py:121 judge/models/problem.py:147 +#: judge/models/contest.py:121 judge/models/problem.py:148 msgid "Plain-text, shown in meta description tag, e.g. for social media." msgstr "" @@ -896,7 +908,7 @@ msgstr "" "Mật khẩu truy cập cho các thí sinh muốn tham gia kỳ thi. Để trống nếu không " "dùng." -#: judge/models/contest.py:125 judge/models/problem.py:141 +#: judge/models/contest.py:125 judge/models/problem.py:142 msgid "personae non gratae" msgstr "" @@ -1041,14 +1053,14 @@ msgid "contest participations" msgstr "lần tham gia kỳ thi" #: judge/models/contest.py:491 judge/models/contest.py:513 -#: judge/models/contest.py:554 judge/models/problem.py:409 -#: judge/models/problem.py:414 judge/models/problem.py:432 +#: judge/models/contest.py:554 judge/models/problem.py:410 +#: judge/models/problem.py:415 judge/models/problem.py:433 #: judge/models/problem_data.py:40 msgid "problem" msgstr "bài tập" #: judge/models/contest.py:493 judge/models/contest.py:517 -#: judge/models/problem.py:129 +#: judge/models/problem.py:130 msgid "points" msgstr "điểm" @@ -1180,7 +1192,7 @@ msgstr "mục cha" msgid "post title" msgstr "tiêu đề bài đăng" -#: judge/models/interface.py:67 judge/models/problem.py:452 +#: judge/models/interface.py:67 judge/models/problem.py:453 msgid "authors" msgstr "tác giả" @@ -1188,7 +1200,7 @@ msgstr "tác giả" msgid "slug" msgstr "slug" -#: judge/models/interface.py:69 judge/models/problem.py:450 +#: judge/models/interface.py:69 judge/models/problem.py:451 msgid "public visibility" msgstr "khả năng hiển thị công khai" @@ -1252,287 +1264,287 @@ msgstr "thời gian gửi" msgid "messages in the thread" msgstr "tin nhắn trong chuỗi" -#: judge/models/problem.py:26 +#: judge/models/problem.py:27 msgid "problem category ID" msgstr "mã của nhóm bài" -#: judge/models/problem.py:27 +#: judge/models/problem.py:28 msgid "problem category name" msgstr "tên nhóm bài" -#: judge/models/problem.py:34 +#: judge/models/problem.py:35 msgid "problem type" msgstr "dạng bài" -#: judge/models/problem.py:35 judge/models/problem.py:113 +#: judge/models/problem.py:36 judge/models/problem.py:114 msgid "problem types" msgstr "dạng bài" -#: judge/models/problem.py:39 +#: judge/models/problem.py:40 msgid "problem group ID" msgstr "mã của nhóm bài" -#: judge/models/problem.py:40 +#: judge/models/problem.py:41 msgid "problem group name" msgstr "tên nhóm bài" -#: judge/models/problem.py:47 judge/models/problem.py:116 +#: judge/models/problem.py:48 judge/models/problem.py:117 msgid "problem group" msgstr "nhóm bài" -#: judge/models/problem.py:48 +#: judge/models/problem.py:49 msgid "problem groups" msgstr "nhóm bài" -#: judge/models/problem.py:52 +#: judge/models/problem.py:53 msgid "key" msgstr "" -#: judge/models/problem.py:54 +#: judge/models/problem.py:55 msgid "link" msgstr "đường dẫn" -#: judge/models/problem.py:55 +#: judge/models/problem.py:56 msgid "full name" msgstr "tên đầy đủ" -#: judge/models/problem.py:56 judge/models/profile.py:33 +#: judge/models/problem.py:57 judge/models/profile.py:33 #: judge/models/runtime.py:24 msgid "short name" msgstr "tên ngắn" -#: judge/models/problem.py:57 +#: judge/models/problem.py:58 msgid "Displayed on pages under this license" msgstr "Được hiển thị trên các trang theo giấy phép này" -#: judge/models/problem.py:58 +#: judge/models/problem.py:59 msgid "icon" msgstr "icon" -#: judge/models/problem.py:58 +#: judge/models/problem.py:59 msgid "URL to the icon" msgstr "Đường dẫn icon" -#: judge/models/problem.py:59 +#: judge/models/problem.py:60 msgid "license text" msgstr "văn bản giấy phép" -#: judge/models/problem.py:68 +#: judge/models/problem.py:69 msgid "license" msgstr "" -#: judge/models/problem.py:69 +#: judge/models/problem.py:70 msgid "licenses" msgstr "" -#: judge/models/problem.py:96 +#: judge/models/problem.py:97 msgid "problem code" msgstr "" -#: judge/models/problem.py:98 +#: judge/models/problem.py:99 msgid "A short, unique code for the problem, used in the url after /problem/" msgstr "" -#: judge/models/problem.py:100 +#: judge/models/problem.py:101 msgid "problem name" msgstr "" -#: judge/models/problem.py:101 +#: judge/models/problem.py:102 msgid "The full name of the problem, as shown in the problem list." msgstr "" -#: judge/models/problem.py:103 +#: judge/models/problem.py:104 msgid "problem body" msgstr "" -#: judge/models/problem.py:104 +#: judge/models/problem.py:105 msgid "creators" msgstr "" -#: judge/models/problem.py:105 +#: judge/models/problem.py:106 msgid "These users will be able to edit the problem, and be listed as authors." msgstr "" -#: judge/models/problem.py:107 +#: judge/models/problem.py:108 msgid "curators" msgstr "" -#: judge/models/problem.py:108 +#: judge/models/problem.py:109 msgid "" "These users will be able to edit the problem, but not be listed as authors." msgstr "" -#: judge/models/problem.py:110 +#: judge/models/problem.py:111 msgid "testers" msgstr "" -#: judge/models/problem.py:112 +#: judge/models/problem.py:113 msgid "These users will be able to view the private problem, but not edit it." msgstr "" -#: judge/models/problem.py:114 +#: judge/models/problem.py:115 msgid "The type of problem, as shown on the problem's page." msgstr "" -#: judge/models/problem.py:117 +#: judge/models/problem.py:118 msgid "The group of problem, shown under Category in the problem list." msgstr "" -#: judge/models/problem.py:119 +#: judge/models/problem.py:120 msgid "" "The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) " "are supported." msgstr "" -#: judge/models/problem.py:123 judge/models/problem.py:437 +#: judge/models/problem.py:124 judge/models/problem.py:438 msgid "memory limit" msgstr "" -#: judge/models/problem.py:124 +#: judge/models/problem.py:125 msgid "" "The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 " "kilobytes)." msgstr "" -#: judge/models/problem.py:130 +#: judge/models/problem.py:131 msgid "" "Points awarded for problem completion. Points are displayed with a 'p' " "suffix if partial." msgstr "" -#: judge/models/problem.py:133 +#: judge/models/problem.py:134 msgid "allows partial points" msgstr "" -#: judge/models/problem.py:134 +#: judge/models/problem.py:135 msgid "allowed languages" msgstr "" -#: judge/models/problem.py:135 +#: judge/models/problem.py:136 msgid "List of allowed submission languages." msgstr "" -#: judge/models/problem.py:137 +#: judge/models/problem.py:138 msgid "manually managed" msgstr "" -#: judge/models/problem.py:138 +#: judge/models/problem.py:139 msgid "Whether judges should be allowed to manage data or not." msgstr "" -#: judge/models/problem.py:139 +#: judge/models/problem.py:140 msgid "date of publishing" msgstr "" -#: judge/models/problem.py:140 +#: judge/models/problem.py:141 msgid "" "Doesn't have magic ability to auto-publish due to backward compatibility" msgstr "" -#: judge/models/problem.py:142 +#: judge/models/problem.py:143 msgid "Bans the selected users from submitting to this problem." msgstr "" -#: judge/models/problem.py:144 +#: judge/models/problem.py:145 msgid "The license under which this problem is published." msgstr "" -#: judge/models/problem.py:146 +#: judge/models/problem.py:147 msgid "problem summary" msgstr "" -#: judge/models/problem.py:148 +#: judge/models/problem.py:149 msgid "number of users" msgstr "" -#: judge/models/problem.py:149 +#: judge/models/problem.py:150 msgid "The number of users who solved the problem." msgstr "" -#: judge/models/problem.py:150 +#: judge/models/problem.py:151 msgid "solve rate" msgstr "" -#: judge/models/problem.py:156 +#: judge/models/problem.py:157 msgid "If private, only these organizations may see the problem." msgstr "" -#: judge/models/problem.py:415 judge/models/problem.py:433 +#: judge/models/problem.py:416 judge/models/problem.py:434 #: judge/models/runtime.py:111 msgid "language" msgstr "" -#: judge/models/problem.py:416 +#: judge/models/problem.py:417 msgid "translated name" msgstr "" -#: judge/models/problem.py:417 +#: judge/models/problem.py:418 msgid "translated description" msgstr "" -#: judge/models/problem.py:421 +#: judge/models/problem.py:422 msgid "problem translation" msgstr "" -#: judge/models/problem.py:422 +#: judge/models/problem.py:423 msgid "problem translations" msgstr "" -#: judge/models/problem.py:426 +#: judge/models/problem.py:427 msgid "clarified problem" msgstr "" -#: judge/models/problem.py:427 +#: judge/models/problem.py:428 msgid "clarification body" msgstr "" -#: judge/models/problem.py:428 +#: judge/models/problem.py:429 msgid "clarification timestamp" msgstr "" -#: judge/models/problem.py:443 +#: judge/models/problem.py:444 msgid "language-specific resource limit" msgstr "" -#: judge/models/problem.py:444 +#: judge/models/problem.py:445 msgid "language-specific resource limits" msgstr "" -#: judge/models/problem.py:448 +#: judge/models/problem.py:449 msgid "associated problem" msgstr "" -#: judge/models/problem.py:451 +#: judge/models/problem.py:452 msgid "publish date" msgstr "" -#: judge/models/problem.py:453 +#: judge/models/problem.py:454 msgid "editorial content" msgstr "nội dung lời giải" -#: judge/models/problem.py:469 +#: judge/models/problem.py:470 msgid "solution" msgstr "lời giải" -#: judge/models/problem.py:470 +#: judge/models/problem.py:471 msgid "solutions" msgstr "lời giải" -#: judge/models/problem.py:475 +#: judge/models/problem.py:476 #, fuzzy #| msgid "point value" msgid "proposed point value" msgstr "điểm" -#: judge/models/problem.py:476 +#: judge/models/problem.py:477 msgid "The amount of points you think this problem deserves." msgstr "" -#: judge/models/problem.py:486 +#: judge/models/problem.py:487 msgid "The time this vote was cast" msgstr "" -#: judge/models/problem.py:492 +#: judge/models/problem.py:493 msgid "vote" msgstr "" @@ -2386,16 +2398,12 @@ msgid "Page %d of Posts" msgstr "Trang %d" #: judge/views/blog.py:134 -#, fuzzy -#| msgid "Ticket title" msgid "Ticket feed" -msgstr "Tiêu đề báo cáo" +msgstr "Báo cáo" #: judge/views/blog.py:152 -#, fuzzy -#| msgid "Comment body" msgid "Comment feed" -msgstr "Nội dung bình luận" +msgstr "Bình luận" #: judge/views/comment.py:28 msgid "Messing around, are we?" @@ -2710,42 +2718,40 @@ msgstr "Hướng dẫn cho {0}" msgid "Editorial for {0}" msgstr "Hướng dẫn cho {0}" -#: judge/views/problem.py:342 templates/blog/list.html:121 -#: templates/contest/contest.html:79 templates/user/user-about.html:28 -#: templates/user/user-tabs.html:5 templates/user/users-table.html:29 +#: judge/views/problem.py:342 templates/contest/contest.html:79 +#: templates/user/user-about.html:28 templates/user/user-tabs.html:5 +#: templates/user/users-table.html:29 msgid "Problems" msgstr "Bài tập" -#: judge/views/problem.py:589 -#, fuzzy -#| msgid "Problem code" +#: judge/views/problem.py:592 msgid "Problem feed" -msgstr "Mã bài" +msgstr "Bài tập" -#: judge/views/problem.py:685 +#: judge/views/problem.py:731 msgid "Banned from submitting" msgstr "Bị cấm nộp bài" -#: judge/views/problem.py:686 +#: judge/views/problem.py:732 msgid "" "You have been declared persona non grata for this problem. You are " "permanently barred from submitting this problem." msgstr "Bạn đã bị cấm nộp bài này." -#: judge/views/problem.py:700 +#: judge/views/problem.py:746 msgid "Too many submissions" msgstr "Quá nhiều lần nộp" -#: judge/views/problem.py:701 +#: judge/views/problem.py:747 msgid "You have exceeded the submission limit for this problem." msgstr "Bạn đã vượt quá số lần nộp cho bài này." -#: judge/views/problem.py:761 judge/views/problem.py:764 +#: judge/views/problem.py:807 judge/views/problem.py:810 #, python-format msgid "Submit to %(problem)s" msgstr "Nộp bài cho %(problem)s" -#: judge/views/problem.py:779 +#: judge/views/problem.py:825 msgid "Clone Problem" msgstr "Nhân bản bài tập" @@ -3228,10 +3234,8 @@ msgstr "Xem bài nộp" #: templates/admin/judge/problem/change_form.html:19 #: templates/admin/judge/problem/change_form.html:22 -#, fuzzy -#| msgid "View on site" msgid "View votes" -msgstr "Xem trên trang" +msgstr "Xem bình chọn" #: templates/admin/judge/profile/change_form.html:14 #: templates/admin/judge/profile/change_form.html:17 @@ -3322,61 +3326,57 @@ msgstr "" " vào %(time)s\n" " " -#: templates/blog/list.html:107 -msgid "Feed" -msgstr "" - -#: templates/blog/list.html:109 -msgid "Events" -msgstr "Sự kiện" - -#: templates/blog/list.html:117 +#: templates/blog/list.html:59 msgid "News" msgstr "Tin tức" -#: templates/blog/list.html:125 templates/comments/list.html:2 +#: templates/blog/list.html:60 templates/comments/list.html:2 msgid "Comments" msgstr "Bình luận" -#: templates/blog/list.html:129 +#: templates/blog/list.html:61 msgid "Tickets" msgstr "Báo cáo" -#: templates/blog/list.html:148 +#: templates/blog/list.html:62 +msgid "Events" +msgstr "Sự kiện" + +#: templates/blog/list.html:78 msgid "You have no ticket" msgstr "Bạn không có báo cáo" -#: templates/blog/list.html:163 templates/problem/list.html:347 +#: templates/blog/list.html:94 templates/problem/list.html:320 #: templates/problem/problem.html:407 msgid "Clarifications" msgstr "Thông báo" -#: templates/blog/list.html:169 +#: templates/blog/list.html:100 msgid "Add" msgstr "Thêm mới" -#: templates/blog/list.html:188 templates/problem/list.html:369 +#: templates/blog/list.html:119 templates/problem/list.html:342 #: templates/problem/problem.html:418 msgid "No clarifications have been made at this time." msgstr "Không có thông báo nào." -#: templates/blog/list.html:196 +#: templates/blog/list.html:127 msgid "Ongoing contests" msgstr "Kỳ thi đang diễn ra" -#: templates/blog/list.html:204 +#: templates/blog/list.html:135 msgid "Ends in" msgstr "Còn" -#: templates/blog/list.html:214 +#: templates/blog/list.html:145 msgid "Upcoming contests" msgstr "Kỳ thi sắp diễn ra" -#: templates/blog/list.html:230 +#: templates/blog/list.html:161 msgid "Top Rating" msgstr "Top Rating" -#: templates/blog/list.html:246 +#: templates/blog/list.html:177 msgid "Top Score" msgstr "Top Score" @@ -3593,7 +3593,7 @@ msgstr "Hôm nay" msgid "Next" msgstr "Tiếp" -#: templates/contest/contest-list-tabs.html:21 +#: templates/contest/contest-list-tabs.html:21 templates/problem/list.html:174 #: templates/problem/problem-list-tabs.html:5 msgid "List" msgstr "Danh sách" @@ -3714,11 +3714,12 @@ msgstr "Tỷ lệ AC" #: templates/contest/contest.html:86 templates/contest/list.html:237 #: templates/contest/list.html:291 templates/contest/list.html:371 -#: templates/problem/list.html:223 templates/problem/list.html:254 +#: templates/problem/list.html:196 templates/problem/list.html:227 msgid "Users" msgstr "Số lượng" -#: templates/contest/contest.html:111 templates/problem/list.html:330 +#: templates/contest/contest.html:111 templates/problem/list.html:231 +#: templates/problem/list.html:303 msgid "Editorial" msgstr "Hướng dẫn" @@ -3738,7 +3739,7 @@ msgstr "" msgid "Show" msgstr "Hiển thị" -#: templates/contest/list.html:102 templates/problem/list.html:68 +#: templates/contest/list.html:102 templates/problem/list.html:76 msgid "Organizations..." msgstr "Tổ chức..." @@ -3864,7 +3865,7 @@ msgstr "Thêm vào đó, chỉ những tổ chức này mới được tham gia msgid "Only the following organizations may access this contest:" msgstr "Chỉ những tổ chức sau được tham gia kỳ thi:" -#: templates/contest/ranking-table.html:9 templates/problem/search-form.html:35 +#: templates/contest/ranking-table.html:9 templates/problem/search-form.html:36 msgid "Organization" msgstr "Tổ chức" @@ -4249,32 +4250,40 @@ msgstr "" "viết hướng dẫn này.

Chép code từ bài hướng dẫn để nộp bài là " "hành vi có thể dẫn đến khóa tài khoản." -#: templates/problem/list.html:66 +#: templates/problem/list.html:74 msgid "Filter by type..." msgstr "Lọc theo dạng..." -#: templates/problem/list.html:193 -msgid "Hot problems" -msgstr "Bài tập mới" +#: templates/problem/list.html:173 +msgid "Feed" +msgstr "Gợi ý" -#: templates/problem/list.html:218 templates/problem/list.html:240 -#: templates/problem/search-form.html:45 templates/user/user-problems.html:57 +#: templates/problem/list.html:191 templates/problem/list.html:213 +#: templates/problem/search-form.html:46 templates/user/user-problems.html:57 msgid "Category" msgstr "Nhóm" -#: templates/problem/list.html:220 templates/problem/list.html:244 +#: templates/problem/list.html:193 templates/problem/list.html:217 msgid "Types" msgstr "Dạng" -#: templates/problem/list.html:251 +#: templates/problem/list.html:224 #, python-format msgid "AC %%" msgstr "AC %%" -#: templates/problem/list.html:342 +#: templates/problem/list.html:315 msgid "Add clarifications" msgstr "Thêm thông báo" +#: templates/problem/list.html:351 +msgid "FOR YOU" +msgstr "DÀNH CHO BẠN" + +#: templates/problem/list.html:354 +msgid "NEW" +msgstr "MỚI NHẤT" + #: templates/problem/manage_submission.html:55 msgid "Leave empty to not filter by language" msgstr "Để trống nếu không lọc theo ngôn ngữ" @@ -4492,40 +4501,40 @@ msgid "Search problems..." msgstr "Tìm bài tập..." #: templates/problem/search-form.html:14 -msgid "Full text search" -msgstr "" - -#: templates/problem/search-form.html:21 msgid "Hide solved problems" msgstr "Ẩn các bài đã giải" -#: templates/problem/search-form.html:27 +#: templates/problem/search-form.html:20 msgid "Show problem types" msgstr "Hiển thị dạng bài" -#: templates/problem/search-form.html:32 +#: templates/problem/search-form.html:26 msgid "Show editorial" msgstr "Hiển thị hướng dẫn" -#: templates/problem/search-form.html:48 templates/problem/search-form.html:50 +#: templates/problem/search-form.html:32 +msgid "Have editorial" +msgstr "Có hướng dẫn" + +#: templates/problem/search-form.html:49 templates/problem/search-form.html:51 #: templates/submission/submission-list-tabs.html:4 msgid "All" msgstr "Tất cả" -#: templates/problem/search-form.html:62 +#: templates/problem/search-form.html:63 msgid "Problem types" msgstr "Dạng bài" -#: templates/problem/search-form.html:73 +#: templates/problem/search-form.html:74 msgid "Point range" msgstr "Mốc điểm" -#: templates/problem/search-form.html:79 templates/submission/list.html:331 +#: templates/problem/search-form.html:80 templates/submission/list.html:331 #: templates/ticket/list.html:248 msgid "Go" msgstr "Lọc" -#: templates/problem/search-form.html:80 +#: templates/problem/search-form.html:81 msgid "Random" msgstr "Ngẫu nhiên" @@ -5340,6 +5349,9 @@ msgstr "Thông tin" msgid "Check all" msgstr "Chọn tất cả" +#~ msgid "Hot problems" +#~ msgstr "Bài tập mới" + #~ msgid "Discuss {0}" #~ msgstr "Thảo luận {0}" diff --git a/resources/blog.scss b/resources/blog.scss index 8cd8693..edfa1a3 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -1,6 +1,6 @@ @import "vars"; -.blog-content { +.middle-content { padding-right: 0em; vertical-align: top; margin-right: 0; @@ -32,11 +32,18 @@ } } -.blog-sidebar { +.blog-sidebar, .right-sidebar { width: 100%; margin-left: auto; } +.left-sidebar-item.active { + color: green; + .sidebar-icon { + color: green; + } +} + .blog-sidebox { h3 { padding-bottom: 0.25em; @@ -95,19 +102,21 @@ } .left-sidebar-item { display: inline-block; + margin-left: 2em; } - .blog-left-sidebar { + .left-sidebar { text-align: right; padding-right: 1em; margin-bottom: 1em; + border-radius: 7px; } } @media (min-width: 800px) { - .blog-content, .blog-sidebar { + .middle-content, .blog-sidebar, .right-sidebar { display: block !important; } - .blog-content { + .middle-content { margin-right: 1em !important; } @@ -115,20 +124,20 @@ display: none; } - #blog-container { + #three-col-container { display: flex; } - .blog-content { + .middle-content { max-width: 71.5%; margin-left: 10%; } - .blog-sidebar { + .blog-sidebar, .right-sidebar { width: 18%; } - .blog-left-sidebar { + .left-sidebar { width: 8%; margin-right: 1em; position: fixed; @@ -203,7 +212,7 @@ color: gray; } -.blog-left-sidebar { +.left-sidebar { background-color: #f0f1f3; color: #616161; } diff --git a/resources/fontawesome/fontawesome.css b/resources/fontawesome/fontawesome.css new file mode 100755 index 0000000..e7eb5fe --- /dev/null +++ b/resources/fontawesome/fontawesome.css @@ -0,0 +1,6338 @@ +/*! + * Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2022 Fonticons, Inc. + */ +.fa { + font-family: var(--fa-style-family, "Font Awesome 6 Free"); + font-weight: var(--fa-style, 900); } + +.fa, +.fas, +.fa-solid, +.far, +.fa-regular, +.fal, +.fa-light, +.fat, +.fa-thin, +.fad, +.fa-duotone, +.fab, +.fa-brands { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: var(--fa-display, inline-block); + font-style: normal; + font-variant: normal; + line-height: 1; + text-rendering: auto; } + +.fa-1x { + font-size: 1em; } + +.fa-2x { + font-size: 2em; } + +.fa-3x { + font-size: 3em; } + +.fa-4x { + font-size: 4em; } + +.fa-5x { + font-size: 5em; } + +.fa-6x { + font-size: 6em; } + +.fa-7x { + font-size: 7em; } + +.fa-8x { + font-size: 8em; } + +.fa-9x { + font-size: 9em; } + +.fa-10x { + font-size: 10em; } + +.fa-2xs { + font-size: 0.625em; + line-height: 0.1em; + vertical-align: 0.225em; } + +.fa-xs { + font-size: 0.75em; + line-height: 0.08333em; + vertical-align: 0.125em; } + +.fa-sm { + font-size: 0.875em; + line-height: 0.07143em; + vertical-align: 0.05357em; } + +.fa-lg { + font-size: 1.25em; + line-height: 0.05em; + vertical-align: -0.075em; } + +.fa-xl { + font-size: 1.5em; + line-height: 0.04167em; + vertical-align: -0.125em; } + +.fa-2xl { + font-size: 2em; + line-height: 0.03125em; + vertical-align: -0.1875em; } + +.fa-fw { + text-align: center; + width: 1.25em; } + +.fa-ul { + list-style-type: none; + margin-left: var(--fa-li-margin, 2.5em); + padding-left: 0; } + .fa-ul > li { + position: relative; } + +.fa-li { + left: calc(var(--fa-li-width, 2em) * -1); + position: absolute; + text-align: center; + width: var(--fa-li-width, 2em); + line-height: inherit; } + +.fa-border { + border-color: var(--fa-border-color, #eee); + border-radius: var(--fa-border-radius, 0.1em); + border-style: var(--fa-border-style, solid); + border-width: var(--fa-border-width, 0.08em); + padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); } + +.fa-pull-left { + float: left; + margin-right: var(--fa-pull-margin, 0.3em); } + +.fa-pull-right { + float: right; + margin-left: var(--fa-pull-margin, 0.3em); } + +.fa-beat { + -webkit-animation-name: fa-beat; + animation-name: fa-beat; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-bounce { + -webkit-animation-name: fa-bounce; + animation-name: fa-bounce; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); } + +.fa-fade { + -webkit-animation-name: fa-fade; + animation-name: fa-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-beat-fade { + -webkit-animation-name: fa-beat-fade; + animation-name: fa-beat-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-flip { + -webkit-animation-name: fa-flip; + animation-name: fa-flip; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-shake { + -webkit-animation-name: fa-shake; + animation-name: fa-shake; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 2s); + animation-duration: var(--fa-animation-duration, 2s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin-reverse { + --fa-animation-direction: reverse; } + +.fa-pulse, +.fa-spin-pulse { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); + animation-timing-function: var(--fa-animation-timing, steps(8)); } + +@media (prefers-reduced-motion: reduce) { + .fa-beat, + .fa-bounce, + .fa-fade, + .fa-beat-fade, + .fa-flip, + .fa-pulse, + .fa-shake, + .fa-spin, + .fa-spin-pulse { + -webkit-animation-delay: -1ms; + animation-delay: -1ms; + -webkit-animation-duration: 1ms; + animation-duration: 1ms; + -webkit-animation-iteration-count: 1; + animation-iteration-count: 1; + transition-delay: 0s; + transition-duration: 0s; } } + +@-webkit-keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@-webkit-keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@-webkit-keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@-webkit-keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@-webkit-keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@-webkit-keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +.fa-rotate-90 { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +.fa-flip-both, +.fa-flip-horizontal.fa-flip-vertical { + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); } + +.fa-rotate-by { + -webkit-transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, none)); } + +.fa-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2.5em; } + +.fa-stack-1x, +.fa-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; + z-index: var(--fa-stack-z-index, auto); } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: var(--fa-inverse, #fff); } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ +.fa-0::before { + content: "\30"; } + +.fa-1::before { + content: "\31"; } + +.fa-2::before { + content: "\32"; } + +.fa-3::before { + content: "\33"; } + +.fa-4::before { + content: "\34"; } + +.fa-5::before { + content: "\35"; } + +.fa-6::before { + content: "\36"; } + +.fa-7::before { + content: "\37"; } + +.fa-8::before { + content: "\38"; } + +.fa-9::before { + content: "\39"; } + +.fa-a::before { + content: "\41"; } + +.fa-address-book::before { + content: "\f2b9"; } + +.fa-contact-book::before { + content: "\f2b9"; } + +.fa-address-card::before { + content: "\f2bb"; } + +.fa-contact-card::before { + content: "\f2bb"; } + +.fa-vcard::before { + content: "\f2bb"; } + +.fa-align-center::before { + content: "\f037"; } + +.fa-align-justify::before { + content: "\f039"; } + +.fa-align-left::before { + content: "\f036"; } + +.fa-align-right::before { + content: "\f038"; } + +.fa-anchor::before { + content: "\f13d"; } + +.fa-anchor-circle-check::before { + content: "\e4aa"; } + +.fa-anchor-circle-exclamation::before { + content: "\e4ab"; } + +.fa-anchor-circle-xmark::before { + content: "\e4ac"; } + +.fa-anchor-lock::before { + content: "\e4ad"; } + +.fa-angle-down::before { + content: "\f107"; } + +.fa-angle-left::before { + content: "\f104"; } + +.fa-angle-right::before { + content: "\f105"; } + +.fa-angle-up::before { + content: "\f106"; } + +.fa-angles-down::before { + content: "\f103"; } + +.fa-angle-double-down::before { + content: "\f103"; } + +.fa-angles-left::before { + content: "\f100"; } + +.fa-angle-double-left::before { + content: "\f100"; } + +.fa-angles-right::before { + content: "\f101"; } + +.fa-angle-double-right::before { + content: "\f101"; } + +.fa-angles-up::before { + content: "\f102"; } + +.fa-angle-double-up::before { + content: "\f102"; } + +.fa-ankh::before { + content: "\f644"; } + +.fa-apple-whole::before { + content: "\f5d1"; } + +.fa-apple-alt::before { + content: "\f5d1"; } + +.fa-archway::before { + content: "\f557"; } + +.fa-arrow-down::before { + content: "\f063"; } + +.fa-arrow-down-1-9::before { + content: "\f162"; } + +.fa-sort-numeric-asc::before { + content: "\f162"; } + +.fa-sort-numeric-down::before { + content: "\f162"; } + +.fa-arrow-down-9-1::before { + content: "\f886"; } + +.fa-sort-numeric-desc::before { + content: "\f886"; } + +.fa-sort-numeric-down-alt::before { + content: "\f886"; } + +.fa-arrow-down-a-z::before { + content: "\f15d"; } + +.fa-sort-alpha-asc::before { + content: "\f15d"; } + +.fa-sort-alpha-down::before { + content: "\f15d"; } + +.fa-arrow-down-long::before { + content: "\f175"; } + +.fa-long-arrow-down::before { + content: "\f175"; } + +.fa-arrow-down-short-wide::before { + content: "\f884"; } + +.fa-sort-amount-desc::before { + content: "\f884"; } + +.fa-sort-amount-down-alt::before { + content: "\f884"; } + +.fa-arrow-down-up-across-line::before { + content: "\e4af"; } + +.fa-arrow-down-up-lock::before { + content: "\e4b0"; } + +.fa-arrow-down-wide-short::before { + content: "\f160"; } + +.fa-sort-amount-asc::before { + content: "\f160"; } + +.fa-sort-amount-down::before { + content: "\f160"; } + +.fa-arrow-down-z-a::before { + content: "\f881"; } + +.fa-sort-alpha-desc::before { + content: "\f881"; } + +.fa-sort-alpha-down-alt::before { + content: "\f881"; } + +.fa-arrow-left::before { + content: "\f060"; } + +.fa-arrow-left-long::before { + content: "\f177"; } + +.fa-long-arrow-left::before { + content: "\f177"; } + +.fa-arrow-pointer::before { + content: "\f245"; } + +.fa-mouse-pointer::before { + content: "\f245"; } + +.fa-arrow-right::before { + content: "\f061"; } + +.fa-arrow-right-arrow-left::before { + content: "\f0ec"; } + +.fa-exchange::before { + content: "\f0ec"; } + +.fa-arrow-right-from-bracket::before { + content: "\f08b"; } + +.fa-sign-out::before { + content: "\f08b"; } + +.fa-arrow-right-long::before { + content: "\f178"; } + +.fa-long-arrow-right::before { + content: "\f178"; } + +.fa-arrow-right-to-bracket::before { + content: "\f090"; } + +.fa-sign-in::before { + content: "\f090"; } + +.fa-arrow-right-to-city::before { + content: "\e4b3"; } + +.fa-arrow-rotate-left::before { + content: "\f0e2"; } + +.fa-arrow-left-rotate::before { + content: "\f0e2"; } + +.fa-arrow-rotate-back::before { + content: "\f0e2"; } + +.fa-arrow-rotate-backward::before { + content: "\f0e2"; } + +.fa-undo::before { + content: "\f0e2"; } + +.fa-arrow-rotate-right::before { + content: "\f01e"; } + +.fa-arrow-right-rotate::before { + content: "\f01e"; } + +.fa-arrow-rotate-forward::before { + content: "\f01e"; } + +.fa-redo::before { + content: "\f01e"; } + +.fa-arrow-trend-down::before { + content: "\e097"; } + +.fa-arrow-trend-up::before { + content: "\e098"; } + +.fa-arrow-turn-down::before { + content: "\f149"; } + +.fa-level-down::before { + content: "\f149"; } + +.fa-arrow-turn-up::before { + content: "\f148"; } + +.fa-level-up::before { + content: "\f148"; } + +.fa-arrow-up::before { + content: "\f062"; } + +.fa-arrow-up-1-9::before { + content: "\f163"; } + +.fa-sort-numeric-up::before { + content: "\f163"; } + +.fa-arrow-up-9-1::before { + content: "\f887"; } + +.fa-sort-numeric-up-alt::before { + content: "\f887"; } + +.fa-arrow-up-a-z::before { + content: "\f15e"; } + +.fa-sort-alpha-up::before { + content: "\f15e"; } + +.fa-arrow-up-from-bracket::before { + content: "\e09a"; } + +.fa-arrow-up-from-ground-water::before { + content: "\e4b5"; } + +.fa-arrow-up-from-water-pump::before { + content: "\e4b6"; } + +.fa-arrow-up-long::before { + content: "\f176"; } + +.fa-long-arrow-up::before { + content: "\f176"; } + +.fa-arrow-up-right-dots::before { + content: "\e4b7"; } + +.fa-arrow-up-right-from-square::before { + content: "\f08e"; } + +.fa-external-link::before { + content: "\f08e"; } + +.fa-arrow-up-short-wide::before { + content: "\f885"; } + +.fa-sort-amount-up-alt::before { + content: "\f885"; } + +.fa-arrow-up-wide-short::before { + content: "\f161"; } + +.fa-sort-amount-up::before { + content: "\f161"; } + +.fa-arrow-up-z-a::before { + content: "\f882"; } + +.fa-sort-alpha-up-alt::before { + content: "\f882"; } + +.fa-arrows-down-to-line::before { + content: "\e4b8"; } + +.fa-arrows-down-to-people::before { + content: "\e4b9"; } + +.fa-arrows-left-right::before { + content: "\f07e"; } + +.fa-arrows-h::before { + content: "\f07e"; } + +.fa-arrows-left-right-to-line::before { + content: "\e4ba"; } + +.fa-arrows-rotate::before { + content: "\f021"; } + +.fa-refresh::before { + content: "\f021"; } + +.fa-sync::before { + content: "\f021"; } + +.fa-arrows-spin::before { + content: "\e4bb"; } + +.fa-arrows-split-up-and-left::before { + content: "\e4bc"; } + +.fa-arrows-to-circle::before { + content: "\e4bd"; } + +.fa-arrows-to-dot::before { + content: "\e4be"; } + +.fa-arrows-to-eye::before { + content: "\e4bf"; } + +.fa-arrows-turn-right::before { + content: "\e4c0"; } + +.fa-arrows-turn-to-dots::before { + content: "\e4c1"; } + +.fa-arrows-up-down::before { + content: "\f07d"; } + +.fa-arrows-v::before { + content: "\f07d"; } + +.fa-arrows-up-down-left-right::before { + content: "\f047"; } + +.fa-arrows::before { + content: "\f047"; } + +.fa-arrows-up-to-line::before { + content: "\e4c2"; } + +.fa-asterisk::before { + content: "\2a"; } + +.fa-at::before { + content: "\40"; } + +.fa-atom::before { + content: "\f5d2"; } + +.fa-audio-description::before { + content: "\f29e"; } + +.fa-austral-sign::before { + content: "\e0a9"; } + +.fa-award::before { + content: "\f559"; } + +.fa-b::before { + content: "\42"; } + +.fa-baby::before { + content: "\f77c"; } + +.fa-baby-carriage::before { + content: "\f77d"; } + +.fa-carriage-baby::before { + content: "\f77d"; } + +.fa-backward::before { + content: "\f04a"; } + +.fa-backward-fast::before { + content: "\f049"; } + +.fa-fast-backward::before { + content: "\f049"; } + +.fa-backward-step::before { + content: "\f048"; } + +.fa-step-backward::before { + content: "\f048"; } + +.fa-bacon::before { + content: "\f7e5"; } + +.fa-bacteria::before { + content: "\e059"; } + +.fa-bacterium::before { + content: "\e05a"; } + +.fa-bag-shopping::before { + content: "\f290"; } + +.fa-shopping-bag::before { + content: "\f290"; } + +.fa-bahai::before { + content: "\f666"; } + +.fa-baht-sign::before { + content: "\e0ac"; } + +.fa-ban::before { + content: "\f05e"; } + +.fa-cancel::before { + content: "\f05e"; } + +.fa-ban-smoking::before { + content: "\f54d"; } + +.fa-smoking-ban::before { + content: "\f54d"; } + +.fa-bandage::before { + content: "\f462"; } + +.fa-band-aid::before { + content: "\f462"; } + +.fa-barcode::before { + content: "\f02a"; } + +.fa-bars::before { + content: "\f0c9"; } + +.fa-navicon::before { + content: "\f0c9"; } + +.fa-bars-progress::before { + content: "\f828"; } + +.fa-tasks-alt::before { + content: "\f828"; } + +.fa-bars-staggered::before { + content: "\f550"; } + +.fa-reorder::before { + content: "\f550"; } + +.fa-stream::before { + content: "\f550"; } + +.fa-baseball::before { + content: "\f433"; } + +.fa-baseball-ball::before { + content: "\f433"; } + +.fa-baseball-bat-ball::before { + content: "\f432"; } + +.fa-basket-shopping::before { + content: "\f291"; } + +.fa-shopping-basket::before { + content: "\f291"; } + +.fa-basketball::before { + content: "\f434"; } + +.fa-basketball-ball::before { + content: "\f434"; } + +.fa-bath::before { + content: "\f2cd"; } + +.fa-bathtub::before { + content: "\f2cd"; } + +.fa-battery-empty::before { + content: "\f244"; } + +.fa-battery-0::before { + content: "\f244"; } + +.fa-battery-full::before { + content: "\f240"; } + +.fa-battery::before { + content: "\f240"; } + +.fa-battery-5::before { + content: "\f240"; } + +.fa-battery-half::before { + content: "\f242"; } + +.fa-battery-3::before { + content: "\f242"; } + +.fa-battery-quarter::before { + content: "\f243"; } + +.fa-battery-2::before { + content: "\f243"; } + +.fa-battery-three-quarters::before { + content: "\f241"; } + +.fa-battery-4::before { + content: "\f241"; } + +.fa-bed::before { + content: "\f236"; } + +.fa-bed-pulse::before { + content: "\f487"; } + +.fa-procedures::before { + content: "\f487"; } + +.fa-beer-mug-empty::before { + content: "\f0fc"; } + +.fa-beer::before { + content: "\f0fc"; } + +.fa-bell::before { + content: "\f0f3"; } + +.fa-bell-concierge::before { + content: "\f562"; } + +.fa-concierge-bell::before { + content: "\f562"; } + +.fa-bell-slash::before { + content: "\f1f6"; } + +.fa-bezier-curve::before { + content: "\f55b"; } + +.fa-bicycle::before { + content: "\f206"; } + +.fa-binoculars::before { + content: "\f1e5"; } + +.fa-biohazard::before { + content: "\f780"; } + +.fa-bitcoin-sign::before { + content: "\e0b4"; } + +.fa-blender::before { + content: "\f517"; } + +.fa-blender-phone::before { + content: "\f6b6"; } + +.fa-blog::before { + content: "\f781"; } + +.fa-bold::before { + content: "\f032"; } + +.fa-bolt::before { + content: "\f0e7"; } + +.fa-zap::before { + content: "\f0e7"; } + +.fa-bolt-lightning::before { + content: "\e0b7"; } + +.fa-bomb::before { + content: "\f1e2"; } + +.fa-bone::before { + content: "\f5d7"; } + +.fa-bong::before { + content: "\f55c"; } + +.fa-book::before { + content: "\f02d"; } + +.fa-book-atlas::before { + content: "\f558"; } + +.fa-atlas::before { + content: "\f558"; } + +.fa-book-bible::before { + content: "\f647"; } + +.fa-bible::before { + content: "\f647"; } + +.fa-book-bookmark::before { + content: "\e0bb"; } + +.fa-book-journal-whills::before { + content: "\f66a"; } + +.fa-journal-whills::before { + content: "\f66a"; } + +.fa-book-medical::before { + content: "\f7e6"; } + +.fa-book-open::before { + content: "\f518"; } + +.fa-book-open-reader::before { + content: "\f5da"; } + +.fa-book-reader::before { + content: "\f5da"; } + +.fa-book-quran::before { + content: "\f687"; } + +.fa-quran::before { + content: "\f687"; } + +.fa-book-skull::before { + content: "\f6b7"; } + +.fa-book-dead::before { + content: "\f6b7"; } + +.fa-bookmark::before { + content: "\f02e"; } + +.fa-border-all::before { + content: "\f84c"; } + +.fa-border-none::before { + content: "\f850"; } + +.fa-border-top-left::before { + content: "\f853"; } + +.fa-border-style::before { + content: "\f853"; } + +.fa-bore-hole::before { + content: "\e4c3"; } + +.fa-bottle-droplet::before { + content: "\e4c4"; } + +.fa-bottle-water::before { + content: "\e4c5"; } + +.fa-bowl-food::before { + content: "\e4c6"; } + +.fa-bowl-rice::before { + content: "\e2eb"; } + +.fa-bowling-ball::before { + content: "\f436"; } + +.fa-box::before { + content: "\f466"; } + +.fa-box-archive::before { + content: "\f187"; } + +.fa-archive::before { + content: "\f187"; } + +.fa-box-open::before { + content: "\f49e"; } + +.fa-box-tissue::before { + content: "\e05b"; } + +.fa-boxes-packing::before { + content: "\e4c7"; } + +.fa-boxes-stacked::before { + content: "\f468"; } + +.fa-boxes::before { + content: "\f468"; } + +.fa-boxes-alt::before { + content: "\f468"; } + +.fa-braille::before { + content: "\f2a1"; } + +.fa-brain::before { + content: "\f5dc"; } + +.fa-brazilian-real-sign::before { + content: "\e46c"; } + +.fa-bread-slice::before { + content: "\f7ec"; } + +.fa-bridge::before { + content: "\e4c8"; } + +.fa-bridge-circle-check::before { + content: "\e4c9"; } + +.fa-bridge-circle-exclamation::before { + content: "\e4ca"; } + +.fa-bridge-circle-xmark::before { + content: "\e4cb"; } + +.fa-bridge-lock::before { + content: "\e4cc"; } + +.fa-bridge-water::before { + content: "\e4ce"; } + +.fa-briefcase::before { + content: "\f0b1"; } + +.fa-briefcase-medical::before { + content: "\f469"; } + +.fa-broom::before { + content: "\f51a"; } + +.fa-broom-ball::before { + content: "\f458"; } + +.fa-quidditch::before { + content: "\f458"; } + +.fa-quidditch-broom-ball::before { + content: "\f458"; } + +.fa-brush::before { + content: "\f55d"; } + +.fa-bucket::before { + content: "\e4cf"; } + +.fa-bug::before { + content: "\f188"; } + +.fa-bug-slash::before { + content: "\e490"; } + +.fa-bugs::before { + content: "\e4d0"; } + +.fa-building::before { + content: "\f1ad"; } + +.fa-building-circle-arrow-right::before { + content: "\e4d1"; } + +.fa-building-circle-check::before { + content: "\e4d2"; } + +.fa-building-circle-exclamation::before { + content: "\e4d3"; } + +.fa-building-circle-xmark::before { + content: "\e4d4"; } + +.fa-building-columns::before { + content: "\f19c"; } + +.fa-bank::before { + content: "\f19c"; } + +.fa-institution::before { + content: "\f19c"; } + +.fa-museum::before { + content: "\f19c"; } + +.fa-university::before { + content: "\f19c"; } + +.fa-building-flag::before { + content: "\e4d5"; } + +.fa-building-lock::before { + content: "\e4d6"; } + +.fa-building-ngo::before { + content: "\e4d7"; } + +.fa-building-shield::before { + content: "\e4d8"; } + +.fa-building-un::before { + content: "\e4d9"; } + +.fa-building-user::before { + content: "\e4da"; } + +.fa-building-wheat::before { + content: "\e4db"; } + +.fa-bullhorn::before { + content: "\f0a1"; } + +.fa-bullseye::before { + content: "\f140"; } + +.fa-burger::before { + content: "\f805"; } + +.fa-hamburger::before { + content: "\f805"; } + +.fa-burst::before { + content: "\e4dc"; } + +.fa-bus::before { + content: "\f207"; } + +.fa-bus-simple::before { + content: "\f55e"; } + +.fa-bus-alt::before { + content: "\f55e"; } + +.fa-business-time::before { + content: "\f64a"; } + +.fa-briefcase-clock::before { + content: "\f64a"; } + +.fa-c::before { + content: "\43"; } + +.fa-cake-candles::before { + content: "\f1fd"; } + +.fa-birthday-cake::before { + content: "\f1fd"; } + +.fa-cake::before { + content: "\f1fd"; } + +.fa-calculator::before { + content: "\f1ec"; } + +.fa-calendar::before { + content: "\f133"; } + +.fa-calendar-check::before { + content: "\f274"; } + +.fa-calendar-day::before { + content: "\f783"; } + +.fa-calendar-days::before { + content: "\f073"; } + +.fa-calendar-alt::before { + content: "\f073"; } + +.fa-calendar-minus::before { + content: "\f272"; } + +.fa-calendar-plus::before { + content: "\f271"; } + +.fa-calendar-week::before { + content: "\f784"; } + +.fa-calendar-xmark::before { + content: "\f273"; } + +.fa-calendar-times::before { + content: "\f273"; } + +.fa-camera::before { + content: "\f030"; } + +.fa-camera-alt::before { + content: "\f030"; } + +.fa-camera-retro::before { + content: "\f083"; } + +.fa-camera-rotate::before { + content: "\e0d8"; } + +.fa-campground::before { + content: "\f6bb"; } + +.fa-candy-cane::before { + content: "\f786"; } + +.fa-cannabis::before { + content: "\f55f"; } + +.fa-capsules::before { + content: "\f46b"; } + +.fa-car::before { + content: "\f1b9"; } + +.fa-automobile::before { + content: "\f1b9"; } + +.fa-car-battery::before { + content: "\f5df"; } + +.fa-battery-car::before { + content: "\f5df"; } + +.fa-car-burst::before { + content: "\f5e1"; } + +.fa-car-crash::before { + content: "\f5e1"; } + +.fa-car-on::before { + content: "\e4dd"; } + +.fa-car-rear::before { + content: "\f5de"; } + +.fa-car-alt::before { + content: "\f5de"; } + +.fa-car-side::before { + content: "\f5e4"; } + +.fa-car-tunnel::before { + content: "\e4de"; } + +.fa-caravan::before { + content: "\f8ff"; } + +.fa-caret-down::before { + content: "\f0d7"; } + +.fa-caret-left::before { + content: "\f0d9"; } + +.fa-caret-right::before { + content: "\f0da"; } + +.fa-caret-up::before { + content: "\f0d8"; } + +.fa-carrot::before { + content: "\f787"; } + +.fa-cart-arrow-down::before { + content: "\f218"; } + +.fa-cart-flatbed::before { + content: "\f474"; } + +.fa-dolly-flatbed::before { + content: "\f474"; } + +.fa-cart-flatbed-suitcase::before { + content: "\f59d"; } + +.fa-luggage-cart::before { + content: "\f59d"; } + +.fa-cart-plus::before { + content: "\f217"; } + +.fa-cart-shopping::before { + content: "\f07a"; } + +.fa-shopping-cart::before { + content: "\f07a"; } + +.fa-cash-register::before { + content: "\f788"; } + +.fa-cat::before { + content: "\f6be"; } + +.fa-cedi-sign::before { + content: "\e0df"; } + +.fa-cent-sign::before { + content: "\e3f5"; } + +.fa-certificate::before { + content: "\f0a3"; } + +.fa-chair::before { + content: "\f6c0"; } + +.fa-chalkboard::before { + content: "\f51b"; } + +.fa-blackboard::before { + content: "\f51b"; } + +.fa-chalkboard-user::before { + content: "\f51c"; } + +.fa-chalkboard-teacher::before { + content: "\f51c"; } + +.fa-champagne-glasses::before { + content: "\f79f"; } + +.fa-glass-cheers::before { + content: "\f79f"; } + +.fa-charging-station::before { + content: "\f5e7"; } + +.fa-chart-area::before { + content: "\f1fe"; } + +.fa-area-chart::before { + content: "\f1fe"; } + +.fa-chart-bar::before { + content: "\f080"; } + +.fa-bar-chart::before { + content: "\f080"; } + +.fa-chart-column::before { + content: "\e0e3"; } + +.fa-chart-gantt::before { + content: "\e0e4"; } + +.fa-chart-line::before { + content: "\f201"; } + +.fa-line-chart::before { + content: "\f201"; } + +.fa-chart-pie::before { + content: "\f200"; } + +.fa-pie-chart::before { + content: "\f200"; } + +.fa-chart-simple::before { + content: "\e473"; } + +.fa-check::before { + content: "\f00c"; } + +.fa-check-double::before { + content: "\f560"; } + +.fa-check-to-slot::before { + content: "\f772"; } + +.fa-vote-yea::before { + content: "\f772"; } + +.fa-cheese::before { + content: "\f7ef"; } + +.fa-chess::before { + content: "\f439"; } + +.fa-chess-bishop::before { + content: "\f43a"; } + +.fa-chess-board::before { + content: "\f43c"; } + +.fa-chess-king::before { + content: "\f43f"; } + +.fa-chess-knight::before { + content: "\f441"; } + +.fa-chess-pawn::before { + content: "\f443"; } + +.fa-chess-queen::before { + content: "\f445"; } + +.fa-chess-rook::before { + content: "\f447"; } + +.fa-chevron-down::before { + content: "\f078"; } + +.fa-chevron-left::before { + content: "\f053"; } + +.fa-chevron-right::before { + content: "\f054"; } + +.fa-chevron-up::before { + content: "\f077"; } + +.fa-child::before { + content: "\f1ae"; } + +.fa-child-dress::before { + content: "\e59c"; } + +.fa-child-reaching::before { + content: "\e59d"; } + +.fa-child-rifle::before { + content: "\e4e0"; } + +.fa-children::before { + content: "\e4e1"; } + +.fa-church::before { + content: "\f51d"; } + +.fa-circle::before { + content: "\f111"; } + +.fa-circle-arrow-down::before { + content: "\f0ab"; } + +.fa-arrow-circle-down::before { + content: "\f0ab"; } + +.fa-circle-arrow-left::before { + content: "\f0a8"; } + +.fa-arrow-circle-left::before { + content: "\f0a8"; } + +.fa-circle-arrow-right::before { + content: "\f0a9"; } + +.fa-arrow-circle-right::before { + content: "\f0a9"; } + +.fa-circle-arrow-up::before { + content: "\f0aa"; } + +.fa-arrow-circle-up::before { + content: "\f0aa"; } + +.fa-circle-check::before { + content: "\f058"; } + +.fa-check-circle::before { + content: "\f058"; } + +.fa-circle-chevron-down::before { + content: "\f13a"; } + +.fa-chevron-circle-down::before { + content: "\f13a"; } + +.fa-circle-chevron-left::before { + content: "\f137"; } + +.fa-chevron-circle-left::before { + content: "\f137"; } + +.fa-circle-chevron-right::before { + content: "\f138"; } + +.fa-chevron-circle-right::before { + content: "\f138"; } + +.fa-circle-chevron-up::before { + content: "\f139"; } + +.fa-chevron-circle-up::before { + content: "\f139"; } + +.fa-circle-dollar-to-slot::before { + content: "\f4b9"; } + +.fa-donate::before { + content: "\f4b9"; } + +.fa-circle-dot::before { + content: "\f192"; } + +.fa-dot-circle::before { + content: "\f192"; } + +.fa-circle-down::before { + content: "\f358"; } + +.fa-arrow-alt-circle-down::before { + content: "\f358"; } + +.fa-circle-exclamation::before { + content: "\f06a"; } + +.fa-exclamation-circle::before { + content: "\f06a"; } + +.fa-circle-h::before { + content: "\f47e"; } + +.fa-hospital-symbol::before { + content: "\f47e"; } + +.fa-circle-half-stroke::before { + content: "\f042"; } + +.fa-adjust::before { + content: "\f042"; } + +.fa-circle-info::before { + content: "\f05a"; } + +.fa-info-circle::before { + content: "\f05a"; } + +.fa-circle-left::before { + content: "\f359"; } + +.fa-arrow-alt-circle-left::before { + content: "\f359"; } + +.fa-circle-minus::before { + content: "\f056"; } + +.fa-minus-circle::before { + content: "\f056"; } + +.fa-circle-nodes::before { + content: "\e4e2"; } + +.fa-circle-notch::before { + content: "\f1ce"; } + +.fa-circle-pause::before { + content: "\f28b"; } + +.fa-pause-circle::before { + content: "\f28b"; } + +.fa-circle-play::before { + content: "\f144"; } + +.fa-play-circle::before { + content: "\f144"; } + +.fa-circle-plus::before { + content: "\f055"; } + +.fa-plus-circle::before { + content: "\f055"; } + +.fa-circle-question::before { + content: "\f059"; } + +.fa-question-circle::before { + content: "\f059"; } + +.fa-circle-radiation::before { + content: "\f7ba"; } + +.fa-radiation-alt::before { + content: "\f7ba"; } + +.fa-circle-right::before { + content: "\f35a"; } + +.fa-arrow-alt-circle-right::before { + content: "\f35a"; } + +.fa-circle-stop::before { + content: "\f28d"; } + +.fa-stop-circle::before { + content: "\f28d"; } + +.fa-circle-up::before { + content: "\f35b"; } + +.fa-arrow-alt-circle-up::before { + content: "\f35b"; } + +.fa-circle-user::before { + content: "\f2bd"; } + +.fa-user-circle::before { + content: "\f2bd"; } + +.fa-circle-xmark::before { + content: "\f057"; } + +.fa-times-circle::before { + content: "\f057"; } + +.fa-xmark-circle::before { + content: "\f057"; } + +.fa-city::before { + content: "\f64f"; } + +.fa-clapperboard::before { + content: "\e131"; } + +.fa-clipboard::before { + content: "\f328"; } + +.fa-clipboard-check::before { + content: "\f46c"; } + +.fa-clipboard-list::before { + content: "\f46d"; } + +.fa-clipboard-question::before { + content: "\e4e3"; } + +.fa-clipboard-user::before { + content: "\f7f3"; } + +.fa-clock::before { + content: "\f017"; } + +.fa-clock-four::before { + content: "\f017"; } + +.fa-clock-rotate-left::before { + content: "\f1da"; } + +.fa-history::before { + content: "\f1da"; } + +.fa-clone::before { + content: "\f24d"; } + +.fa-closed-captioning::before { + content: "\f20a"; } + +.fa-cloud::before { + content: "\f0c2"; } + +.fa-cloud-arrow-down::before { + content: "\f0ed"; } + +.fa-cloud-download::before { + content: "\f0ed"; } + +.fa-cloud-download-alt::before { + content: "\f0ed"; } + +.fa-cloud-arrow-up::before { + content: "\f0ee"; } + +.fa-cloud-upload::before { + content: "\f0ee"; } + +.fa-cloud-upload-alt::before { + content: "\f0ee"; } + +.fa-cloud-bolt::before { + content: "\f76c"; } + +.fa-thunderstorm::before { + content: "\f76c"; } + +.fa-cloud-meatball::before { + content: "\f73b"; } + +.fa-cloud-moon::before { + content: "\f6c3"; } + +.fa-cloud-moon-rain::before { + content: "\f73c"; } + +.fa-cloud-rain::before { + content: "\f73d"; } + +.fa-cloud-showers-heavy::before { + content: "\f740"; } + +.fa-cloud-showers-water::before { + content: "\e4e4"; } + +.fa-cloud-sun::before { + content: "\f6c4"; } + +.fa-cloud-sun-rain::before { + content: "\f743"; } + +.fa-clover::before { + content: "\e139"; } + +.fa-code::before { + content: "\f121"; } + +.fa-code-branch::before { + content: "\f126"; } + +.fa-code-commit::before { + content: "\f386"; } + +.fa-code-compare::before { + content: "\e13a"; } + +.fa-code-fork::before { + content: "\e13b"; } + +.fa-code-merge::before { + content: "\f387"; } + +.fa-code-pull-request::before { + content: "\e13c"; } + +.fa-coins::before { + content: "\f51e"; } + +.fa-colon-sign::before { + content: "\e140"; } + +.fa-comment::before { + content: "\f075"; } + +.fa-comment-dollar::before { + content: "\f651"; } + +.fa-comment-dots::before { + content: "\f4ad"; } + +.fa-commenting::before { + content: "\f4ad"; } + +.fa-comment-medical::before { + content: "\f7f5"; } + +.fa-comment-slash::before { + content: "\f4b3"; } + +.fa-comment-sms::before { + content: "\f7cd"; } + +.fa-sms::before { + content: "\f7cd"; } + +.fa-comments::before { + content: "\f086"; } + +.fa-comments-dollar::before { + content: "\f653"; } + +.fa-compact-disc::before { + content: "\f51f"; } + +.fa-compass::before { + content: "\f14e"; } + +.fa-compass-drafting::before { + content: "\f568"; } + +.fa-drafting-compass::before { + content: "\f568"; } + +.fa-compress::before { + content: "\f066"; } + +.fa-computer::before { + content: "\e4e5"; } + +.fa-computer-mouse::before { + content: "\f8cc"; } + +.fa-mouse::before { + content: "\f8cc"; } + +.fa-cookie::before { + content: "\f563"; } + +.fa-cookie-bite::before { + content: "\f564"; } + +.fa-copy::before { + content: "\f0c5"; } + +.fa-copyright::before { + content: "\f1f9"; } + +.fa-couch::before { + content: "\f4b8"; } + +.fa-cow::before { + content: "\f6c8"; } + +.fa-credit-card::before { + content: "\f09d"; } + +.fa-credit-card-alt::before { + content: "\f09d"; } + +.fa-crop::before { + content: "\f125"; } + +.fa-crop-simple::before { + content: "\f565"; } + +.fa-crop-alt::before { + content: "\f565"; } + +.fa-cross::before { + content: "\f654"; } + +.fa-crosshairs::before { + content: "\f05b"; } + +.fa-crow::before { + content: "\f520"; } + +.fa-crown::before { + content: "\f521"; } + +.fa-crutch::before { + content: "\f7f7"; } + +.fa-cruzeiro-sign::before { + content: "\e152"; } + +.fa-cube::before { + content: "\f1b2"; } + +.fa-cubes::before { + content: "\f1b3"; } + +.fa-cubes-stacked::before { + content: "\e4e6"; } + +.fa-d::before { + content: "\44"; } + +.fa-database::before { + content: "\f1c0"; } + +.fa-delete-left::before { + content: "\f55a"; } + +.fa-backspace::before { + content: "\f55a"; } + +.fa-democrat::before { + content: "\f747"; } + +.fa-desktop::before { + content: "\f390"; } + +.fa-desktop-alt::before { + content: "\f390"; } + +.fa-dharmachakra::before { + content: "\f655"; } + +.fa-diagram-next::before { + content: "\e476"; } + +.fa-diagram-predecessor::before { + content: "\e477"; } + +.fa-diagram-project::before { + content: "\f542"; } + +.fa-project-diagram::before { + content: "\f542"; } + +.fa-diagram-successor::before { + content: "\e47a"; } + +.fa-diamond::before { + content: "\f219"; } + +.fa-diamond-turn-right::before { + content: "\f5eb"; } + +.fa-directions::before { + content: "\f5eb"; } + +.fa-dice::before { + content: "\f522"; } + +.fa-dice-d20::before { + content: "\f6cf"; } + +.fa-dice-d6::before { + content: "\f6d1"; } + +.fa-dice-five::before { + content: "\f523"; } + +.fa-dice-four::before { + content: "\f524"; } + +.fa-dice-one::before { + content: "\f525"; } + +.fa-dice-six::before { + content: "\f526"; } + +.fa-dice-three::before { + content: "\f527"; } + +.fa-dice-two::before { + content: "\f528"; } + +.fa-disease::before { + content: "\f7fa"; } + +.fa-display::before { + content: "\e163"; } + +.fa-divide::before { + content: "\f529"; } + +.fa-dna::before { + content: "\f471"; } + +.fa-dog::before { + content: "\f6d3"; } + +.fa-dollar-sign::before { + content: "\24"; } + +.fa-dollar::before { + content: "\24"; } + +.fa-usd::before { + content: "\24"; } + +.fa-dolly::before { + content: "\f472"; } + +.fa-dolly-box::before { + content: "\f472"; } + +.fa-dong-sign::before { + content: "\e169"; } + +.fa-door-closed::before { + content: "\f52a"; } + +.fa-door-open::before { + content: "\f52b"; } + +.fa-dove::before { + content: "\f4ba"; } + +.fa-down-left-and-up-right-to-center::before { + content: "\f422"; } + +.fa-compress-alt::before { + content: "\f422"; } + +.fa-down-long::before { + content: "\f309"; } + +.fa-long-arrow-alt-down::before { + content: "\f309"; } + +.fa-download::before { + content: "\f019"; } + +.fa-dragon::before { + content: "\f6d5"; } + +.fa-draw-polygon::before { + content: "\f5ee"; } + +.fa-droplet::before { + content: "\f043"; } + +.fa-tint::before { + content: "\f043"; } + +.fa-droplet-slash::before { + content: "\f5c7"; } + +.fa-tint-slash::before { + content: "\f5c7"; } + +.fa-drum::before { + content: "\f569"; } + +.fa-drum-steelpan::before { + content: "\f56a"; } + +.fa-drumstick-bite::before { + content: "\f6d7"; } + +.fa-dumbbell::before { + content: "\f44b"; } + +.fa-dumpster::before { + content: "\f793"; } + +.fa-dumpster-fire::before { + content: "\f794"; } + +.fa-dungeon::before { + content: "\f6d9"; } + +.fa-e::before { + content: "\45"; } + +.fa-ear-deaf::before { + content: "\f2a4"; } + +.fa-deaf::before { + content: "\f2a4"; } + +.fa-deafness::before { + content: "\f2a4"; } + +.fa-hard-of-hearing::before { + content: "\f2a4"; } + +.fa-ear-listen::before { + content: "\f2a2"; } + +.fa-assistive-listening-systems::before { + content: "\f2a2"; } + +.fa-earth-africa::before { + content: "\f57c"; } + +.fa-globe-africa::before { + content: "\f57c"; } + +.fa-earth-americas::before { + content: "\f57d"; } + +.fa-earth::before { + content: "\f57d"; } + +.fa-earth-america::before { + content: "\f57d"; } + +.fa-globe-americas::before { + content: "\f57d"; } + +.fa-earth-asia::before { + content: "\f57e"; } + +.fa-globe-asia::before { + content: "\f57e"; } + +.fa-earth-europe::before { + content: "\f7a2"; } + +.fa-globe-europe::before { + content: "\f7a2"; } + +.fa-earth-oceania::before { + content: "\e47b"; } + +.fa-globe-oceania::before { + content: "\e47b"; } + +.fa-egg::before { + content: "\f7fb"; } + +.fa-eject::before { + content: "\f052"; } + +.fa-elevator::before { + content: "\e16d"; } + +.fa-ellipsis::before { + content: "\f141"; } + +.fa-ellipsis-h::before { + content: "\f141"; } + +.fa-ellipsis-vertical::before { + content: "\f142"; } + +.fa-ellipsis-v::before { + content: "\f142"; } + +.fa-envelope::before { + content: "\f0e0"; } + +.fa-envelope-circle-check::before { + content: "\e4e8"; } + +.fa-envelope-open::before { + content: "\f2b6"; } + +.fa-envelope-open-text::before { + content: "\f658"; } + +.fa-envelopes-bulk::before { + content: "\f674"; } + +.fa-mail-bulk::before { + content: "\f674"; } + +.fa-equals::before { + content: "\3d"; } + +.fa-eraser::before { + content: "\f12d"; } + +.fa-ethernet::before { + content: "\f796"; } + +.fa-euro-sign::before { + content: "\f153"; } + +.fa-eur::before { + content: "\f153"; } + +.fa-euro::before { + content: "\f153"; } + +.fa-exclamation::before { + content: "\21"; } + +.fa-expand::before { + content: "\f065"; } + +.fa-explosion::before { + content: "\e4e9"; } + +.fa-eye::before { + content: "\f06e"; } + +.fa-eye-dropper::before { + content: "\f1fb"; } + +.fa-eye-dropper-empty::before { + content: "\f1fb"; } + +.fa-eyedropper::before { + content: "\f1fb"; } + +.fa-eye-low-vision::before { + content: "\f2a8"; } + +.fa-low-vision::before { + content: "\f2a8"; } + +.fa-eye-slash::before { + content: "\f070"; } + +.fa-f::before { + content: "\46"; } + +.fa-face-angry::before { + content: "\f556"; } + +.fa-angry::before { + content: "\f556"; } + +.fa-face-dizzy::before { + content: "\f567"; } + +.fa-dizzy::before { + content: "\f567"; } + +.fa-face-flushed::before { + content: "\f579"; } + +.fa-flushed::before { + content: "\f579"; } + +.fa-face-frown::before { + content: "\f119"; } + +.fa-frown::before { + content: "\f119"; } + +.fa-face-frown-open::before { + content: "\f57a"; } + +.fa-frown-open::before { + content: "\f57a"; } + +.fa-face-grimace::before { + content: "\f57f"; } + +.fa-grimace::before { + content: "\f57f"; } + +.fa-face-grin::before { + content: "\f580"; } + +.fa-grin::before { + content: "\f580"; } + +.fa-face-grin-beam::before { + content: "\f582"; } + +.fa-grin-beam::before { + content: "\f582"; } + +.fa-face-grin-beam-sweat::before { + content: "\f583"; } + +.fa-grin-beam-sweat::before { + content: "\f583"; } + +.fa-face-grin-hearts::before { + content: "\f584"; } + +.fa-grin-hearts::before { + content: "\f584"; } + +.fa-face-grin-squint::before { + content: "\f585"; } + +.fa-grin-squint::before { + content: "\f585"; } + +.fa-face-grin-squint-tears::before { + content: "\f586"; } + +.fa-grin-squint-tears::before { + content: "\f586"; } + +.fa-face-grin-stars::before { + content: "\f587"; } + +.fa-grin-stars::before { + content: "\f587"; } + +.fa-face-grin-tears::before { + content: "\f588"; } + +.fa-grin-tears::before { + content: "\f588"; } + +.fa-face-grin-tongue::before { + content: "\f589"; } + +.fa-grin-tongue::before { + content: "\f589"; } + +.fa-face-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-face-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-face-grin-wide::before { + content: "\f581"; } + +.fa-grin-alt::before { + content: "\f581"; } + +.fa-face-grin-wink::before { + content: "\f58c"; } + +.fa-grin-wink::before { + content: "\f58c"; } + +.fa-face-kiss::before { + content: "\f596"; } + +.fa-kiss::before { + content: "\f596"; } + +.fa-face-kiss-beam::before { + content: "\f597"; } + +.fa-kiss-beam::before { + content: "\f597"; } + +.fa-face-kiss-wink-heart::before { + content: "\f598"; } + +.fa-kiss-wink-heart::before { + content: "\f598"; } + +.fa-face-laugh::before { + content: "\f599"; } + +.fa-laugh::before { + content: "\f599"; } + +.fa-face-laugh-beam::before { + content: "\f59a"; } + +.fa-laugh-beam::before { + content: "\f59a"; } + +.fa-face-laugh-squint::before { + content: "\f59b"; } + +.fa-laugh-squint::before { + content: "\f59b"; } + +.fa-face-laugh-wink::before { + content: "\f59c"; } + +.fa-laugh-wink::before { + content: "\f59c"; } + +.fa-face-meh::before { + content: "\f11a"; } + +.fa-meh::before { + content: "\f11a"; } + +.fa-face-meh-blank::before { + content: "\f5a4"; } + +.fa-meh-blank::before { + content: "\f5a4"; } + +.fa-face-rolling-eyes::before { + content: "\f5a5"; } + +.fa-meh-rolling-eyes::before { + content: "\f5a5"; } + +.fa-face-sad-cry::before { + content: "\f5b3"; } + +.fa-sad-cry::before { + content: "\f5b3"; } + +.fa-face-sad-tear::before { + content: "\f5b4"; } + +.fa-sad-tear::before { + content: "\f5b4"; } + +.fa-face-smile::before { + content: "\f118"; } + +.fa-smile::before { + content: "\f118"; } + +.fa-face-smile-beam::before { + content: "\f5b8"; } + +.fa-smile-beam::before { + content: "\f5b8"; } + +.fa-face-smile-wink::before { + content: "\f4da"; } + +.fa-smile-wink::before { + content: "\f4da"; } + +.fa-face-surprise::before { + content: "\f5c2"; } + +.fa-surprise::before { + content: "\f5c2"; } + +.fa-face-tired::before { + content: "\f5c8"; } + +.fa-tired::before { + content: "\f5c8"; } + +.fa-fan::before { + content: "\f863"; } + +.fa-faucet::before { + content: "\e005"; } + +.fa-faucet-drip::before { + content: "\e006"; } + +.fa-fax::before { + content: "\f1ac"; } + +.fa-feather::before { + content: "\f52d"; } + +.fa-feather-pointed::before { + content: "\f56b"; } + +.fa-feather-alt::before { + content: "\f56b"; } + +.fa-ferry::before { + content: "\e4ea"; } + +.fa-file::before { + content: "\f15b"; } + +.fa-file-arrow-down::before { + content: "\f56d"; } + +.fa-file-download::before { + content: "\f56d"; } + +.fa-file-arrow-up::before { + content: "\f574"; } + +.fa-file-upload::before { + content: "\f574"; } + +.fa-file-audio::before { + content: "\f1c7"; } + +.fa-file-circle-check::before { + content: "\e493"; } + +.fa-file-circle-exclamation::before { + content: "\e4eb"; } + +.fa-file-circle-minus::before { + content: "\e4ed"; } + +.fa-file-circle-plus::before { + content: "\e4ee"; } + +.fa-file-circle-question::before { + content: "\e4ef"; } + +.fa-file-circle-xmark::before { + content: "\e494"; } + +.fa-file-code::before { + content: "\f1c9"; } + +.fa-file-contract::before { + content: "\f56c"; } + +.fa-file-csv::before { + content: "\f6dd"; } + +.fa-file-excel::before { + content: "\f1c3"; } + +.fa-file-export::before { + content: "\f56e"; } + +.fa-arrow-right-from-file::before { + content: "\f56e"; } + +.fa-file-image::before { + content: "\f1c5"; } + +.fa-file-import::before { + content: "\f56f"; } + +.fa-arrow-right-to-file::before { + content: "\f56f"; } + +.fa-file-invoice::before { + content: "\f570"; } + +.fa-file-invoice-dollar::before { + content: "\f571"; } + +.fa-file-lines::before { + content: "\f15c"; } + +.fa-file-alt::before { + content: "\f15c"; } + +.fa-file-text::before { + content: "\f15c"; } + +.fa-file-medical::before { + content: "\f477"; } + +.fa-file-pdf::before { + content: "\f1c1"; } + +.fa-file-pen::before { + content: "\f31c"; } + +.fa-file-edit::before { + content: "\f31c"; } + +.fa-file-powerpoint::before { + content: "\f1c4"; } + +.fa-file-prescription::before { + content: "\f572"; } + +.fa-file-shield::before { + content: "\e4f0"; } + +.fa-file-signature::before { + content: "\f573"; } + +.fa-file-video::before { + content: "\f1c8"; } + +.fa-file-waveform::before { + content: "\f478"; } + +.fa-file-medical-alt::before { + content: "\f478"; } + +.fa-file-word::before { + content: "\f1c2"; } + +.fa-file-zipper::before { + content: "\f1c6"; } + +.fa-file-archive::before { + content: "\f1c6"; } + +.fa-fill::before { + content: "\f575"; } + +.fa-fill-drip::before { + content: "\f576"; } + +.fa-film::before { + content: "\f008"; } + +.fa-filter::before { + content: "\f0b0"; } + +.fa-filter-circle-dollar::before { + content: "\f662"; } + +.fa-funnel-dollar::before { + content: "\f662"; } + +.fa-filter-circle-xmark::before { + content: "\e17b"; } + +.fa-fingerprint::before { + content: "\f577"; } + +.fa-fire::before { + content: "\f06d"; } + +.fa-fire-burner::before { + content: "\e4f1"; } + +.fa-fire-extinguisher::before { + content: "\f134"; } + +.fa-fire-flame-curved::before { + content: "\f7e4"; } + +.fa-fire-alt::before { + content: "\f7e4"; } + +.fa-fire-flame-simple::before { + content: "\f46a"; } + +.fa-burn::before { + content: "\f46a"; } + +.fa-fish::before { + content: "\f578"; } + +.fa-fish-fins::before { + content: "\e4f2"; } + +.fa-flag::before { + content: "\f024"; } + +.fa-flag-checkered::before { + content: "\f11e"; } + +.fa-flag-usa::before { + content: "\f74d"; } + +.fa-flask::before { + content: "\f0c3"; } + +.fa-flask-vial::before { + content: "\e4f3"; } + +.fa-floppy-disk::before { + content: "\f0c7"; } + +.fa-save::before { + content: "\f0c7"; } + +.fa-florin-sign::before { + content: "\e184"; } + +.fa-folder::before { + content: "\f07b"; } + +.fa-folder-blank::before { + content: "\f07b"; } + +.fa-folder-closed::before { + content: "\e185"; } + +.fa-folder-minus::before { + content: "\f65d"; } + +.fa-folder-open::before { + content: "\f07c"; } + +.fa-folder-plus::before { + content: "\f65e"; } + +.fa-folder-tree::before { + content: "\f802"; } + +.fa-font::before { + content: "\f031"; } + +.fa-football::before { + content: "\f44e"; } + +.fa-football-ball::before { + content: "\f44e"; } + +.fa-forward::before { + content: "\f04e"; } + +.fa-forward-fast::before { + content: "\f050"; } + +.fa-fast-forward::before { + content: "\f050"; } + +.fa-forward-step::before { + content: "\f051"; } + +.fa-step-forward::before { + content: "\f051"; } + +.fa-franc-sign::before { + content: "\e18f"; } + +.fa-frog::before { + content: "\f52e"; } + +.fa-futbol::before { + content: "\f1e3"; } + +.fa-futbol-ball::before { + content: "\f1e3"; } + +.fa-soccer-ball::before { + content: "\f1e3"; } + +.fa-g::before { + content: "\47"; } + +.fa-gamepad::before { + content: "\f11b"; } + +.fa-gas-pump::before { + content: "\f52f"; } + +.fa-gauge::before { + content: "\f624"; } + +.fa-dashboard::before { + content: "\f624"; } + +.fa-gauge-med::before { + content: "\f624"; } + +.fa-tachometer-alt-average::before { + content: "\f624"; } + +.fa-gauge-high::before { + content: "\f625"; } + +.fa-tachometer-alt::before { + content: "\f625"; } + +.fa-tachometer-alt-fast::before { + content: "\f625"; } + +.fa-gauge-simple::before { + content: "\f629"; } + +.fa-gauge-simple-med::before { + content: "\f629"; } + +.fa-tachometer-average::before { + content: "\f629"; } + +.fa-gauge-simple-high::before { + content: "\f62a"; } + +.fa-tachometer::before { + content: "\f62a"; } + +.fa-tachometer-fast::before { + content: "\f62a"; } + +.fa-gavel::before { + content: "\f0e3"; } + +.fa-legal::before { + content: "\f0e3"; } + +.fa-gear::before { + content: "\f013"; } + +.fa-cog::before { + content: "\f013"; } + +.fa-gears::before { + content: "\f085"; } + +.fa-cogs::before { + content: "\f085"; } + +.fa-gem::before { + content: "\f3a5"; } + +.fa-genderless::before { + content: "\f22d"; } + +.fa-ghost::before { + content: "\f6e2"; } + +.fa-gift::before { + content: "\f06b"; } + +.fa-gifts::before { + content: "\f79c"; } + +.fa-glass-water::before { + content: "\e4f4"; } + +.fa-glass-water-droplet::before { + content: "\e4f5"; } + +.fa-glasses::before { + content: "\f530"; } + +.fa-globe::before { + content: "\f0ac"; } + +.fa-golf-ball-tee::before { + content: "\f450"; } + +.fa-golf-ball::before { + content: "\f450"; } + +.fa-gopuram::before { + content: "\f664"; } + +.fa-graduation-cap::before { + content: "\f19d"; } + +.fa-mortar-board::before { + content: "\f19d"; } + +.fa-greater-than::before { + content: "\3e"; } + +.fa-greater-than-equal::before { + content: "\f532"; } + +.fa-grip::before { + content: "\f58d"; } + +.fa-grip-horizontal::before { + content: "\f58d"; } + +.fa-grip-lines::before { + content: "\f7a4"; } + +.fa-grip-lines-vertical::before { + content: "\f7a5"; } + +.fa-grip-vertical::before { + content: "\f58e"; } + +.fa-group-arrows-rotate::before { + content: "\e4f6"; } + +.fa-guarani-sign::before { + content: "\e19a"; } + +.fa-guitar::before { + content: "\f7a6"; } + +.fa-gun::before { + content: "\e19b"; } + +.fa-h::before { + content: "\48"; } + +.fa-hammer::before { + content: "\f6e3"; } + +.fa-hamsa::before { + content: "\f665"; } + +.fa-hand::before { + content: "\f256"; } + +.fa-hand-paper::before { + content: "\f256"; } + +.fa-hand-back-fist::before { + content: "\f255"; } + +.fa-hand-rock::before { + content: "\f255"; } + +.fa-hand-dots::before { + content: "\f461"; } + +.fa-allergies::before { + content: "\f461"; } + +.fa-hand-fist::before { + content: "\f6de"; } + +.fa-fist-raised::before { + content: "\f6de"; } + +.fa-hand-holding::before { + content: "\f4bd"; } + +.fa-hand-holding-dollar::before { + content: "\f4c0"; } + +.fa-hand-holding-usd::before { + content: "\f4c0"; } + +.fa-hand-holding-droplet::before { + content: "\f4c1"; } + +.fa-hand-holding-water::before { + content: "\f4c1"; } + +.fa-hand-holding-hand::before { + content: "\e4f7"; } + +.fa-hand-holding-heart::before { + content: "\f4be"; } + +.fa-hand-holding-medical::before { + content: "\e05c"; } + +.fa-hand-lizard::before { + content: "\f258"; } + +.fa-hand-middle-finger::before { + content: "\f806"; } + +.fa-hand-peace::before { + content: "\f25b"; } + +.fa-hand-point-down::before { + content: "\f0a7"; } + +.fa-hand-point-left::before { + content: "\f0a5"; } + +.fa-hand-point-right::before { + content: "\f0a4"; } + +.fa-hand-point-up::before { + content: "\f0a6"; } + +.fa-hand-pointer::before { + content: "\f25a"; } + +.fa-hand-scissors::before { + content: "\f257"; } + +.fa-hand-sparkles::before { + content: "\e05d"; } + +.fa-hand-spock::before { + content: "\f259"; } + +.fa-handcuffs::before { + content: "\e4f8"; } + +.fa-hands::before { + content: "\f2a7"; } + +.fa-sign-language::before { + content: "\f2a7"; } + +.fa-signing::before { + content: "\f2a7"; } + +.fa-hands-asl-interpreting::before { + content: "\f2a3"; } + +.fa-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-asl-interpreting::before { + content: "\f2a3"; } + +.fa-hands-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-hands-bound::before { + content: "\e4f9"; } + +.fa-hands-bubbles::before { + content: "\e05e"; } + +.fa-hands-wash::before { + content: "\e05e"; } + +.fa-hands-clapping::before { + content: "\e1a8"; } + +.fa-hands-holding::before { + content: "\f4c2"; } + +.fa-hands-holding-child::before { + content: "\e4fa"; } + +.fa-hands-holding-circle::before { + content: "\e4fb"; } + +.fa-hands-praying::before { + content: "\f684"; } + +.fa-praying-hands::before { + content: "\f684"; } + +.fa-handshake::before { + content: "\f2b5"; } + +.fa-handshake-angle::before { + content: "\f4c4"; } + +.fa-hands-helping::before { + content: "\f4c4"; } + +.fa-handshake-simple::before { + content: "\f4c6"; } + +.fa-handshake-alt::before { + content: "\f4c6"; } + +.fa-handshake-simple-slash::before { + content: "\e05f"; } + +.fa-handshake-alt-slash::before { + content: "\e05f"; } + +.fa-handshake-slash::before { + content: "\e060"; } + +.fa-hanukiah::before { + content: "\f6e6"; } + +.fa-hard-drive::before { + content: "\f0a0"; } + +.fa-hdd::before { + content: "\f0a0"; } + +.fa-hashtag::before { + content: "\23"; } + +.fa-hat-cowboy::before { + content: "\f8c0"; } + +.fa-hat-cowboy-side::before { + content: "\f8c1"; } + +.fa-hat-wizard::before { + content: "\f6e8"; } + +.fa-head-side-cough::before { + content: "\e061"; } + +.fa-head-side-cough-slash::before { + content: "\e062"; } + +.fa-head-side-mask::before { + content: "\e063"; } + +.fa-head-side-virus::before { + content: "\e064"; } + +.fa-heading::before { + content: "\f1dc"; } + +.fa-header::before { + content: "\f1dc"; } + +.fa-headphones::before { + content: "\f025"; } + +.fa-headphones-simple::before { + content: "\f58f"; } + +.fa-headphones-alt::before { + content: "\f58f"; } + +.fa-headset::before { + content: "\f590"; } + +.fa-heart::before { + content: "\f004"; } + +.fa-heart-circle-bolt::before { + content: "\e4fc"; } + +.fa-heart-circle-check::before { + content: "\e4fd"; } + +.fa-heart-circle-exclamation::before { + content: "\e4fe"; } + +.fa-heart-circle-minus::before { + content: "\e4ff"; } + +.fa-heart-circle-plus::before { + content: "\e500"; } + +.fa-heart-circle-xmark::before { + content: "\e501"; } + +.fa-heart-crack::before { + content: "\f7a9"; } + +.fa-heart-broken::before { + content: "\f7a9"; } + +.fa-heart-pulse::before { + content: "\f21e"; } + +.fa-heartbeat::before { + content: "\f21e"; } + +.fa-helicopter::before { + content: "\f533"; } + +.fa-helicopter-symbol::before { + content: "\e502"; } + +.fa-helmet-safety::before { + content: "\f807"; } + +.fa-hard-hat::before { + content: "\f807"; } + +.fa-hat-hard::before { + content: "\f807"; } + +.fa-helmet-un::before { + content: "\e503"; } + +.fa-highlighter::before { + content: "\f591"; } + +.fa-hill-avalanche::before { + content: "\e507"; } + +.fa-hill-rockslide::before { + content: "\e508"; } + +.fa-hippo::before { + content: "\f6ed"; } + +.fa-hockey-puck::before { + content: "\f453"; } + +.fa-holly-berry::before { + content: "\f7aa"; } + +.fa-horse::before { + content: "\f6f0"; } + +.fa-horse-head::before { + content: "\f7ab"; } + +.fa-hospital::before { + content: "\f0f8"; } + +.fa-hospital-alt::before { + content: "\f0f8"; } + +.fa-hospital-wide::before { + content: "\f0f8"; } + +.fa-hospital-user::before { + content: "\f80d"; } + +.fa-hot-tub-person::before { + content: "\f593"; } + +.fa-hot-tub::before { + content: "\f593"; } + +.fa-hotdog::before { + content: "\f80f"; } + +.fa-hotel::before { + content: "\f594"; } + +.fa-hourglass::before { + content: "\f254"; } + +.fa-hourglass-2::before { + content: "\f254"; } + +.fa-hourglass-half::before { + content: "\f254"; } + +.fa-hourglass-empty::before { + content: "\f252"; } + +.fa-hourglass-end::before { + content: "\f253"; } + +.fa-hourglass-3::before { + content: "\f253"; } + +.fa-hourglass-start::before { + content: "\f251"; } + +.fa-hourglass-1::before { + content: "\f251"; } + +.fa-house::before { + content: "\f015"; } + +.fa-home::before { + content: "\f015"; } + +.fa-home-alt::before { + content: "\f015"; } + +.fa-home-lg-alt::before { + content: "\f015"; } + +.fa-house-chimney::before { + content: "\e3af"; } + +.fa-home-lg::before { + content: "\e3af"; } + +.fa-house-chimney-crack::before { + content: "\f6f1"; } + +.fa-house-damage::before { + content: "\f6f1"; } + +.fa-house-chimney-medical::before { + content: "\f7f2"; } + +.fa-clinic-medical::before { + content: "\f7f2"; } + +.fa-house-chimney-user::before { + content: "\e065"; } + +.fa-house-chimney-window::before { + content: "\e00d"; } + +.fa-house-circle-check::before { + content: "\e509"; } + +.fa-house-circle-exclamation::before { + content: "\e50a"; } + +.fa-house-circle-xmark::before { + content: "\e50b"; } + +.fa-house-crack::before { + content: "\e3b1"; } + +.fa-house-fire::before { + content: "\e50c"; } + +.fa-house-flag::before { + content: "\e50d"; } + +.fa-house-flood-water::before { + content: "\e50e"; } + +.fa-house-flood-water-circle-arrow-right::before { + content: "\e50f"; } + +.fa-house-laptop::before { + content: "\e066"; } + +.fa-laptop-house::before { + content: "\e066"; } + +.fa-house-lock::before { + content: "\e510"; } + +.fa-house-medical::before { + content: "\e3b2"; } + +.fa-house-medical-circle-check::before { + content: "\e511"; } + +.fa-house-medical-circle-exclamation::before { + content: "\e512"; } + +.fa-house-medical-circle-xmark::before { + content: "\e513"; } + +.fa-house-medical-flag::before { + content: "\e514"; } + +.fa-house-signal::before { + content: "\e012"; } + +.fa-house-tsunami::before { + content: "\e515"; } + +.fa-house-user::before { + content: "\e1b0"; } + +.fa-home-user::before { + content: "\e1b0"; } + +.fa-hryvnia-sign::before { + content: "\f6f2"; } + +.fa-hryvnia::before { + content: "\f6f2"; } + +.fa-hurricane::before { + content: "\f751"; } + +.fa-i::before { + content: "\49"; } + +.fa-i-cursor::before { + content: "\f246"; } + +.fa-ice-cream::before { + content: "\f810"; } + +.fa-icicles::before { + content: "\f7ad"; } + +.fa-icons::before { + content: "\f86d"; } + +.fa-heart-music-camera-bolt::before { + content: "\f86d"; } + +.fa-id-badge::before { + content: "\f2c1"; } + +.fa-id-card::before { + content: "\f2c2"; } + +.fa-drivers-license::before { + content: "\f2c2"; } + +.fa-id-card-clip::before { + content: "\f47f"; } + +.fa-id-card-alt::before { + content: "\f47f"; } + +.fa-igloo::before { + content: "\f7ae"; } + +.fa-image::before { + content: "\f03e"; } + +.fa-image-portrait::before { + content: "\f3e0"; } + +.fa-portrait::before { + content: "\f3e0"; } + +.fa-images::before { + content: "\f302"; } + +.fa-inbox::before { + content: "\f01c"; } + +.fa-indent::before { + content: "\f03c"; } + +.fa-indian-rupee-sign::before { + content: "\e1bc"; } + +.fa-indian-rupee::before { + content: "\e1bc"; } + +.fa-inr::before { + content: "\e1bc"; } + +.fa-industry::before { + content: "\f275"; } + +.fa-infinity::before { + content: "\f534"; } + +.fa-info::before { + content: "\f129"; } + +.fa-italic::before { + content: "\f033"; } + +.fa-j::before { + content: "\4a"; } + +.fa-jar::before { + content: "\e516"; } + +.fa-jar-wheat::before { + content: "\e517"; } + +.fa-jedi::before { + content: "\f669"; } + +.fa-jet-fighter::before { + content: "\f0fb"; } + +.fa-fighter-jet::before { + content: "\f0fb"; } + +.fa-jet-fighter-up::before { + content: "\e518"; } + +.fa-joint::before { + content: "\f595"; } + +.fa-jug-detergent::before { + content: "\e519"; } + +.fa-k::before { + content: "\4b"; } + +.fa-kaaba::before { + content: "\f66b"; } + +.fa-key::before { + content: "\f084"; } + +.fa-keyboard::before { + content: "\f11c"; } + +.fa-khanda::before { + content: "\f66d"; } + +.fa-kip-sign::before { + content: "\e1c4"; } + +.fa-kit-medical::before { + content: "\f479"; } + +.fa-first-aid::before { + content: "\f479"; } + +.fa-kitchen-set::before { + content: "\e51a"; } + +.fa-kiwi-bird::before { + content: "\f535"; } + +.fa-l::before { + content: "\4c"; } + +.fa-land-mine-on::before { + content: "\e51b"; } + +.fa-landmark::before { + content: "\f66f"; } + +.fa-landmark-dome::before { + content: "\f752"; } + +.fa-landmark-alt::before { + content: "\f752"; } + +.fa-landmark-flag::before { + content: "\e51c"; } + +.fa-language::before { + content: "\f1ab"; } + +.fa-laptop::before { + content: "\f109"; } + +.fa-laptop-code::before { + content: "\f5fc"; } + +.fa-laptop-file::before { + content: "\e51d"; } + +.fa-laptop-medical::before { + content: "\f812"; } + +.fa-lari-sign::before { + content: "\e1c8"; } + +.fa-layer-group::before { + content: "\f5fd"; } + +.fa-leaf::before { + content: "\f06c"; } + +.fa-left-long::before { + content: "\f30a"; } + +.fa-long-arrow-alt-left::before { + content: "\f30a"; } + +.fa-left-right::before { + content: "\f337"; } + +.fa-arrows-alt-h::before { + content: "\f337"; } + +.fa-lemon::before { + content: "\f094"; } + +.fa-less-than::before { + content: "\3c"; } + +.fa-less-than-equal::before { + content: "\f537"; } + +.fa-life-ring::before { + content: "\f1cd"; } + +.fa-lightbulb::before { + content: "\f0eb"; } + +.fa-lines-leaning::before { + content: "\e51e"; } + +.fa-link::before { + content: "\f0c1"; } + +.fa-chain::before { + content: "\f0c1"; } + +.fa-link-slash::before { + content: "\f127"; } + +.fa-chain-broken::before { + content: "\f127"; } + +.fa-chain-slash::before { + content: "\f127"; } + +.fa-unlink::before { + content: "\f127"; } + +.fa-lira-sign::before { + content: "\f195"; } + +.fa-list::before { + content: "\f03a"; } + +.fa-list-squares::before { + content: "\f03a"; } + +.fa-list-check::before { + content: "\f0ae"; } + +.fa-tasks::before { + content: "\f0ae"; } + +.fa-list-ol::before { + content: "\f0cb"; } + +.fa-list-1-2::before { + content: "\f0cb"; } + +.fa-list-numeric::before { + content: "\f0cb"; } + +.fa-list-ul::before { + content: "\f0ca"; } + +.fa-list-dots::before { + content: "\f0ca"; } + +.fa-litecoin-sign::before { + content: "\e1d3"; } + +.fa-location-arrow::before { + content: "\f124"; } + +.fa-location-crosshairs::before { + content: "\f601"; } + +.fa-location::before { + content: "\f601"; } + +.fa-location-dot::before { + content: "\f3c5"; } + +.fa-map-marker-alt::before { + content: "\f3c5"; } + +.fa-location-pin::before { + content: "\f041"; } + +.fa-map-marker::before { + content: "\f041"; } + +.fa-location-pin-lock::before { + content: "\e51f"; } + +.fa-lock::before { + content: "\f023"; } + +.fa-lock-open::before { + content: "\f3c1"; } + +.fa-locust::before { + content: "\e520"; } + +.fa-lungs::before { + content: "\f604"; } + +.fa-lungs-virus::before { + content: "\e067"; } + +.fa-m::before { + content: "\4d"; } + +.fa-magnet::before { + content: "\f076"; } + +.fa-magnifying-glass::before { + content: "\f002"; } + +.fa-search::before { + content: "\f002"; } + +.fa-magnifying-glass-arrow-right::before { + content: "\e521"; } + +.fa-magnifying-glass-chart::before { + content: "\e522"; } + +.fa-magnifying-glass-dollar::before { + content: "\f688"; } + +.fa-search-dollar::before { + content: "\f688"; } + +.fa-magnifying-glass-location::before { + content: "\f689"; } + +.fa-search-location::before { + content: "\f689"; } + +.fa-magnifying-glass-minus::before { + content: "\f010"; } + +.fa-search-minus::before { + content: "\f010"; } + +.fa-magnifying-glass-plus::before { + content: "\f00e"; } + +.fa-search-plus::before { + content: "\f00e"; } + +.fa-manat-sign::before { + content: "\e1d5"; } + +.fa-map::before { + content: "\f279"; } + +.fa-map-location::before { + content: "\f59f"; } + +.fa-map-marked::before { + content: "\f59f"; } + +.fa-map-location-dot::before { + content: "\f5a0"; } + +.fa-map-marked-alt::before { + content: "\f5a0"; } + +.fa-map-pin::before { + content: "\f276"; } + +.fa-marker::before { + content: "\f5a1"; } + +.fa-mars::before { + content: "\f222"; } + +.fa-mars-and-venus::before { + content: "\f224"; } + +.fa-mars-and-venus-burst::before { + content: "\e523"; } + +.fa-mars-double::before { + content: "\f227"; } + +.fa-mars-stroke::before { + content: "\f229"; } + +.fa-mars-stroke-right::before { + content: "\f22b"; } + +.fa-mars-stroke-h::before { + content: "\f22b"; } + +.fa-mars-stroke-up::before { + content: "\f22a"; } + +.fa-mars-stroke-v::before { + content: "\f22a"; } + +.fa-martini-glass::before { + content: "\f57b"; } + +.fa-glass-martini-alt::before { + content: "\f57b"; } + +.fa-martini-glass-citrus::before { + content: "\f561"; } + +.fa-cocktail::before { + content: "\f561"; } + +.fa-martini-glass-empty::before { + content: "\f000"; } + +.fa-glass-martini::before { + content: "\f000"; } + +.fa-mask::before { + content: "\f6fa"; } + +.fa-mask-face::before { + content: "\e1d7"; } + +.fa-mask-ventilator::before { + content: "\e524"; } + +.fa-masks-theater::before { + content: "\f630"; } + +.fa-theater-masks::before { + content: "\f630"; } + +.fa-mattress-pillow::before { + content: "\e525"; } + +.fa-maximize::before { + content: "\f31e"; } + +.fa-expand-arrows-alt::before { + content: "\f31e"; } + +.fa-medal::before { + content: "\f5a2"; } + +.fa-memory::before { + content: "\f538"; } + +.fa-menorah::before { + content: "\f676"; } + +.fa-mercury::before { + content: "\f223"; } + +.fa-message::before { + content: "\f27a"; } + +.fa-comment-alt::before { + content: "\f27a"; } + +.fa-meteor::before { + content: "\f753"; } + +.fa-microchip::before { + content: "\f2db"; } + +.fa-microphone::before { + content: "\f130"; } + +.fa-microphone-lines::before { + content: "\f3c9"; } + +.fa-microphone-alt::before { + content: "\f3c9"; } + +.fa-microphone-lines-slash::before { + content: "\f539"; } + +.fa-microphone-alt-slash::before { + content: "\f539"; } + +.fa-microphone-slash::before { + content: "\f131"; } + +.fa-microscope::before { + content: "\f610"; } + +.fa-mill-sign::before { + content: "\e1ed"; } + +.fa-minimize::before { + content: "\f78c"; } + +.fa-compress-arrows-alt::before { + content: "\f78c"; } + +.fa-minus::before { + content: "\f068"; } + +.fa-subtract::before { + content: "\f068"; } + +.fa-mitten::before { + content: "\f7b5"; } + +.fa-mobile::before { + content: "\f3ce"; } + +.fa-mobile-android::before { + content: "\f3ce"; } + +.fa-mobile-phone::before { + content: "\f3ce"; } + +.fa-mobile-button::before { + content: "\f10b"; } + +.fa-mobile-retro::before { + content: "\e527"; } + +.fa-mobile-screen::before { + content: "\f3cf"; } + +.fa-mobile-android-alt::before { + content: "\f3cf"; } + +.fa-mobile-screen-button::before { + content: "\f3cd"; } + +.fa-mobile-alt::before { + content: "\f3cd"; } + +.fa-money-bill::before { + content: "\f0d6"; } + +.fa-money-bill-1::before { + content: "\f3d1"; } + +.fa-money-bill-alt::before { + content: "\f3d1"; } + +.fa-money-bill-1-wave::before { + content: "\f53b"; } + +.fa-money-bill-wave-alt::before { + content: "\f53b"; } + +.fa-money-bill-transfer::before { + content: "\e528"; } + +.fa-money-bill-trend-up::before { + content: "\e529"; } + +.fa-money-bill-wave::before { + content: "\f53a"; } + +.fa-money-bill-wheat::before { + content: "\e52a"; } + +.fa-money-bills::before { + content: "\e1f3"; } + +.fa-money-check::before { + content: "\f53c"; } + +.fa-money-check-dollar::before { + content: "\f53d"; } + +.fa-money-check-alt::before { + content: "\f53d"; } + +.fa-monument::before { + content: "\f5a6"; } + +.fa-moon::before { + content: "\f186"; } + +.fa-mortar-pestle::before { + content: "\f5a7"; } + +.fa-mosque::before { + content: "\f678"; } + +.fa-mosquito::before { + content: "\e52b"; } + +.fa-mosquito-net::before { + content: "\e52c"; } + +.fa-motorcycle::before { + content: "\f21c"; } + +.fa-mound::before { + content: "\e52d"; } + +.fa-mountain::before { + content: "\f6fc"; } + +.fa-mountain-city::before { + content: "\e52e"; } + +.fa-mountain-sun::before { + content: "\e52f"; } + +.fa-mug-hot::before { + content: "\f7b6"; } + +.fa-mug-saucer::before { + content: "\f0f4"; } + +.fa-coffee::before { + content: "\f0f4"; } + +.fa-music::before { + content: "\f001"; } + +.fa-n::before { + content: "\4e"; } + +.fa-naira-sign::before { + content: "\e1f6"; } + +.fa-network-wired::before { + content: "\f6ff"; } + +.fa-neuter::before { + content: "\f22c"; } + +.fa-newspaper::before { + content: "\f1ea"; } + +.fa-not-equal::before { + content: "\f53e"; } + +.fa-note-sticky::before { + content: "\f249"; } + +.fa-sticky-note::before { + content: "\f249"; } + +.fa-notes-medical::before { + content: "\f481"; } + +.fa-o::before { + content: "\4f"; } + +.fa-object-group::before { + content: "\f247"; } + +.fa-object-ungroup::before { + content: "\f248"; } + +.fa-oil-can::before { + content: "\f613"; } + +.fa-oil-well::before { + content: "\e532"; } + +.fa-om::before { + content: "\f679"; } + +.fa-otter::before { + content: "\f700"; } + +.fa-outdent::before { + content: "\f03b"; } + +.fa-dedent::before { + content: "\f03b"; } + +.fa-p::before { + content: "\50"; } + +.fa-pager::before { + content: "\f815"; } + +.fa-paint-roller::before { + content: "\f5aa"; } + +.fa-paintbrush::before { + content: "\f1fc"; } + +.fa-paint-brush::before { + content: "\f1fc"; } + +.fa-palette::before { + content: "\f53f"; } + +.fa-pallet::before { + content: "\f482"; } + +.fa-panorama::before { + content: "\e209"; } + +.fa-paper-plane::before { + content: "\f1d8"; } + +.fa-paperclip::before { + content: "\f0c6"; } + +.fa-parachute-box::before { + content: "\f4cd"; } + +.fa-paragraph::before { + content: "\f1dd"; } + +.fa-passport::before { + content: "\f5ab"; } + +.fa-paste::before { + content: "\f0ea"; } + +.fa-file-clipboard::before { + content: "\f0ea"; } + +.fa-pause::before { + content: "\f04c"; } + +.fa-paw::before { + content: "\f1b0"; } + +.fa-peace::before { + content: "\f67c"; } + +.fa-pen::before { + content: "\f304"; } + +.fa-pen-clip::before { + content: "\f305"; } + +.fa-pen-alt::before { + content: "\f305"; } + +.fa-pen-fancy::before { + content: "\f5ac"; } + +.fa-pen-nib::before { + content: "\f5ad"; } + +.fa-pen-ruler::before { + content: "\f5ae"; } + +.fa-pencil-ruler::before { + content: "\f5ae"; } + +.fa-pen-to-square::before { + content: "\f044"; } + +.fa-edit::before { + content: "\f044"; } + +.fa-pencil::before { + content: "\f303"; } + +.fa-pencil-alt::before { + content: "\f303"; } + +.fa-people-arrows-left-right::before { + content: "\e068"; } + +.fa-people-arrows::before { + content: "\e068"; } + +.fa-people-carry-box::before { + content: "\f4ce"; } + +.fa-people-carry::before { + content: "\f4ce"; } + +.fa-people-group::before { + content: "\e533"; } + +.fa-people-line::before { + content: "\e534"; } + +.fa-people-pulling::before { + content: "\e535"; } + +.fa-people-robbery::before { + content: "\e536"; } + +.fa-people-roof::before { + content: "\e537"; } + +.fa-pepper-hot::before { + content: "\f816"; } + +.fa-percent::before { + content: "\25"; } + +.fa-percentage::before { + content: "\25"; } + +.fa-person::before { + content: "\f183"; } + +.fa-male::before { + content: "\f183"; } + +.fa-person-arrow-down-to-line::before { + content: "\e538"; } + +.fa-person-arrow-up-from-line::before { + content: "\e539"; } + +.fa-person-biking::before { + content: "\f84a"; } + +.fa-biking::before { + content: "\f84a"; } + +.fa-person-booth::before { + content: "\f756"; } + +.fa-person-breastfeeding::before { + content: "\e53a"; } + +.fa-person-burst::before { + content: "\e53b"; } + +.fa-person-cane::before { + content: "\e53c"; } + +.fa-person-chalkboard::before { + content: "\e53d"; } + +.fa-person-circle-check::before { + content: "\e53e"; } + +.fa-person-circle-exclamation::before { + content: "\e53f"; } + +.fa-person-circle-minus::before { + content: "\e540"; } + +.fa-person-circle-plus::before { + content: "\e541"; } + +.fa-person-circle-question::before { + content: "\e542"; } + +.fa-person-circle-xmark::before { + content: "\e543"; } + +.fa-person-digging::before { + content: "\f85e"; } + +.fa-digging::before { + content: "\f85e"; } + +.fa-person-dots-from-line::before { + content: "\f470"; } + +.fa-diagnoses::before { + content: "\f470"; } + +.fa-person-dress::before { + content: "\f182"; } + +.fa-female::before { + content: "\f182"; } + +.fa-person-dress-burst::before { + content: "\e544"; } + +.fa-person-drowning::before { + content: "\e545"; } + +.fa-person-falling::before { + content: "\e546"; } + +.fa-person-falling-burst::before { + content: "\e547"; } + +.fa-person-half-dress::before { + content: "\e548"; } + +.fa-person-harassing::before { + content: "\e549"; } + +.fa-person-hiking::before { + content: "\f6ec"; } + +.fa-hiking::before { + content: "\f6ec"; } + +.fa-person-military-pointing::before { + content: "\e54a"; } + +.fa-person-military-rifle::before { + content: "\e54b"; } + +.fa-person-military-to-person::before { + content: "\e54c"; } + +.fa-person-praying::before { + content: "\f683"; } + +.fa-pray::before { + content: "\f683"; } + +.fa-person-pregnant::before { + content: "\e31e"; } + +.fa-person-rays::before { + content: "\e54d"; } + +.fa-person-rifle::before { + content: "\e54e"; } + +.fa-person-running::before { + content: "\f70c"; } + +.fa-running::before { + content: "\f70c"; } + +.fa-person-shelter::before { + content: "\e54f"; } + +.fa-person-skating::before { + content: "\f7c5"; } + +.fa-skating::before { + content: "\f7c5"; } + +.fa-person-skiing::before { + content: "\f7c9"; } + +.fa-skiing::before { + content: "\f7c9"; } + +.fa-person-skiing-nordic::before { + content: "\f7ca"; } + +.fa-skiing-nordic::before { + content: "\f7ca"; } + +.fa-person-snowboarding::before { + content: "\f7ce"; } + +.fa-snowboarding::before { + content: "\f7ce"; } + +.fa-person-swimming::before { + content: "\f5c4"; } + +.fa-swimmer::before { + content: "\f5c4"; } + +.fa-person-through-window::before { + content: "\e433"; } + +.fa-person-walking::before { + content: "\f554"; } + +.fa-walking::before { + content: "\f554"; } + +.fa-person-walking-arrow-loop-left::before { + content: "\e551"; } + +.fa-person-walking-arrow-right::before { + content: "\e552"; } + +.fa-person-walking-dashed-line-arrow-right::before { + content: "\e553"; } + +.fa-person-walking-luggage::before { + content: "\e554"; } + +.fa-person-walking-with-cane::before { + content: "\f29d"; } + +.fa-blind::before { + content: "\f29d"; } + +.fa-peseta-sign::before { + content: "\e221"; } + +.fa-peso-sign::before { + content: "\e222"; } + +.fa-phone::before { + content: "\f095"; } + +.fa-phone-flip::before { + content: "\f879"; } + +.fa-phone-alt::before { + content: "\f879"; } + +.fa-phone-slash::before { + content: "\f3dd"; } + +.fa-phone-volume::before { + content: "\f2a0"; } + +.fa-volume-control-phone::before { + content: "\f2a0"; } + +.fa-photo-film::before { + content: "\f87c"; } + +.fa-photo-video::before { + content: "\f87c"; } + +.fa-piggy-bank::before { + content: "\f4d3"; } + +.fa-pills::before { + content: "\f484"; } + +.fa-pizza-slice::before { + content: "\f818"; } + +.fa-place-of-worship::before { + content: "\f67f"; } + +.fa-plane::before { + content: "\f072"; } + +.fa-plane-arrival::before { + content: "\f5af"; } + +.fa-plane-circle-check::before { + content: "\e555"; } + +.fa-plane-circle-exclamation::before { + content: "\e556"; } + +.fa-plane-circle-xmark::before { + content: "\e557"; } + +.fa-plane-departure::before { + content: "\f5b0"; } + +.fa-plane-lock::before { + content: "\e558"; } + +.fa-plane-slash::before { + content: "\e069"; } + +.fa-plane-up::before { + content: "\e22d"; } + +.fa-plant-wilt::before { + content: "\e43b"; } + +.fa-plate-wheat::before { + content: "\e55a"; } + +.fa-play::before { + content: "\f04b"; } + +.fa-plug::before { + content: "\f1e6"; } + +.fa-plug-circle-bolt::before { + content: "\e55b"; } + +.fa-plug-circle-check::before { + content: "\e55c"; } + +.fa-plug-circle-exclamation::before { + content: "\e55d"; } + +.fa-plug-circle-minus::before { + content: "\e55e"; } + +.fa-plug-circle-plus::before { + content: "\e55f"; } + +.fa-plug-circle-xmark::before { + content: "\e560"; } + +.fa-plus::before { + content: "\2b"; } + +.fa-add::before { + content: "\2b"; } + +.fa-plus-minus::before { + content: "\e43c"; } + +.fa-podcast::before { + content: "\f2ce"; } + +.fa-poo::before { + content: "\f2fe"; } + +.fa-poo-storm::before { + content: "\f75a"; } + +.fa-poo-bolt::before { + content: "\f75a"; } + +.fa-poop::before { + content: "\f619"; } + +.fa-power-off::before { + content: "\f011"; } + +.fa-prescription::before { + content: "\f5b1"; } + +.fa-prescription-bottle::before { + content: "\f485"; } + +.fa-prescription-bottle-medical::before { + content: "\f486"; } + +.fa-prescription-bottle-alt::before { + content: "\f486"; } + +.fa-print::before { + content: "\f02f"; } + +.fa-pump-medical::before { + content: "\e06a"; } + +.fa-pump-soap::before { + content: "\e06b"; } + +.fa-puzzle-piece::before { + content: "\f12e"; } + +.fa-q::before { + content: "\51"; } + +.fa-qrcode::before { + content: "\f029"; } + +.fa-question::before { + content: "\3f"; } + +.fa-quote-left::before { + content: "\f10d"; } + +.fa-quote-left-alt::before { + content: "\f10d"; } + +.fa-quote-right::before { + content: "\f10e"; } + +.fa-quote-right-alt::before { + content: "\f10e"; } + +.fa-r::before { + content: "\52"; } + +.fa-radiation::before { + content: "\f7b9"; } + +.fa-radio::before { + content: "\f8d7"; } + +.fa-rainbow::before { + content: "\f75b"; } + +.fa-ranking-star::before { + content: "\e561"; } + +.fa-receipt::before { + content: "\f543"; } + +.fa-record-vinyl::before { + content: "\f8d9"; } + +.fa-rectangle-ad::before { + content: "\f641"; } + +.fa-ad::before { + content: "\f641"; } + +.fa-rectangle-list::before { + content: "\f022"; } + +.fa-list-alt::before { + content: "\f022"; } + +.fa-rectangle-xmark::before { + content: "\f410"; } + +.fa-rectangle-times::before { + content: "\f410"; } + +.fa-times-rectangle::before { + content: "\f410"; } + +.fa-window-close::before { + content: "\f410"; } + +.fa-recycle::before { + content: "\f1b8"; } + +.fa-registered::before { + content: "\f25d"; } + +.fa-repeat::before { + content: "\f363"; } + +.fa-reply::before { + content: "\f3e5"; } + +.fa-mail-reply::before { + content: "\f3e5"; } + +.fa-reply-all::before { + content: "\f122"; } + +.fa-mail-reply-all::before { + content: "\f122"; } + +.fa-republican::before { + content: "\f75e"; } + +.fa-restroom::before { + content: "\f7bd"; } + +.fa-retweet::before { + content: "\f079"; } + +.fa-ribbon::before { + content: "\f4d6"; } + +.fa-right-from-bracket::before { + content: "\f2f5"; } + +.fa-sign-out-alt::before { + content: "\f2f5"; } + +.fa-right-left::before { + content: "\f362"; } + +.fa-exchange-alt::before { + content: "\f362"; } + +.fa-right-long::before { + content: "\f30b"; } + +.fa-long-arrow-alt-right::before { + content: "\f30b"; } + +.fa-right-to-bracket::before { + content: "\f2f6"; } + +.fa-sign-in-alt::before { + content: "\f2f6"; } + +.fa-ring::before { + content: "\f70b"; } + +.fa-road::before { + content: "\f018"; } + +.fa-road-barrier::before { + content: "\e562"; } + +.fa-road-bridge::before { + content: "\e563"; } + +.fa-road-circle-check::before { + content: "\e564"; } + +.fa-road-circle-exclamation::before { + content: "\e565"; } + +.fa-road-circle-xmark::before { + content: "\e566"; } + +.fa-road-lock::before { + content: "\e567"; } + +.fa-road-spikes::before { + content: "\e568"; } + +.fa-robot::before { + content: "\f544"; } + +.fa-rocket::before { + content: "\f135"; } + +.fa-rotate::before { + content: "\f2f1"; } + +.fa-sync-alt::before { + content: "\f2f1"; } + +.fa-rotate-left::before { + content: "\f2ea"; } + +.fa-rotate-back::before { + content: "\f2ea"; } + +.fa-rotate-backward::before { + content: "\f2ea"; } + +.fa-undo-alt::before { + content: "\f2ea"; } + +.fa-rotate-right::before { + content: "\f2f9"; } + +.fa-redo-alt::before { + content: "\f2f9"; } + +.fa-rotate-forward::before { + content: "\f2f9"; } + +.fa-route::before { + content: "\f4d7"; } + +.fa-rss::before { + content: "\f09e"; } + +.fa-feed::before { + content: "\f09e"; } + +.fa-ruble-sign::before { + content: "\f158"; } + +.fa-rouble::before { + content: "\f158"; } + +.fa-rub::before { + content: "\f158"; } + +.fa-ruble::before { + content: "\f158"; } + +.fa-rug::before { + content: "\e569"; } + +.fa-ruler::before { + content: "\f545"; } + +.fa-ruler-combined::before { + content: "\f546"; } + +.fa-ruler-horizontal::before { + content: "\f547"; } + +.fa-ruler-vertical::before { + content: "\f548"; } + +.fa-rupee-sign::before { + content: "\f156"; } + +.fa-rupee::before { + content: "\f156"; } + +.fa-rupiah-sign::before { + content: "\e23d"; } + +.fa-s::before { + content: "\53"; } + +.fa-sack-dollar::before { + content: "\f81d"; } + +.fa-sack-xmark::before { + content: "\e56a"; } + +.fa-sailboat::before { + content: "\e445"; } + +.fa-satellite::before { + content: "\f7bf"; } + +.fa-satellite-dish::before { + content: "\f7c0"; } + +.fa-scale-balanced::before { + content: "\f24e"; } + +.fa-balance-scale::before { + content: "\f24e"; } + +.fa-scale-unbalanced::before { + content: "\f515"; } + +.fa-balance-scale-left::before { + content: "\f515"; } + +.fa-scale-unbalanced-flip::before { + content: "\f516"; } + +.fa-balance-scale-right::before { + content: "\f516"; } + +.fa-school::before { + content: "\f549"; } + +.fa-school-circle-check::before { + content: "\e56b"; } + +.fa-school-circle-exclamation::before { + content: "\e56c"; } + +.fa-school-circle-xmark::before { + content: "\e56d"; } + +.fa-school-flag::before { + content: "\e56e"; } + +.fa-school-lock::before { + content: "\e56f"; } + +.fa-scissors::before { + content: "\f0c4"; } + +.fa-cut::before { + content: "\f0c4"; } + +.fa-screwdriver::before { + content: "\f54a"; } + +.fa-screwdriver-wrench::before { + content: "\f7d9"; } + +.fa-tools::before { + content: "\f7d9"; } + +.fa-scroll::before { + content: "\f70e"; } + +.fa-scroll-torah::before { + content: "\f6a0"; } + +.fa-torah::before { + content: "\f6a0"; } + +.fa-sd-card::before { + content: "\f7c2"; } + +.fa-section::before { + content: "\e447"; } + +.fa-seedling::before { + content: "\f4d8"; } + +.fa-sprout::before { + content: "\f4d8"; } + +.fa-server::before { + content: "\f233"; } + +.fa-shapes::before { + content: "\f61f"; } + +.fa-triangle-circle-square::before { + content: "\f61f"; } + +.fa-share::before { + content: "\f064"; } + +.fa-arrow-turn-right::before { + content: "\f064"; } + +.fa-mail-forward::before { + content: "\f064"; } + +.fa-share-from-square::before { + content: "\f14d"; } + +.fa-share-square::before { + content: "\f14d"; } + +.fa-share-nodes::before { + content: "\f1e0"; } + +.fa-share-alt::before { + content: "\f1e0"; } + +.fa-sheet-plastic::before { + content: "\e571"; } + +.fa-shekel-sign::before { + content: "\f20b"; } + +.fa-ils::before { + content: "\f20b"; } + +.fa-shekel::before { + content: "\f20b"; } + +.fa-sheqel::before { + content: "\f20b"; } + +.fa-sheqel-sign::before { + content: "\f20b"; } + +.fa-shield::before { + content: "\f132"; } + +.fa-shield-blank::before { + content: "\f132"; } + +.fa-shield-cat::before { + content: "\e572"; } + +.fa-shield-dog::before { + content: "\e573"; } + +.fa-shield-halved::before { + content: "\f3ed"; } + +.fa-shield-alt::before { + content: "\f3ed"; } + +.fa-shield-heart::before { + content: "\e574"; } + +.fa-shield-virus::before { + content: "\e06c"; } + +.fa-ship::before { + content: "\f21a"; } + +.fa-shirt::before { + content: "\f553"; } + +.fa-t-shirt::before { + content: "\f553"; } + +.fa-tshirt::before { + content: "\f553"; } + +.fa-shoe-prints::before { + content: "\f54b"; } + +.fa-shop::before { + content: "\f54f"; } + +.fa-store-alt::before { + content: "\f54f"; } + +.fa-shop-lock::before { + content: "\e4a5"; } + +.fa-shop-slash::before { + content: "\e070"; } + +.fa-store-alt-slash::before { + content: "\e070"; } + +.fa-shower::before { + content: "\f2cc"; } + +.fa-shrimp::before { + content: "\e448"; } + +.fa-shuffle::before { + content: "\f074"; } + +.fa-random::before { + content: "\f074"; } + +.fa-shuttle-space::before { + content: "\f197"; } + +.fa-space-shuttle::before { + content: "\f197"; } + +.fa-sign-hanging::before { + content: "\f4d9"; } + +.fa-sign::before { + content: "\f4d9"; } + +.fa-signal::before { + content: "\f012"; } + +.fa-signal-5::before { + content: "\f012"; } + +.fa-signal-perfect::before { + content: "\f012"; } + +.fa-signature::before { + content: "\f5b7"; } + +.fa-signs-post::before { + content: "\f277"; } + +.fa-map-signs::before { + content: "\f277"; } + +.fa-sim-card::before { + content: "\f7c4"; } + +.fa-sink::before { + content: "\e06d"; } + +.fa-sitemap::before { + content: "\f0e8"; } + +.fa-skull::before { + content: "\f54c"; } + +.fa-skull-crossbones::before { + content: "\f714"; } + +.fa-slash::before { + content: "\f715"; } + +.fa-sleigh::before { + content: "\f7cc"; } + +.fa-sliders::before { + content: "\f1de"; } + +.fa-sliders-h::before { + content: "\f1de"; } + +.fa-smog::before { + content: "\f75f"; } + +.fa-smoking::before { + content: "\f48d"; } + +.fa-snowflake::before { + content: "\f2dc"; } + +.fa-snowman::before { + content: "\f7d0"; } + +.fa-snowplow::before { + content: "\f7d2"; } + +.fa-soap::before { + content: "\e06e"; } + +.fa-socks::before { + content: "\f696"; } + +.fa-solar-panel::before { + content: "\f5ba"; } + +.fa-sort::before { + content: "\f0dc"; } + +.fa-unsorted::before { + content: "\f0dc"; } + +.fa-sort-down::before { + content: "\f0dd"; } + +.fa-sort-desc::before { + content: "\f0dd"; } + +.fa-sort-up::before { + content: "\f0de"; } + +.fa-sort-asc::before { + content: "\f0de"; } + +.fa-spa::before { + content: "\f5bb"; } + +.fa-spaghetti-monster-flying::before { + content: "\f67b"; } + +.fa-pastafarianism::before { + content: "\f67b"; } + +.fa-spell-check::before { + content: "\f891"; } + +.fa-spider::before { + content: "\f717"; } + +.fa-spinner::before { + content: "\f110"; } + +.fa-splotch::before { + content: "\f5bc"; } + +.fa-spoon::before { + content: "\f2e5"; } + +.fa-utensil-spoon::before { + content: "\f2e5"; } + +.fa-spray-can::before { + content: "\f5bd"; } + +.fa-spray-can-sparkles::before { + content: "\f5d0"; } + +.fa-air-freshener::before { + content: "\f5d0"; } + +.fa-square::before { + content: "\f0c8"; } + +.fa-square-arrow-up-right::before { + content: "\f14c"; } + +.fa-external-link-square::before { + content: "\f14c"; } + +.fa-square-caret-down::before { + content: "\f150"; } + +.fa-caret-square-down::before { + content: "\f150"; } + +.fa-square-caret-left::before { + content: "\f191"; } + +.fa-caret-square-left::before { + content: "\f191"; } + +.fa-square-caret-right::before { + content: "\f152"; } + +.fa-caret-square-right::before { + content: "\f152"; } + +.fa-square-caret-up::before { + content: "\f151"; } + +.fa-caret-square-up::before { + content: "\f151"; } + +.fa-square-check::before { + content: "\f14a"; } + +.fa-check-square::before { + content: "\f14a"; } + +.fa-square-envelope::before { + content: "\f199"; } + +.fa-envelope-square::before { + content: "\f199"; } + +.fa-square-full::before { + content: "\f45c"; } + +.fa-square-h::before { + content: "\f0fd"; } + +.fa-h-square::before { + content: "\f0fd"; } + +.fa-square-minus::before { + content: "\f146"; } + +.fa-minus-square::before { + content: "\f146"; } + +.fa-square-nfi::before { + content: "\e576"; } + +.fa-square-parking::before { + content: "\f540"; } + +.fa-parking::before { + content: "\f540"; } + +.fa-square-pen::before { + content: "\f14b"; } + +.fa-pen-square::before { + content: "\f14b"; } + +.fa-pencil-square::before { + content: "\f14b"; } + +.fa-square-person-confined::before { + content: "\e577"; } + +.fa-square-phone::before { + content: "\f098"; } + +.fa-phone-square::before { + content: "\f098"; } + +.fa-square-phone-flip::before { + content: "\f87b"; } + +.fa-phone-square-alt::before { + content: "\f87b"; } + +.fa-square-plus::before { + content: "\f0fe"; } + +.fa-plus-square::before { + content: "\f0fe"; } + +.fa-square-poll-horizontal::before { + content: "\f682"; } + +.fa-poll-h::before { + content: "\f682"; } + +.fa-square-poll-vertical::before { + content: "\f681"; } + +.fa-poll::before { + content: "\f681"; } + +.fa-square-root-variable::before { + content: "\f698"; } + +.fa-square-root-alt::before { + content: "\f698"; } + +.fa-square-rss::before { + content: "\f143"; } + +.fa-rss-square::before { + content: "\f143"; } + +.fa-square-share-nodes::before { + content: "\f1e1"; } + +.fa-share-alt-square::before { + content: "\f1e1"; } + +.fa-square-up-right::before { + content: "\f360"; } + +.fa-external-link-square-alt::before { + content: "\f360"; } + +.fa-square-virus::before { + content: "\e578"; } + +.fa-square-xmark::before { + content: "\f2d3"; } + +.fa-times-square::before { + content: "\f2d3"; } + +.fa-xmark-square::before { + content: "\f2d3"; } + +.fa-staff-aesculapius::before { + content: "\e579"; } + +.fa-rod-asclepius::before { + content: "\e579"; } + +.fa-rod-snake::before { + content: "\e579"; } + +.fa-staff-snake::before { + content: "\e579"; } + +.fa-stairs::before { + content: "\e289"; } + +.fa-stamp::before { + content: "\f5bf"; } + +.fa-star::before { + content: "\f005"; } + +.fa-star-and-crescent::before { + content: "\f699"; } + +.fa-star-half::before { + content: "\f089"; } + +.fa-star-half-stroke::before { + content: "\f5c0"; } + +.fa-star-half-alt::before { + content: "\f5c0"; } + +.fa-star-of-david::before { + content: "\f69a"; } + +.fa-star-of-life::before { + content: "\f621"; } + +.fa-sterling-sign::before { + content: "\f154"; } + +.fa-gbp::before { + content: "\f154"; } + +.fa-pound-sign::before { + content: "\f154"; } + +.fa-stethoscope::before { + content: "\f0f1"; } + +.fa-stop::before { + content: "\f04d"; } + +.fa-stopwatch::before { + content: "\f2f2"; } + +.fa-stopwatch-20::before { + content: "\e06f"; } + +.fa-store::before { + content: "\f54e"; } + +.fa-store-slash::before { + content: "\e071"; } + +.fa-street-view::before { + content: "\f21d"; } + +.fa-strikethrough::before { + content: "\f0cc"; } + +.fa-stroopwafel::before { + content: "\f551"; } + +.fa-subscript::before { + content: "\f12c"; } + +.fa-suitcase::before { + content: "\f0f2"; } + +.fa-suitcase-medical::before { + content: "\f0fa"; } + +.fa-medkit::before { + content: "\f0fa"; } + +.fa-suitcase-rolling::before { + content: "\f5c1"; } + +.fa-sun::before { + content: "\f185"; } + +.fa-sun-plant-wilt::before { + content: "\e57a"; } + +.fa-superscript::before { + content: "\f12b"; } + +.fa-swatchbook::before { + content: "\f5c3"; } + +.fa-synagogue::before { + content: "\f69b"; } + +.fa-syringe::before { + content: "\f48e"; } + +.fa-t::before { + content: "\54"; } + +.fa-table::before { + content: "\f0ce"; } + +.fa-table-cells::before { + content: "\f00a"; } + +.fa-th::before { + content: "\f00a"; } + +.fa-table-cells-large::before { + content: "\f009"; } + +.fa-th-large::before { + content: "\f009"; } + +.fa-table-columns::before { + content: "\f0db"; } + +.fa-columns::before { + content: "\f0db"; } + +.fa-table-list::before { + content: "\f00b"; } + +.fa-th-list::before { + content: "\f00b"; } + +.fa-table-tennis-paddle-ball::before { + content: "\f45d"; } + +.fa-ping-pong-paddle-ball::before { + content: "\f45d"; } + +.fa-table-tennis::before { + content: "\f45d"; } + +.fa-tablet::before { + content: "\f3fb"; } + +.fa-tablet-android::before { + content: "\f3fb"; } + +.fa-tablet-button::before { + content: "\f10a"; } + +.fa-tablet-screen-button::before { + content: "\f3fa"; } + +.fa-tablet-alt::before { + content: "\f3fa"; } + +.fa-tablets::before { + content: "\f490"; } + +.fa-tachograph-digital::before { + content: "\f566"; } + +.fa-digital-tachograph::before { + content: "\f566"; } + +.fa-tag::before { + content: "\f02b"; } + +.fa-tags::before { + content: "\f02c"; } + +.fa-tape::before { + content: "\f4db"; } + +.fa-tarp::before { + content: "\e57b"; } + +.fa-tarp-droplet::before { + content: "\e57c"; } + +.fa-taxi::before { + content: "\f1ba"; } + +.fa-cab::before { + content: "\f1ba"; } + +.fa-teeth::before { + content: "\f62e"; } + +.fa-teeth-open::before { + content: "\f62f"; } + +.fa-temperature-arrow-down::before { + content: "\e03f"; } + +.fa-temperature-down::before { + content: "\e03f"; } + +.fa-temperature-arrow-up::before { + content: "\e040"; } + +.fa-temperature-up::before { + content: "\e040"; } + +.fa-temperature-empty::before { + content: "\f2cb"; } + +.fa-temperature-0::before { + content: "\f2cb"; } + +.fa-thermometer-0::before { + content: "\f2cb"; } + +.fa-thermometer-empty::before { + content: "\f2cb"; } + +.fa-temperature-full::before { + content: "\f2c7"; } + +.fa-temperature-4::before { + content: "\f2c7"; } + +.fa-thermometer-4::before { + content: "\f2c7"; } + +.fa-thermometer-full::before { + content: "\f2c7"; } + +.fa-temperature-half::before { + content: "\f2c9"; } + +.fa-temperature-2::before { + content: "\f2c9"; } + +.fa-thermometer-2::before { + content: "\f2c9"; } + +.fa-thermometer-half::before { + content: "\f2c9"; } + +.fa-temperature-high::before { + content: "\f769"; } + +.fa-temperature-low::before { + content: "\f76b"; } + +.fa-temperature-quarter::before { + content: "\f2ca"; } + +.fa-temperature-1::before { + content: "\f2ca"; } + +.fa-thermometer-1::before { + content: "\f2ca"; } + +.fa-thermometer-quarter::before { + content: "\f2ca"; } + +.fa-temperature-three-quarters::before { + content: "\f2c8"; } + +.fa-temperature-3::before { + content: "\f2c8"; } + +.fa-thermometer-3::before { + content: "\f2c8"; } + +.fa-thermometer-three-quarters::before { + content: "\f2c8"; } + +.fa-tenge-sign::before { + content: "\f7d7"; } + +.fa-tenge::before { + content: "\f7d7"; } + +.fa-tent::before { + content: "\e57d"; } + +.fa-tent-arrow-down-to-line::before { + content: "\e57e"; } + +.fa-tent-arrow-left-right::before { + content: "\e57f"; } + +.fa-tent-arrow-turn-left::before { + content: "\e580"; } + +.fa-tent-arrows-down::before { + content: "\e581"; } + +.fa-tents::before { + content: "\e582"; } + +.fa-terminal::before { + content: "\f120"; } + +.fa-text-height::before { + content: "\f034"; } + +.fa-text-slash::before { + content: "\f87d"; } + +.fa-remove-format::before { + content: "\f87d"; } + +.fa-text-width::before { + content: "\f035"; } + +.fa-thermometer::before { + content: "\f491"; } + +.fa-thumbs-down::before { + content: "\f165"; } + +.fa-thumbs-up::before { + content: "\f164"; } + +.fa-thumbtack::before { + content: "\f08d"; } + +.fa-thumb-tack::before { + content: "\f08d"; } + +.fa-ticket::before { + content: "\f145"; } + +.fa-ticket-simple::before { + content: "\f3ff"; } + +.fa-ticket-alt::before { + content: "\f3ff"; } + +.fa-timeline::before { + content: "\e29c"; } + +.fa-toggle-off::before { + content: "\f204"; } + +.fa-toggle-on::before { + content: "\f205"; } + +.fa-toilet::before { + content: "\f7d8"; } + +.fa-toilet-paper::before { + content: "\f71e"; } + +.fa-toilet-paper-slash::before { + content: "\e072"; } + +.fa-toilet-portable::before { + content: "\e583"; } + +.fa-toilets-portable::before { + content: "\e584"; } + +.fa-toolbox::before { + content: "\f552"; } + +.fa-tooth::before { + content: "\f5c9"; } + +.fa-torii-gate::before { + content: "\f6a1"; } + +.fa-tornado::before { + content: "\f76f"; } + +.fa-tower-broadcast::before { + content: "\f519"; } + +.fa-broadcast-tower::before { + content: "\f519"; } + +.fa-tower-cell::before { + content: "\e585"; } + +.fa-tower-observation::before { + content: "\e586"; } + +.fa-tractor::before { + content: "\f722"; } + +.fa-trademark::before { + content: "\f25c"; } + +.fa-traffic-light::before { + content: "\f637"; } + +.fa-trailer::before { + content: "\e041"; } + +.fa-train::before { + content: "\f238"; } + +.fa-train-subway::before { + content: "\f239"; } + +.fa-subway::before { + content: "\f239"; } + +.fa-train-tram::before { + content: "\f7da"; } + +.fa-tram::before { + content: "\f7da"; } + +.fa-transgender::before { + content: "\f225"; } + +.fa-transgender-alt::before { + content: "\f225"; } + +.fa-trash::before { + content: "\f1f8"; } + +.fa-trash-arrow-up::before { + content: "\f829"; } + +.fa-trash-restore::before { + content: "\f829"; } + +.fa-trash-can::before { + content: "\f2ed"; } + +.fa-trash-alt::before { + content: "\f2ed"; } + +.fa-trash-can-arrow-up::before { + content: "\f82a"; } + +.fa-trash-restore-alt::before { + content: "\f82a"; } + +.fa-tree::before { + content: "\f1bb"; } + +.fa-tree-city::before { + content: "\e587"; } + +.fa-triangle-exclamation::before { + content: "\f071"; } + +.fa-exclamation-triangle::before { + content: "\f071"; } + +.fa-warning::before { + content: "\f071"; } + +.fa-trophy::before { + content: "\f091"; } + +.fa-trowel::before { + content: "\e589"; } + +.fa-trowel-bricks::before { + content: "\e58a"; } + +.fa-truck::before { + content: "\f0d1"; } + +.fa-truck-arrow-right::before { + content: "\e58b"; } + +.fa-truck-droplet::before { + content: "\e58c"; } + +.fa-truck-fast::before { + content: "\f48b"; } + +.fa-shipping-fast::before { + content: "\f48b"; } + +.fa-truck-field::before { + content: "\e58d"; } + +.fa-truck-field-un::before { + content: "\e58e"; } + +.fa-truck-front::before { + content: "\e2b7"; } + +.fa-truck-medical::before { + content: "\f0f9"; } + +.fa-ambulance::before { + content: "\f0f9"; } + +.fa-truck-monster::before { + content: "\f63b"; } + +.fa-truck-moving::before { + content: "\f4df"; } + +.fa-truck-pickup::before { + content: "\f63c"; } + +.fa-truck-plane::before { + content: "\e58f"; } + +.fa-truck-ramp-box::before { + content: "\f4de"; } + +.fa-truck-loading::before { + content: "\f4de"; } + +.fa-tty::before { + content: "\f1e4"; } + +.fa-teletype::before { + content: "\f1e4"; } + +.fa-turkish-lira-sign::before { + content: "\e2bb"; } + +.fa-try::before { + content: "\e2bb"; } + +.fa-turkish-lira::before { + content: "\e2bb"; } + +.fa-turn-down::before { + content: "\f3be"; } + +.fa-level-down-alt::before { + content: "\f3be"; } + +.fa-turn-up::before { + content: "\f3bf"; } + +.fa-level-up-alt::before { + content: "\f3bf"; } + +.fa-tv::before { + content: "\f26c"; } + +.fa-television::before { + content: "\f26c"; } + +.fa-tv-alt::before { + content: "\f26c"; } + +.fa-u::before { + content: "\55"; } + +.fa-umbrella::before { + content: "\f0e9"; } + +.fa-umbrella-beach::before { + content: "\f5ca"; } + +.fa-underline::before { + content: "\f0cd"; } + +.fa-universal-access::before { + content: "\f29a"; } + +.fa-unlock::before { + content: "\f09c"; } + +.fa-unlock-keyhole::before { + content: "\f13e"; } + +.fa-unlock-alt::before { + content: "\f13e"; } + +.fa-up-down::before { + content: "\f338"; } + +.fa-arrows-alt-v::before { + content: "\f338"; } + +.fa-up-down-left-right::before { + content: "\f0b2"; } + +.fa-arrows-alt::before { + content: "\f0b2"; } + +.fa-up-long::before { + content: "\f30c"; } + +.fa-long-arrow-alt-up::before { + content: "\f30c"; } + +.fa-up-right-and-down-left-from-center::before { + content: "\f424"; } + +.fa-expand-alt::before { + content: "\f424"; } + +.fa-up-right-from-square::before { + content: "\f35d"; } + +.fa-external-link-alt::before { + content: "\f35d"; } + +.fa-upload::before { + content: "\f093"; } + +.fa-user::before { + content: "\f007"; } + +.fa-user-astronaut::before { + content: "\f4fb"; } + +.fa-user-check::before { + content: "\f4fc"; } + +.fa-user-clock::before { + content: "\f4fd"; } + +.fa-user-doctor::before { + content: "\f0f0"; } + +.fa-user-md::before { + content: "\f0f0"; } + +.fa-user-gear::before { + content: "\f4fe"; } + +.fa-user-cog::before { + content: "\f4fe"; } + +.fa-user-graduate::before { + content: "\f501"; } + +.fa-user-group::before { + content: "\f500"; } + +.fa-user-friends::before { + content: "\f500"; } + +.fa-user-injured::before { + content: "\f728"; } + +.fa-user-large::before { + content: "\f406"; } + +.fa-user-alt::before { + content: "\f406"; } + +.fa-user-large-slash::before { + content: "\f4fa"; } + +.fa-user-alt-slash::before { + content: "\f4fa"; } + +.fa-user-lock::before { + content: "\f502"; } + +.fa-user-minus::before { + content: "\f503"; } + +.fa-user-ninja::before { + content: "\f504"; } + +.fa-user-nurse::before { + content: "\f82f"; } + +.fa-user-pen::before { + content: "\f4ff"; } + +.fa-user-edit::before { + content: "\f4ff"; } + +.fa-user-plus::before { + content: "\f234"; } + +.fa-user-secret::before { + content: "\f21b"; } + +.fa-user-shield::before { + content: "\f505"; } + +.fa-user-slash::before { + content: "\f506"; } + +.fa-user-tag::before { + content: "\f507"; } + +.fa-user-tie::before { + content: "\f508"; } + +.fa-user-xmark::before { + content: "\f235"; } + +.fa-user-times::before { + content: "\f235"; } + +.fa-users::before { + content: "\f0c0"; } + +.fa-users-between-lines::before { + content: "\e591"; } + +.fa-users-gear::before { + content: "\f509"; } + +.fa-users-cog::before { + content: "\f509"; } + +.fa-users-line::before { + content: "\e592"; } + +.fa-users-rays::before { + content: "\e593"; } + +.fa-users-rectangle::before { + content: "\e594"; } + +.fa-users-slash::before { + content: "\e073"; } + +.fa-users-viewfinder::before { + content: "\e595"; } + +.fa-utensils::before { + content: "\f2e7"; } + +.fa-cutlery::before { + content: "\f2e7"; } + +.fa-v::before { + content: "\56"; } + +.fa-van-shuttle::before { + content: "\f5b6"; } + +.fa-shuttle-van::before { + content: "\f5b6"; } + +.fa-vault::before { + content: "\e2c5"; } + +.fa-vector-square::before { + content: "\f5cb"; } + +.fa-venus::before { + content: "\f221"; } + +.fa-venus-double::before { + content: "\f226"; } + +.fa-venus-mars::before { + content: "\f228"; } + +.fa-vest::before { + content: "\e085"; } + +.fa-vest-patches::before { + content: "\e086"; } + +.fa-vial::before { + content: "\f492"; } + +.fa-vial-circle-check::before { + content: "\e596"; } + +.fa-vial-virus::before { + content: "\e597"; } + +.fa-vials::before { + content: "\f493"; } + +.fa-video::before { + content: "\f03d"; } + +.fa-video-camera::before { + content: "\f03d"; } + +.fa-video-slash::before { + content: "\f4e2"; } + +.fa-vihara::before { + content: "\f6a7"; } + +.fa-virus::before { + content: "\e074"; } + +.fa-virus-covid::before { + content: "\e4a8"; } + +.fa-virus-covid-slash::before { + content: "\e4a9"; } + +.fa-virus-slash::before { + content: "\e075"; } + +.fa-viruses::before { + content: "\e076"; } + +.fa-voicemail::before { + content: "\f897"; } + +.fa-volcano::before { + content: "\f770"; } + +.fa-volleyball::before { + content: "\f45f"; } + +.fa-volleyball-ball::before { + content: "\f45f"; } + +.fa-volume-high::before { + content: "\f028"; } + +.fa-volume-up::before { + content: "\f028"; } + +.fa-volume-low::before { + content: "\f027"; } + +.fa-volume-down::before { + content: "\f027"; } + +.fa-volume-off::before { + content: "\f026"; } + +.fa-volume-xmark::before { + content: "\f6a9"; } + +.fa-volume-mute::before { + content: "\f6a9"; } + +.fa-volume-times::before { + content: "\f6a9"; } + +.fa-vr-cardboard::before { + content: "\f729"; } + +.fa-w::before { + content: "\57"; } + +.fa-walkie-talkie::before { + content: "\f8ef"; } + +.fa-wallet::before { + content: "\f555"; } + +.fa-wand-magic::before { + content: "\f0d0"; } + +.fa-magic::before { + content: "\f0d0"; } + +.fa-wand-magic-sparkles::before { + content: "\e2ca"; } + +.fa-magic-wand-sparkles::before { + content: "\e2ca"; } + +.fa-wand-sparkles::before { + content: "\f72b"; } + +.fa-warehouse::before { + content: "\f494"; } + +.fa-water::before { + content: "\f773"; } + +.fa-water-ladder::before { + content: "\f5c5"; } + +.fa-ladder-water::before { + content: "\f5c5"; } + +.fa-swimming-pool::before { + content: "\f5c5"; } + +.fa-wave-square::before { + content: "\f83e"; } + +.fa-weight-hanging::before { + content: "\f5cd"; } + +.fa-weight-scale::before { + content: "\f496"; } + +.fa-weight::before { + content: "\f496"; } + +.fa-wheat-awn::before { + content: "\e2cd"; } + +.fa-wheat-alt::before { + content: "\e2cd"; } + +.fa-wheat-awn-circle-exclamation::before { + content: "\e598"; } + +.fa-wheelchair::before { + content: "\f193"; } + +.fa-wheelchair-move::before { + content: "\e2ce"; } + +.fa-wheelchair-alt::before { + content: "\e2ce"; } + +.fa-whiskey-glass::before { + content: "\f7a0"; } + +.fa-glass-whiskey::before { + content: "\f7a0"; } + +.fa-wifi::before { + content: "\f1eb"; } + +.fa-wifi-3::before { + content: "\f1eb"; } + +.fa-wifi-strong::before { + content: "\f1eb"; } + +.fa-wind::before { + content: "\f72e"; } + +.fa-window-maximize::before { + content: "\f2d0"; } + +.fa-window-minimize::before { + content: "\f2d1"; } + +.fa-window-restore::before { + content: "\f2d2"; } + +.fa-wine-bottle::before { + content: "\f72f"; } + +.fa-wine-glass::before { + content: "\f4e3"; } + +.fa-wine-glass-empty::before { + content: "\f5ce"; } + +.fa-wine-glass-alt::before { + content: "\f5ce"; } + +.fa-won-sign::before { + content: "\f159"; } + +.fa-krw::before { + content: "\f159"; } + +.fa-won::before { + content: "\f159"; } + +.fa-worm::before { + content: "\e599"; } + +.fa-wrench::before { + content: "\f0ad"; } + +.fa-x::before { + content: "\58"; } + +.fa-x-ray::before { + content: "\f497"; } + +.fa-xmark::before { + content: "\f00d"; } + +.fa-close::before { + content: "\f00d"; } + +.fa-multiply::before { + content: "\f00d"; } + +.fa-remove::before { + content: "\f00d"; } + +.fa-times::before { + content: "\f00d"; } + +.fa-xmarks-lines::before { + content: "\e59a"; } + +.fa-y::before { + content: "\59"; } + +.fa-yen-sign::before { + content: "\f157"; } + +.fa-cny::before { + content: "\f157"; } + +.fa-jpy::before { + content: "\f157"; } + +.fa-rmb::before { + content: "\f157"; } + +.fa-yen::before { + content: "\f157"; } + +.fa-yin-yang::before { + content: "\f6ad"; } + +.fa-z::before { + content: "\5a"; } + +.sr-only, +.fa-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } + +.sr-only-focusable:not(:focus), +.fa-sr-only-focusable:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } diff --git a/resources/problem.scss b/resources/problem.scss index 1b80f40..060d796 100644 --- a/resources/problem.scss +++ b/resources/problem.scss @@ -77,6 +77,7 @@ flex: 26.5%; max-width: unset; padding-top: 0; + padding-left: 0; } } @@ -293,3 +294,39 @@ ul.problem-list { font-size: 13px; } } + +.problem-feed-option { + width: 90%; + margin-left: auto; + margin-right: auto; + padding: 1em; + border-radius: 5px; + margin-bottom: 1em +} + +.problem-feed-option-item { + padding: 10px 15px; + border-radius: 2em; + font-weight: bold; + background: lightgray; + margin-right: 1em; + color: gray; + cursor: pointer; +} + +.pcodecell { + text-transform: uppercase; +} + +.problem-feed-option-item.active { + background: goldenrod; + color: darkblue; +} + +@media (max-width: 799px) { + .problem-middle-right { + display: flex; + flex-direction: column-reverse; + } +} + diff --git a/templates/blog/list.html b/templates/blog/list.html index d8eee93..98cb698 100644 --- a/templates/blog/list.html +++ b/templates/blog/list.html @@ -1,21 +1,17 @@ -{% extends "base.html" %} -{% block title_row %}{% endblock %} -{% block title_ruler %}{% endblock %} - -{% block media %} - - - - - - +{% extends "three-column-content.html" %} +{% block three_col_media %} {% include "blog/media-css.html" %} {% endblock %} -{% block js_media %} +{% block three_col_js %} {% endblock %} -{% block body %} - {% block before_posts %}{% endblock %} -
- +{% block left_sidebar %} + -
-
- - - - - -
-
- {% set show_organization_private_icon=True %} - {% if feed_type == 'blog' %} - {% for post in posts %} - {% include "blog/content.html" %} - {% endfor %} - {% elif feed_type == 'problem' %} - {% for problem in problems %} - {% include "problem/feed.html" %} - {% endfor %} - {% elif feed_type == 'ticket' %} - {% if tickets %} - {% for ticket in tickets %} - {% include "ticket/feed.html" %} - {% endfor %} - {% else %} -

{{_('You have no ticket')}}

- {% endif %} - {% elif feed_type == 'comment' %} - {% for comment in comments %} - {% include "comments/feed.html" %} - {% endfor %} - {% endif %} - {% if page_obj.num_pages > 1 %} -
{% include "list-pages.html" %}
- {% endif %} -
- -
- {% if request.in_contest_mode and request.participation.contest.use_clarifications %} - - {% endif %} - {% if current_contests %} - - {% endif %} - - {% if future_contests %} - - {% endif %} - - -
-
- {% block after_posts %}{% endblock %} {% endblock %} -{% block bodyend %} - {{ super() }} - {% if REQUIRE_JAX %} - {% include "mathjax-load.html" %} +{% block middle_content %} + {% set show_organization_private_icon=True %} + {% if page_type == 'blog' %} + {% for post in posts %} + {% include "blog/content.html" %} + {% endfor %} + {% elif page_type == 'ticket' %} + {% if tickets %} + {% for ticket in tickets %} + {% include "ticket/feed.html" %} + {% endfor %} + {% else %} +

{{_('You have no ticket')}}

+ {% endif %} + {% elif page_type == 'comment' %} + {% for comment in comments %} + {% include "comments/feed.html" %} + {% endfor %} + {% endif %} + {% if page_obj.num_pages > 1 %} +
{% include "list-pages.html" %}
{% endif %} - {% include "comments/math.html" %} {% endblock %} + +{% block right_sidebar %} + +{% endblock %} \ No newline at end of file diff --git a/templates/problem/feed.html b/templates/problem/feed.html index 9c7d749..56e4d5b 100644 --- a/templates/problem/feed.html +++ b/templates/problem/feed.html @@ -3,6 +3,13 @@ {{ problem.name }} + {% if problem.id in completed_problem_ids %} + + {% elif problem.id in attempted_problems %} + + {% else %} + + {% endif %} {% with authors=problem.authors.all() %} {% if authors %} @@ -12,7 +19,7 @@
{% endif %} {% endwith %} - {% if true %} + {% if show_types %}
{% for type in problem.types_list %} diff --git a/templates/problem/list.html b/templates/problem/list.html index 893e5c8..e66ca31 100644 --- a/templates/problem/list.html +++ b/templates/problem/list.html @@ -1,6 +1,5 @@ -{% extends "common-content.html" %} - -{% block media %} +{% extends "three-column-content.html" %} +{% block three_col_media %}
%s" % (flatatt(ace_attrs), textarea) # add toolbar - html = ('
' - '
%s
') % html + html = ( + '
' + '
%s
' + ) % html return mark_safe(html) diff --git a/dmoj/celery.py b/dmoj/celery.py index 96718ea..5f0c87d 100644 --- a/dmoj/celery.py +++ b/dmoj/celery.py @@ -4,24 +4,30 @@ import socket from celery import Celery from celery.signals import task_failure -app = Celery('dmoj') +app = Celery("dmoj") from django.conf import settings # noqa: E402, I202, django must be imported here -app.config_from_object(settings, namespace='CELERY') -if hasattr(settings, 'CELERY_BROKER_URL_SECRET'): +app.config_from_object(settings, namespace="CELERY") + +if hasattr(settings, "CELERY_BROKER_URL_SECRET"): app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET -if hasattr(settings, 'CELERY_RESULT_BACKEND_SECRET'): +if hasattr(settings, "CELERY_RESULT_BACKEND_SECRET"): app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET # Load task modules from all registered Django app configs. app.autodiscover_tasks() # Logger to enable errors be reported. -logger = logging.getLogger('judge.celery') +logger = logging.getLogger("judge.celery") @task_failure.connect() def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs): - logger.error('Celery Task %s: %s on %s', sender.name, task_id, socket.gethostname(), # noqa: G201 - exc_info=(type(exception), exception, traceback)) + logger.error( + "Celery Task %s: %s on %s", + sender.name, + task_id, + socket.gethostname(), # noqa: G201 + exc_info=(type(exception), exception, traceback), + ) diff --git a/dmoj/decorators.py b/dmoj/decorators.py index b6c6d0d..fe483c5 100644 --- a/dmoj/decorators.py +++ b/dmoj/decorators.py @@ -1,12 +1,14 @@ import time + def timeit(method): def timed(*args, **kw): ts = time.time() result = method(*args, **kw) te = time.time() - if 'log_time' in kw: - name = kw.get('log_name', method.__name__.upper()) - kw['log_time'][name] = int((te - ts) * 1000) + if "log_time" in kw: + name = kw.get("log_name", method.__name__.upper()) + kw["log_time"][name] = int((te - ts) * 1000) return result - return timed \ No newline at end of file + + return timed diff --git a/dmoj/settings.py b/dmoj/settings.py index 634b6e4..d206186 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -22,7 +22,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0' +SECRET_KEY = "5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -30,8 +30,8 @@ DEBUG = True ALLOWED_HOSTS = [] SITE_ID = 1 -SITE_NAME = 'LQDOJ' -SITE_LONG_NAME = 'LQDOJ: Le Quy Don Online Judge' +SITE_NAME = "LQDOJ" +SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge" SITE_ADMIN_EMAIL = False DMOJ_REQUIRE_STAFF_2FA = True @@ -44,13 +44,15 @@ DMOJ_SSL = 0 # Refer to dmoj.ca/post/103-point-system-rework DMOJ_PP_STEP = 0.95 DMOJ_PP_ENTRIES = 100 -DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997 ** n) # noqa: E731 +DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997**n) # noqa: E731 -NODEJS = '/usr/bin/node' -EXIFTOOL = '/usr/bin/exiftool' -ACE_URL = '//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3' -SELECT2_JS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js' -DEFAULT_SELECT2_CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css' +NODEJS = "/usr/bin/node" +EXIFTOOL = "/usr/bin/exiftool" +ACE_URL = "//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3" +SELECT2_JS_URL = "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js" +DEFAULT_SELECT2_CSS = ( + "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css" +) DMOJ_CAMO_URL = None DMOJ_CAMO_KEY = None @@ -74,14 +76,14 @@ DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7 DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1 DMOJ_USER_MAX_ORGANIZATION_COUNT = 10 DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5 -DMOJ_PDF_PROBLEM_CACHE = '' +DMOJ_PDF_PROBLEM_CACHE = "" DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir() DMOJ_STATS_SUBMISSION_RESULT_COLORS = { - 'TLE': '#a3bcbd', - 'AC': '#00a92a', - 'WA': '#ed4420', - 'CE': '#42586d', - 'ERR': '#ffa71c', + "TLE": "#a3bcbd", + "AC": "#00a92a", + "WA": "#ed4420", + "CE": "#42586d", + "ERR": "#ffa71c", } MARKDOWN_STYLES = {} @@ -90,14 +92,14 @@ MARKDOWN_DEFAULT_STYLE = {} MATHOID_URL = False MATHOID_GZIP = False MATHOID_MML_CACHE = None -MATHOID_CSS_CACHE = 'default' -MATHOID_DEFAULT_TYPE = 'auto' +MATHOID_CSS_CACHE = "default" +MATHOID_DEFAULT_TYPE = "auto" MATHOID_MML_CACHE_TTL = 86400 -MATHOID_CACHE_ROOT = tempfile.gettempdir() + '/mathoidCache' +MATHOID_CACHE_ROOT = tempfile.gettempdir() + "/mathoidCache" MATHOID_CACHE_URL = False TEXOID_GZIP = False -TEXOID_META_CACHE = 'default' +TEXOID_META_CACHE = "default" TEXOID_META_CACHE_TTL = 86400 DMOJ_NEWSLETTER_ID_ON_REGISTER = 1 @@ -110,31 +112,33 @@ TIMEZONE_MAP = None TIMEZONE_DETECT_BACKEND = None TERMS_OF_SERVICE_URL = None -DEFAULT_USER_LANGUAGE = 'PY3' +DEFAULT_USER_LANGUAGE = "PY3" -PHANTOMJS = '' +PHANTOMJS = "" PHANTOMJS_PDF_ZOOM = 0.75 PHANTOMJS_PDF_TIMEOUT = 5.0 -PHANTOMJS_PAPER_SIZE = 'Letter' +PHANTOMJS_PAPER_SIZE = "Letter" -SLIMERJS = '' +SLIMERJS = "" SLIMERJS_PDF_ZOOM = 0.75 -SLIMERJS_FIREFOX_PATH = '' -SLIMERJS_PAPER_SIZE = 'Letter' +SLIMERJS_FIREFOX_PATH = "" +SLIMERJS_PAPER_SIZE = "Letter" -PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer' -PUPPETEER_PAPER_SIZE = 'Letter' +PUPPETEER_MODULE = "/usr/lib/node_modules/puppeteer" +PUPPETEER_PAPER_SIZE = "Letter" USE_SELENIUM = False SELENIUM_CUSTOM_CHROME_PATH = None -SELENIUM_CHROMEDRIVER_PATH = 'chromedriver' +SELENIUM_CHROMEDRIVER_PATH = "chromedriver" -PYGMENT_THEME = 'pygment-github.css' +PYGMENT_THEME = "pygment-github.css" INLINE_JQUERY = True INLINE_FONTAWESOME = True -JQUERY_JS = '//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js' -FONTAWESOME_CSS = '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css' -DMOJ_CANONICAL = '' +JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" +FONTAWESOME_CSS = ( + "//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" +) +DMOJ_CANONICAL = "" # Application definition @@ -146,130 +150,130 @@ except ImportError: pass else: del wpadmin - INSTALLED_APPS += ('wpadmin',) + INSTALLED_APPS += ("wpadmin",) WPADMIN = { - 'admin': { - 'title': 'LQDOJ Admin', - 'menu': { - 'top': 'wpadmin.menu.menus.BasicTopMenu', - 'left': 'wpadmin.menu.custom.CustomModelLeftMenuWithDashboard', + "admin": { + "title": "LQDOJ Admin", + "menu": { + "top": "wpadmin.menu.menus.BasicTopMenu", + "left": "wpadmin.menu.custom.CustomModelLeftMenuWithDashboard", }, - 'custom_menu': [ + "custom_menu": [ { - 'model': 'judge.Problem', - 'icon': 'fa-question-circle', - 'children': [ - 'judge.ProblemGroup', - 'judge.ProblemType', - 'judge.ProblemPointsVote', + "model": "judge.Problem", + "icon": "fa-question-circle", + "children": [ + "judge.ProblemGroup", + "judge.ProblemType", + "judge.ProblemPointsVote", ], }, { - 'model': 'judge.Submission', - 'icon': 'fa-check-square-o', - 'children': [ - 'judge.Language', - 'judge.Judge', + "model": "judge.Submission", + "icon": "fa-check-square-o", + "children": [ + "judge.Language", + "judge.Judge", ], }, { - 'model': 'judge.Contest', - 'icon': 'fa-bar-chart', - 'children': [ - 'judge.ContestParticipation', - 'judge.ContestTag', + "model": "judge.Contest", + "icon": "fa-bar-chart", + "children": [ + "judge.ContestParticipation", + "judge.ContestTag", ], }, { - 'model': 'auth.User', - 'icon': 'fa-user', - 'children': [ - 'auth.Group', - 'registration.RegistrationProfile', + "model": "auth.User", + "icon": "fa-user", + "children": [ + "auth.Group", + "registration.RegistrationProfile", ], }, { - 'model': 'judge.Profile', - 'icon': 'fa-user-plus', - 'children': [ - 'judge.Organization', - 'judge.OrganizationRequest', + "model": "judge.Profile", + "icon": "fa-user-plus", + "children": [ + "judge.Organization", + "judge.OrganizationRequest", ], }, { - 'model': 'judge.NavigationBar', - 'icon': 'fa-bars', - 'children': [ - 'judge.MiscConfig', - 'judge.License', - 'sites.Site', - 'redirects.Redirect', + "model": "judge.NavigationBar", + "icon": "fa-bars", + "children": [ + "judge.MiscConfig", + "judge.License", + "sites.Site", + "redirects.Redirect", ], }, - ('judge.BlogPost', 'fa-rss-square'), - ('judge.Comment', 'fa-comment-o'), - ('judge.Ticket', 'fa-exclamation-circle'), - ('flatpages.FlatPage', 'fa-file-text-o'), - ('judge.Solution', 'fa-pencil'), + ("judge.BlogPost", "fa-rss-square"), + ("judge.Comment", "fa-comment-o"), + ("judge.Ticket", "fa-exclamation-circle"), + ("flatpages.FlatPage", "fa-file-text-o"), + ("judge.Solution", "fa-pencil"), ], - 'dashboard': { - 'breadcrumbs': True, + "dashboard": { + "breadcrumbs": True, }, }, } INSTALLED_APPS += ( - 'django.contrib.admin', - 'judge', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.flatpages', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.redirects', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'registration', - 'mptt', - 'reversion', - 'reversion_compare', - 'django_social_share', - 'social_django', - 'compressor', - 'django_ace', - 'pagedown', - 'sortedm2m', - 'statici18n', - 'impersonate', - 'django_jinja', - 'chat_box', - 'newsletter', - 'django.forms', + "django.contrib.admin", + "judge", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.flatpages", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.redirects", + "django.contrib.staticfiles", + "django.contrib.sites", + "django.contrib.sitemaps", + "registration", + "mptt", + "reversion", + "reversion_compare", + "django_social_share", + "social_django", + "compressor", + "django_ace", + "pagedown", + "sortedm2m", + "statici18n", + "impersonate", + "django_jinja", + "chat_box", + "newsletter", + "django.forms", ) MIDDLEWARE = ( - 'judge.middleware.ShortCircuitMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'judge.middleware.DMOJLoginMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'judge.user_log.LogUserAccessMiddleware', - 'judge.timezone.TimezoneMiddleware', - 'impersonate.middleware.ImpersonateMiddleware', - 'judge.middleware.DMOJImpersonationMiddleware', - 'judge.middleware.ContestMiddleware', - 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', - 'judge.social_auth.SocialAuthExceptionMiddleware', - 'django.contrib.redirects.middleware.RedirectFallbackMiddleware', + "judge.middleware.ShortCircuitMiddleware", + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "judge.middleware.DMOJLoginMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "judge.user_log.LogUserAccessMiddleware", + "judge.timezone.TimezoneMiddleware", + "impersonate.middleware.ImpersonateMiddleware", + "judge.middleware.DMOJImpersonationMiddleware", + "judge.middleware.ContestMiddleware", + "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware", + "judge.social_auth.SocialAuthExceptionMiddleware", + "django.contrib.redirects.middleware.RedirectFallbackMiddleware", ) -FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' +FORM_RENDERER = "django.forms.renderers.TemplatesSetting" IMPERSONATE_REQUIRE_SUPERUSER = True IMPERSONATE_DISABLE_LOGGING = True @@ -278,226 +282,229 @@ ACCOUNT_ACTIVATION_DAYS = 7 AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'judge.utils.pwned.PwnedPasswordsValidator', + "NAME": "judge.utils.pwned.PwnedPasswordsValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -SILENCED_SYSTEM_CHECKS = ['urls.W002', 'fields.W342'] +SILENCED_SYSTEM_CHECKS = ["urls.W002", "fields.W342"] -ROOT_URLCONF = 'dmoj.urls' -LOGIN_REDIRECT_URL = '/user' -WSGI_APPLICATION = 'dmoj.wsgi.application' +ROOT_URLCONF = "dmoj.urls" +LOGIN_REDIRECT_URL = "/user" +WSGI_APPLICATION = "dmoj.wsgi.application" TEMPLATES = [ { - 'BACKEND': 'django_jinja.backend.Jinja2', - 'DIRS': [ - os.path.join(BASE_DIR, 'templates'), + "BACKEND": "django_jinja.backend.Jinja2", + "DIRS": [ + os.path.join(BASE_DIR, "templates"), ], - 'APP_DIRS': False, - 'OPTIONS': { - 'match_extension': ('.html', '.txt'), - 'match_regex': '^(?!admin/)', - 'context_processors': [ - 'django.template.context_processors.media', - 'django.template.context_processors.tz', - 'django.template.context_processors.i18n', - 'django.template.context_processors.request', - 'django.contrib.messages.context_processors.messages', - 'judge.template_context.comet_location', - 'judge.template_context.get_resource', - 'judge.template_context.general_info', - 'judge.template_context.site', - 'judge.template_context.site_name', - 'judge.template_context.misc_config', - 'judge.template_context.math_setting', - 'social_django.context_processors.backends', - 'social_django.context_processors.login_redirect', + "APP_DIRS": False, + "OPTIONS": { + "match_extension": (".html", ".txt"), + "match_regex": "^(?!admin/)", + "context_processors": [ + "django.template.context_processors.media", + "django.template.context_processors.tz", + "django.template.context_processors.i18n", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", + "judge.template_context.comet_location", + "judge.template_context.get_resource", + "judge.template_context.general_info", + "judge.template_context.site", + "judge.template_context.site_name", + "judge.template_context.misc_config", + "judge.template_context.math_setting", + "social_django.context_processors.backends", + "social_django.context_processors.login_redirect", ], - 'autoescape': select_autoescape(['html', 'xml']), - 'trim_blocks': True, - 'lstrip_blocks': True, - 'extensions': DEFAULT_EXTENSIONS + [ - 'compressor.contrib.jinja2ext.CompressorExtension', - 'judge.jinja2.DMOJExtension', - 'judge.jinja2.spaceless.SpacelessExtension', + "autoescape": select_autoescape(["html", "xml"]), + "trim_blocks": True, + "lstrip_blocks": True, + "extensions": DEFAULT_EXTENSIONS + + [ + "compressor.contrib.jinja2ext.CompressorExtension", + "judge.jinja2.DMOJExtension", + "judge.jinja2.spaceless.SpacelessExtension", ], }, }, { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'DIRS': [ - os.path.join(BASE_DIR, 'templates'), + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "DIRS": [ + os.path.join(BASE_DIR, "templates"), ], - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.media', - 'django.template.context_processors.tz', - 'django.template.context_processors.i18n', - 'django.template.context_processors.request', - 'django.contrib.messages.context_processors.messages', + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.media", + "django.template.context_processors.tz", + "django.template.context_processors.i18n", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", ], }, }, ] LOCALE_PATHS = [ - os.path.join(BASE_DIR, 'locale'), + os.path.join(BASE_DIR, "locale"), ] LANGUAGES = [ - ('de', _('German')), - ('en', _('English')), - ('es', _('Spanish')), - ('fr', _('French')), - ('hr', _('Croatian')), - ('hu', _('Hungarian')), - ('ja', _('Japanese')), - ('ko', _('Korean')), - ('pt', _('Brazilian Portuguese')), - ('ro', _('Romanian')), - ('ru', _('Russian')), - ('sr-latn', _('Serbian (Latin)')), - ('tr', _('Turkish')), - ('vi', _('Vietnamese')), - ('zh-hans', _('Simplified Chinese')), - ('zh-hant', _('Traditional Chinese')), + ("de", _("German")), + ("en", _("English")), + ("es", _("Spanish")), + ("fr", _("French")), + ("hr", _("Croatian")), + ("hu", _("Hungarian")), + ("ja", _("Japanese")), + ("ko", _("Korean")), + ("pt", _("Brazilian Portuguese")), + ("ro", _("Romanian")), + ("ru", _("Russian")), + ("sr-latn", _("Serbian (Latin)")), + ("tr", _("Turkish")), + ("vi", _("Vietnamese")), + ("zh-hans", _("Simplified Chinese")), + ("zh-hant", _("Traditional Chinese")), ] MARKDOWN_ADMIN_EDITABLE_STYLE = { - 'safe_mode': False, - 'use_camo': True, - 'texoid': True, - 'math': True, + "safe_mode": False, + "use_camo": True, + "texoid": True, + "math": True, } MARKDOWN_DEFAULT_STYLE = { - 'safe_mode': True, - 'nofollow': True, - 'use_camo': True, - 'math': True, + "safe_mode": True, + "nofollow": True, + "use_camo": True, + "math": True, } MARKDOWN_USER_LARGE_STYLE = { - 'safe_mode': True, - 'nofollow': True, - 'use_camo': True, - 'math': True, + "safe_mode": True, + "nofollow": True, + "use_camo": True, + "math": True, } MARKDOWN_STYLES = { - 'comment': MARKDOWN_DEFAULT_STYLE, - 'self-description': MARKDOWN_USER_LARGE_STYLE, - 'problem': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'contest': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'language': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'license': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'judge': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'blog': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'solution': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'contest_tag': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'organization-about': MARKDOWN_USER_LARGE_STYLE, - 'ticket': MARKDOWN_USER_LARGE_STYLE, + "comment": MARKDOWN_DEFAULT_STYLE, + "self-description": MARKDOWN_USER_LARGE_STYLE, + "problem": MARKDOWN_ADMIN_EDITABLE_STYLE, + "contest": MARKDOWN_ADMIN_EDITABLE_STYLE, + "language": MARKDOWN_ADMIN_EDITABLE_STYLE, + "license": MARKDOWN_ADMIN_EDITABLE_STYLE, + "judge": MARKDOWN_ADMIN_EDITABLE_STYLE, + "blog": MARKDOWN_ADMIN_EDITABLE_STYLE, + "solution": MARKDOWN_ADMIN_EDITABLE_STYLE, + "contest_tag": MARKDOWN_ADMIN_EDITABLE_STYLE, + "organization-about": MARKDOWN_USER_LARGE_STYLE, + "ticket": MARKDOWN_USER_LARGE_STYLE, } # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), }, } ENABLE_FTS = False # Bridged configuration -BRIDGED_JUDGE_ADDRESS = [('localhost', 9999)] +BRIDGED_JUDGE_ADDRESS = [("localhost", 9999)] BRIDGED_JUDGE_PROXIES = None -BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)] +BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)] BRIDGED_DJANGO_CONNECT = None # Event Server configuration EVENT_DAEMON_USE = False -EVENT_DAEMON_POST = 'ws://localhost:9997/' -EVENT_DAEMON_GET = 'ws://localhost:9996/' -EVENT_DAEMON_POLL = '/channels/' +EVENT_DAEMON_POST = "ws://localhost:9997/" +EVENT_DAEMON_GET = "ws://localhost:9996/" +EVENT_DAEMON_POLL = "/channels/" EVENT_DAEMON_KEY = None -EVENT_DAEMON_AMQP_EXCHANGE = 'dmoj-events' -EVENT_DAEMON_SUBMISSION_KEY = '6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww' +EVENT_DAEMON_AMQP_EXCHANGE = "dmoj-events" +EVENT_DAEMON_SUBMISSION_KEY = ( + "6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww" +) # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ # Whatever you do, this better be one of the entries in `LANGUAGES`. -LANGUAGE_CODE = 'vi' -TIME_ZONE = 'Asia/Ho_Chi_Minh' -DEFAULT_USER_TIME_ZONE = 'Asia/Ho_Chi_Minh' +LANGUAGE_CODE = "vi" +TIME_ZONE = "Asia/Ho_Chi_Minh" +DEFAULT_USER_TIME_ZONE = "Asia/Ho_Chi_Minh" USE_I18N = True USE_L10N = True USE_TZ = True # Cookies -SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' +SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ -DMOJ_RESOURCES = os.path.join(BASE_DIR, 'resources') +DMOJ_RESOURCES = os.path.join(BASE_DIR, "resources") STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'resources'), + os.path.join(BASE_DIR, "resources"), ] -STATIC_URL = '/static/' +STATIC_URL = "/static/" # Define a cache CACHES = {} # Authentication AUTHENTICATION_BACKENDS = ( - 'social_core.backends.google.GoogleOAuth2', - 'social_core.backends.facebook.FacebookOAuth2', - 'judge.social_auth.GitHubSecureEmailOAuth2', - 'django.contrib.auth.backends.ModelBackend', + "social_core.backends.google.GoogleOAuth2", + "social_core.backends.facebook.FacebookOAuth2", + "judge.social_auth.GitHubSecureEmailOAuth2", + "django.contrib.auth.backends.ModelBackend", ) SOCIAL_AUTH_PIPELINE = ( - 'social_core.pipeline.social_auth.social_details', - 'social_core.pipeline.social_auth.social_uid', - 'social_core.pipeline.social_auth.auth_allowed', - 'judge.social_auth.verify_email', - 'social_core.pipeline.social_auth.social_user', - 'social_core.pipeline.user.get_username', - 'social_core.pipeline.social_auth.associate_by_email', - 'judge.social_auth.choose_username', - 'social_core.pipeline.user.create_user', - 'judge.social_auth.make_profile', - 'social_core.pipeline.social_auth.associate_user', - 'social_core.pipeline.social_auth.load_extra_data', - 'social_core.pipeline.user.user_details', + "social_core.pipeline.social_auth.social_details", + "social_core.pipeline.social_auth.social_uid", + "social_core.pipeline.social_auth.auth_allowed", + "judge.social_auth.verify_email", + "social_core.pipeline.social_auth.social_user", + "social_core.pipeline.user.get_username", + "social_core.pipeline.social_auth.associate_by_email", + "judge.social_auth.choose_username", + "social_core.pipeline.user.create_user", + "judge.social_auth.make_profile", + "social_core.pipeline.social_auth.associate_user", + "social_core.pipeline.social_auth.load_extra_data", + "social_core.pipeline.user.user_details", ) -SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['first_name', 'last_name'] -SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = ['email', 'username'] -SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ['user:email'] -SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] +SOCIAL_AUTH_PROTECTED_USER_FIELDS = ["first_name", "last_name"] +SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = ["email", "username"] +SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ["user:email"] +SOCIAL_AUTH_FACEBOOK_SCOPE = ["email"] SOCIAL_AUTH_SLUGIFY_USERNAMES = True -SOCIAL_AUTH_SLUGIFY_FUNCTION = 'judge.social_auth.slugify_username' +SOCIAL_AUTH_SLUGIFY_FUNCTION = "judge.social_auth.slugify_username" JUDGE_AMQP_PATH = None @@ -529,7 +536,7 @@ NEWSLETTER_BATCH_SIZE = 100 REGISTER_NAME_URL = None try: - with open(os.path.join(os.path.dirname(__file__), 'local_settings.py')) as f: + with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: exec(f.read(), globals()) except IOError: pass diff --git a/dmoj/throttle_mail.py b/dmoj/throttle_mail.py index 7346a21..047e261 100644 --- a/dmoj/throttle_mail.py +++ b/dmoj/throttle_mail.py @@ -8,8 +8,8 @@ DEFAULT_THROTTLE = (10, 60) def new_email(): - cache.add('error_email_throttle', 0, settings.DMOJ_EMAIL_THROTTLING[1]) - return cache.incr('error_email_throttle') + cache.add("error_email_throttle", 0, settings.DMOJ_EMAIL_THROTTLING[1]) + return cache.incr("error_email_throttle") class ThrottledEmailHandler(AdminEmailHandler): diff --git a/dmoj/urls.py b/dmoj/urls.py index b0829d5..fedaace 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -14,429 +14,1076 @@ from django.views.generic import RedirectView from django.contrib.auth.decorators import login_required -from judge.feed import AtomBlogFeed, AtomCommentFeed, AtomProblemFeed, BlogFeed, CommentFeed, ProblemFeed +from judge.feed import ( + AtomBlogFeed, + AtomCommentFeed, + AtomProblemFeed, + BlogFeed, + CommentFeed, + ProblemFeed, +) from judge.forms import CustomAuthenticationForm -from judge.sitemap import BlogPostSitemap, ContestSitemap, HomePageSitemap, OrganizationSitemap, ProblemSitemap, \ - SolutionSitemap, UrlSitemap, UserSitemap -from judge.views import TitledTemplateView, about, api, blog, comment, contests, language, license, mailgun, \ - notification, organization, preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tasks, \ - ticket, totp, user, volunteer, widgets, internal -from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ - problem_data_file, problem_init_view, ProblemZipUploadView +from judge.sitemap import ( + BlogPostSitemap, + ContestSitemap, + HomePageSitemap, + OrganizationSitemap, + ProblemSitemap, + SolutionSitemap, + UrlSitemap, + UserSitemap, +) +from judge.views import ( + TitledTemplateView, + about, + api, + blog, + comment, + contests, + language, + license, + mailgun, + notification, + organization, + preview, + problem, + problem_manage, + ranked_submission, + register, + stats, + status, + submission, + tasks, + ticket, + totp, + user, + volunteer, + widgets, + internal, +) +from judge.views.problem_data import ( + ProblemDataView, + ProblemSubmissionDiff, + problem_data_file, + problem_init_view, + ProblemZipUploadView, +) from judge.views.register import ActivationView, RegistrationView -from judge.views.select2 import AssigneeSelect2View, ChatUserSearchSelect2View, CommentSelect2View, \ - ContestSelect2View, ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \ - UserSearchSelect2View, UserSelect2View +from judge.views.select2 import ( + AssigneeSelect2View, + ChatUserSearchSelect2View, + CommentSelect2View, + ContestSelect2View, + ContestUserSearchSelect2View, + OrganizationSelect2View, + ProblemSelect2View, + TicketUserSelect2View, + UserSearchSelect2View, + UserSelect2View, +) admin.autodiscover() register_patterns = [ - url(r'^activate/complete/$', - TitledTemplateView.as_view(template_name='registration/activation_complete.html', - title='Activation Successful!'), - name='registration_activation_complete'), + url( + r"^activate/complete/$", + TitledTemplateView.as_view( + template_name="registration/activation_complete.html", + title="Activation Successful!", + ), + name="registration_activation_complete", + ), # Activation keys get matched by \w+ instead of the more specific # [a-fA-F0-9]{40} because a bad activation key should still get to the view; # that way it can return a sensible "invalid key" message instead of a # confusing 404. - url(r'^activate/(?P\w+)/$', - ActivationView.as_view(title='Activation key invalid'), - name='registration_activate'), - url(r'^register/$', - RegistrationView.as_view(title='Register'), - name='registration_register'), - url(r'^register/complete/$', - TitledTemplateView.as_view(template_name='registration/registration_complete.html', - title='Registration Completed'), - name='registration_complete'), - url(r'^register/closed/$', - TitledTemplateView.as_view(template_name='registration/registration_closed.html', - title='Registration not allowed'), - name='registration_disallowed'), - url(r'^login/$', auth_views.LoginView.as_view( - template_name='registration/login.html', - extra_context={'title': _('Login')}, - authentication_form=CustomAuthenticationForm, - redirect_authenticated_user=True, - ), name='auth_login'), - url(r'^logout/$', user.UserLogoutView.as_view(), name='auth_logout'), - url(r'^password/change/$', auth_views.PasswordChangeView.as_view( - template_name='registration/password_change_form.html', - ), name='password_change'), - url(r'^password/change/done/$', auth_views.PasswordChangeDoneView.as_view( - template_name='registration/password_change_done.html', - ), name='password_change_done'), - url(r'^password/reset/$', auth_views.PasswordResetView.as_view( - template_name='registration/password_reset.html', - html_email_template_name='registration/password_reset_email.html', - email_template_name='registration/password_reset_email.txt', - ), name='password_reset'), - url(r'^password/reset/confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', + url( + r"^activate/(?P\w+)/$", + ActivationView.as_view(title="Activation key invalid"), + name="registration_activate", + ), + url( + r"^register/$", + RegistrationView.as_view(title="Register"), + name="registration_register", + ), + url( + r"^register/complete/$", + TitledTemplateView.as_view( + template_name="registration/registration_complete.html", + title="Registration Completed", + ), + name="registration_complete", + ), + url( + r"^register/closed/$", + TitledTemplateView.as_view( + template_name="registration/registration_closed.html", + title="Registration not allowed", + ), + name="registration_disallowed", + ), + url( + r"^login/$", + auth_views.LoginView.as_view( + template_name="registration/login.html", + extra_context={"title": _("Login")}, + authentication_form=CustomAuthenticationForm, + redirect_authenticated_user=True, + ), + name="auth_login", + ), + url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"), + url( + r"^password/change/$", + auth_views.PasswordChangeView.as_view( + template_name="registration/password_change_form.html", + ), + name="password_change", + ), + url( + r"^password/change/done/$", + auth_views.PasswordChangeDoneView.as_view( + template_name="registration/password_change_done.html", + ), + name="password_change_done", + ), + url( + r"^password/reset/$", + auth_views.PasswordResetView.as_view( + template_name="registration/password_reset.html", + html_email_template_name="registration/password_reset_email.html", + email_template_name="registration/password_reset_email.txt", + ), + name="password_reset", + ), + url( + r"^password/reset/confirm/(?P[0-9A-Za-z]+)-(?P.+)/$", auth_views.PasswordResetConfirmView.as_view( - template_name='registration/password_reset_confirm.html', - ), name='password_reset_confirm'), - url(r'^password/reset/complete/$', auth_views.PasswordResetCompleteView.as_view( - template_name='registration/password_reset_complete.html', - ), name='password_reset_complete'), - url(r'^password/reset/done/$', auth_views.PasswordResetDoneView.as_view( - template_name='registration/password_reset_done.html', - ), name='password_reset_done'), - url(r'^social/error/$', register.social_auth_error, name='social_auth_error'), - - url(r'^2fa/$', totp.TOTPLoginView.as_view(), name='login_2fa'), - url(r'^2fa/enable/$', totp.TOTPEnableView.as_view(), name='enable_2fa'), - url(r'^2fa/disable/$', totp.TOTPDisableView.as_view(), name='disable_2fa'), + template_name="registration/password_reset_confirm.html", + ), + name="password_reset_confirm", + ), + url( + r"^password/reset/complete/$", + auth_views.PasswordResetCompleteView.as_view( + template_name="registration/password_reset_complete.html", + ), + name="password_reset_complete", + ), + url( + r"^password/reset/done/$", + auth_views.PasswordResetDoneView.as_view( + template_name="registration/password_reset_done.html", + ), + name="password_reset_done", + ), + url(r"^social/error/$", register.social_auth_error, name="social_auth_error"), + url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"), + url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"), + url(r"^2fa/disable/$", totp.TOTPDisableView.as_view(), name="disable_2fa"), ] def exception(request): if not request.user.is_superuser: raise Http404() - raise RuntimeError('@Xyene asked me to cause this') + raise RuntimeError("@Xyene asked me to cause this") def paged_list_view(view, name, **kwargs): - return include([ - url(r'^$', view.as_view(**kwargs), name=name), - url(r'^(?P\d+)$', view.as_view(**kwargs), name=name), - ]) + return include( + [ + url(r"^$", view.as_view(**kwargs), name=name), + url(r"^(?P\d+)$", view.as_view(**kwargs), name=name), + ] + ) urlpatterns = [ - url(r'^$', blog.PostList.as_view(template_name='home.html', title=_('Home')), kwargs={'page': 1}, name='home'), - url(r'^500/$', exception), - url(r'^admin/', admin.site.urls), - url(r'^i18n/', include('django.conf.urls.i18n')), - url(r'^accounts/', include(register_patterns)), - url(r'^', include('social_django.urls')), - - url(r'^feed/', include([ - url(r'^tickets/$', blog.TicketFeed.as_view(), name='ticket_feed'), - url(r'^comments/$', blog.CommentFeed.as_view(), name='comment_feed'), - ])), - url(r'^problems/', paged_list_view(problem.ProblemList, 'problem_list')), - url(r'^problems/random/$', problem.RandomProblem.as_view(), name='problem_random'), - url(r'^problems/feed/', paged_list_view(problem.ProblemFeed, 'problem_feed', feed_type='for_you')), - url(r'^problems/feed/new/', paged_list_view(problem.ProblemFeed, 'problem_feed_new', feed_type='new')), - url(r'^problems/feed/volunteer/', paged_list_view(problem.ProblemFeed, 'problem_feed_volunteer', feed_type='volunteer')), - - url(r'^problem/(?P[^/]+)', include([ - url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'), - url(r'^/editorial$', problem.ProblemSolution.as_view(), name='problem_editorial'), - url(r'^/raw$', problem.ProblemRaw.as_view(), name='problem_raw'), - url(r'^/pdf$', problem.ProblemPdfView.as_view(), name='problem_pdf'), - url(r'^/pdf/(?P[a-z-]+)$', problem.ProblemPdfView.as_view(), name='problem_pdf'), - url(r'^/clone', problem.ProblemClone.as_view(), name='problem_clone'), - url(r'^/submit$', problem.problem_submit, name='problem_submit'), - url(r'^/resubmit/(?P\d+)$', problem.problem_submit, name='problem_submit'), - - url(r'^/rank/', paged_list_view(ranked_submission.RankedSubmissions, 'ranked_submissions')), - url(r'^/submissions/', paged_list_view(submission.ProblemSubmissions, 'chronological_submissions')), - url(r'^/submissions/(?P\w+)/', paged_list_view(submission.UserProblemSubmissions, 'user_submissions')), - - url(r'^/$', lambda _, problem: HttpResponsePermanentRedirect(reverse('problem_detail', args=[problem]))), - - url(r'^/test_data$', ProblemDataView.as_view(), name='problem_data'), - url(r'^/test_data/init$', problem_init_view, name='problem_data_init'), - url(r'^/test_data/diff$', ProblemSubmissionDiff.as_view(), name='problem_submission_diff'), - url(r'^/test_data/upload$', ProblemZipUploadView.as_view(), name='problem_zip_upload'), - url(r'^/data/(?P.+)$', problem_data_file, name='problem_data_file'), - - url(r'^/tickets$', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'), - url(r'^/tickets/new$', ticket.NewProblemTicketView.as_view(), name='new_problem_ticket'), - - url(r'^/vote$', problem.Vote.as_view(), name='vote'), - - url(r'^/manage/submission', include([ - url('^$', problem_manage.ManageProblemSubmissionView.as_view(), name='problem_manage_submissions'), - url('^/action$', problem_manage.ActionSubmissionsView.as_view(), name='problem_submissions_action'), - url('^/action/preview$', problem_manage.PreviewActionSubmissionsView.as_view(), - name='problem_submissions_rejudge_preview'), - url('^/rejudge/success/(?P[A-Za-z0-9-]*)$', problem_manage.rejudge_success, - name='problem_submissions_rejudge_success'), - url('^/rescore/all$', problem_manage.RescoreAllSubmissionsView.as_view(), - name='problem_submissions_rescore_all'), - url('^/rescore/success/(?P[A-Za-z0-9-]*)$', problem_manage.rescore_success, - name='problem_submissions_rescore_success'), - ])), - ])), - - url(r'^submissions/', paged_list_view(submission.AllSubmissions, 'all_submissions')), - url(r'^submissions/user/(?P\w+)/', paged_list_view(submission.AllUserSubmissions, 'all_user_submissions')), - - url(r'^src/(?P\d+)$', submission.SubmissionSource.as_view(), name='submission_source'), - url(r'^src/(?P\d+)/raw$', submission.SubmissionSourceRaw.as_view(), name='submission_source_raw'), - - url(r'^submission/(?P\d+)', include([ - url(r'^$', submission.SubmissionStatus.as_view(), name='submission_status'), - url(r'^/abort$', submission.abort_submission, name='submission_abort'), - url(r'^/html$', submission.single_submission), - ])), - - url(r'^users/', include([ - url(r'^$', user.users, name='user_list'), - url(r'^(?P\d+)$', lambda request, page: - HttpResponsePermanentRedirect('%s?page=%s' % (reverse('user_list'), page))), - url(r'^find$', user.user_ranking_redirect, name='user_ranking_redirect'), - ])), - - url(r'^user$', user.UserAboutPage.as_view(), name='user_page'), - url(r'^edit/profile/$', user.edit_profile, name='user_edit_profile'), - url(r'^user/(?P\w+)', include([ - url(r'^$', user.UserAboutPage.as_view(), name='user_page'), - url(r'^/solved', include([ - url(r'^$', user.UserProblemsPage.as_view(), name='user_problems'), - url(r'/ajax$', user.UserPerformancePointsAjax.as_view(), name='user_pp_ajax'), - ])), - url(r'^/submissions/', paged_list_view(submission.AllUserSubmissions, 'all_user_submissions_old')), - url(r'^/submissions/', lambda _, user: - HttpResponsePermanentRedirect(reverse('all_user_submissions', args=[user]))), - - url(r'^/$', lambda _, user: HttpResponsePermanentRedirect(reverse('user_page', args=[user]))), - ])), - - url(r'^comments/upvote/$', comment.upvote_comment, name='comment_upvote'), - url(r'^comments/downvote/$', comment.downvote_comment, name='comment_downvote'), - url(r'^comments/hide/$', comment.comment_hide, name='comment_hide'), - url(r'^comments/(?P\d+)/', include([ - url(r'^edit$', comment.CommentEdit.as_view(), name='comment_edit'), - url(r'^history/ajax$', comment.CommentRevisionAjax.as_view(), name='comment_revision_ajax'), - url(r'^edit/ajax$', comment.CommentEditAjax.as_view(), name='comment_edit_ajax'), - url(r'^votes/ajax$', comment.CommentVotesAjax.as_view(), name='comment_votes_ajax'), - url(r'^render$', comment.CommentContent.as_view(), name='comment_content'), - ])), - - url(r'^contests/', paged_list_view(contests.ContestList, 'contest_list')), - url(r'^contests/(?P\d+)/(?P\d+)/$', contests.ContestCalendar.as_view(), name='contest_calendar'), - url(r'^contests/tag/(?P[a-z-]+)', include([ - url(r'^$', contests.ContestTagDetail.as_view(), name='contest_tag'), - url(r'^/ajax$', contests.ContestTagDetailAjax.as_view(), name='contest_tag_ajax'), - ])), - - url(r'^contest/(?P\w+)', include([ - url(r'^$', contests.ContestDetail.as_view(), name='contest_view'), - url(r'^/moss$', contests.ContestMossView.as_view(), name='contest_moss'), - url(r'^/moss/delete$', contests.ContestMossDelete.as_view(), name='contest_moss_delete'), - url(r'^/clone$', contests.ContestClone.as_view(), name='contest_clone'), - url(r'^/ranking/$', contests.ContestRanking.as_view(), name='contest_ranking'), - url(r'^/ranking/ajax$', contests.contest_ranking_ajax, name='contest_ranking_ajax'), - url(r'^/join$', contests.ContestJoin.as_view(), name='contest_join'), - url(r'^/leave$', contests.ContestLeave.as_view(), name='contest_leave'), - url(r'^/stats$', contests.ContestStats.as_view(), name='contest_stats'), - - url(r'^/rank/(?P\w+)/', - paged_list_view(ranked_submission.ContestRankedSubmission, 'contest_ranked_submissions')), - - url(r'^/submissions/(?P\w+)/(?P\w+)/', - paged_list_view(submission.UserContestSubmissions, 'contest_user_submissions')), - - url(r'^/participations$', contests.ContestParticipationList.as_view(), name='contest_participation_own'), - url(r'^/participations/(?P\w+)$', - contests.ContestParticipationList.as_view(), name='contest_participation'), - url(r'^/participation/disqualify$', contests.ContestParticipationDisqualify.as_view(), - name='contest_participation_disqualify'), - - url(r'^/clarification$', contests.NewContestClarificationView.as_view(), name='new_contest_clarification'), - url(r'^/clarification/ajax$', contests.ContestClarificationAjax.as_view(), name='contest_clarification_ajax'), - - url(r'^/$', lambda _, contest: HttpResponsePermanentRedirect(reverse('contest_view', args=[contest]))), - ])), - - url(r'^organizations/$', organization.OrganizationList.as_view(), name='organization_list'), - url(r'^organization/(?P\d+)-(?P[\w-]*)', include([ - url(r'^$', organization.OrganizationHome.as_view(), name='organization_home'), - url(r'^/users$', organization.OrganizationUsers.as_view(), name='organization_users'), - url(r'^/join$', organization.JoinOrganization.as_view(), name='join_organization'), - url(r'^/leave$', organization.LeaveOrganization.as_view(), name='leave_organization'), - url(r'^/edit$', organization.EditOrganization.as_view(), name='edit_organization'), - url(r'^/kick$', organization.KickUserWidgetView.as_view(), name='organization_user_kick'), - - url(r'^/request$', organization.RequestJoinOrganization.as_view(), name='request_organization'), - url(r'^/request/(?P\d+)$', organization.OrganizationRequestDetail.as_view(), - name='request_organization_detail'), - url(r'^/requests/', include([ - url(r'^pending$', organization.OrganizationRequestView.as_view(), name='organization_requests_pending'), - url(r'^log$', organization.OrganizationRequestLog.as_view(), name='organization_requests_log'), - url(r'^approved$', organization.OrganizationRequestLog.as_view(states=('A',), tab='approved'), - name='organization_requests_approved'), - url(r'^rejected$', organization.OrganizationRequestLog.as_view(states=('R',), tab='rejected'), - name='organization_requests_rejected'), - ])), - - url(r'^/$', lambda _, pk, slug: HttpResponsePermanentRedirect(reverse('organization_home', args=[pk, slug]))), - ])), - - url(r'^runtimes/$', language.LanguageList.as_view(), name='runtime_list'), - url(r'^runtimes/matrix/$', status.version_matrix, name='version_matrix'), - url(r'^status/$', status.status_all, name='status_all'), - - url(r'^api/', include([ - url(r'^contest/list$', api.api_v1_contest_list), - url(r'^contest/info/(\w+)$', api.api_v1_contest_detail), - url(r'^problem/list$', api.api_v1_problem_list), - url(r'^problem/info/(\w+)$', api.api_v1_problem_info), - url(r'^user/list$', api.api_v1_user_list), - url(r'^user/info/(\w+)$', api.api_v1_user_info), - url(r'^user/submissions/(\w+)$', api.api_v1_user_submissions), - ])), - - url(r'^blog/', paged_list_view(blog.PostList, 'blog_post_list')), - url(r'^post/(?P\d+)-(?P.*)$', blog.PostView.as_view(), name='blog_post'), - - url(r'^license/(?P[-\w.]+)$', license.LicenseDetail.as_view(), name='license'), - - url(r'^mailgun/mail_activate/$', mailgun.MailgunActivationView.as_view(), name='mailgun_activate'), - - url(r'^widgets/', include([ - url(r'^contest_mode$', contests.update_contest_mode, name='contest_mode_ajax'), - url(r'^rejudge$', widgets.rejudge_submission, name='submission_rejudge'), - url(r'^single_submission$', submission.single_submission_query, name='submission_single_query'), - url(r'^submission_testcases$', submission.SubmissionTestCaseQuery.as_view(), name='submission_testcases_query'), - url(r'^detect_timezone$', widgets.DetectTimezone.as_view(), name='detect_timezone'), - url(r'^status-table$', status.status_table, name='status_table'), - - url(r'^template$', problem.LanguageTemplateAjax.as_view(), name='language_template_ajax'), - - url(r'^select2/', include([ - url(r'^user_search$', UserSearchSelect2View.as_view(), name='user_search_select2_ajax'), - url(r'^user_search_chat$', ChatUserSearchSelect2View.as_view(), name='chat_user_search_select2_ajax'), - url(r'^contest_users/(?P\w+)$', ContestUserSearchSelect2View.as_view(), - name='contest_user_search_select2_ajax'), - url(r'^ticket_user$', TicketUserSelect2View.as_view(), name='ticket_user_select2_ajax'), - url(r'^ticket_assignee$', AssigneeSelect2View.as_view(), name='ticket_assignee_select2_ajax'), - ])), - - url(r'^preview/', include([ - url(r'^problem$', preview.ProblemMarkdownPreviewView.as_view(), name='problem_preview'), - url(r'^blog$', preview.BlogMarkdownPreviewView.as_view(), name='blog_preview'), - url(r'^contest$', preview.ContestMarkdownPreviewView.as_view(), name='contest_preview'), - url(r'^comment$', preview.CommentMarkdownPreviewView.as_view(), name='comment_preview'), - url(r'^profile$', preview.ProfileMarkdownPreviewView.as_view(), name='profile_preview'), - url(r'^organization$', preview.OrganizationMarkdownPreviewView.as_view(), name='organization_preview'), - url(r'^solution$', preview.SolutionMarkdownPreviewView.as_view(), name='solution_preview'), - url(r'^license$', preview.LicenseMarkdownPreviewView.as_view(), name='license_preview'), - url(r'^ticket$', preview.TicketMarkdownPreviewView.as_view(), name='ticket_preview'), - ])), - ])), - - url(r'^feed/', include([ - url(r'^problems/rss/$', ProblemFeed(), name='problem_rss'), - url(r'^problems/atom/$', AtomProblemFeed(), name='problem_atom'), - url(r'^comment/rss/$', CommentFeed(), name='comment_rss'), - url(r'^comment/atom/$', AtomCommentFeed(), name='comment_atom'), - url(r'^blog/rss/$', BlogFeed(), name='blog_rss'), - url(r'^blog/atom/$', AtomBlogFeed(), name='blog_atom'), - ])), - - url(r'^stats/', include([ - url('^language/', include([ - url('^$', stats.language, name='language_stats'), - url('^data/all/$', stats.language_data, name='language_stats_data_all'), - url('^data/ac/$', stats.ac_language_data, name='language_stats_data_ac'), - url('^data/status/$', stats.status_data, name='stats_data_status'), - url('^data/ac_rate/$', stats.ac_rate, name='language_stats_data_ac_rate'), - ])), - ])), - - url(r'^tickets/', include([ - url(r'^$', ticket.TicketList.as_view(), name='ticket_list'), - url(r'^ajax$', ticket.TicketListDataAjax.as_view(), name='ticket_ajax'), - ])), - - url(r'^ticket/(?P\d+)', include([ - url(r'^$', ticket.TicketView.as_view(), name='ticket'), - url(r'^/ajax$', ticket.TicketMessageDataAjax.as_view(), name='ticket_message_ajax'), - url(r'^/open$', ticket.TicketStatusChangeView.as_view(open=True), name='ticket_open'), - url(r'^/close$', ticket.TicketStatusChangeView.as_view(open=False), name='ticket_close'), - url(r'^/notes$', ticket.TicketNotesEditView.as_view(), name='ticket_notes'), - ])), - - url(r'^sitemap\.xml$', sitemap, {'sitemaps': { - 'problem': ProblemSitemap, - 'user': UserSitemap, - 'home': HomePageSitemap, - 'contest': ContestSitemap, - 'organization': OrganizationSitemap, - 'blog': BlogPostSitemap, - 'solutions': SolutionSitemap, - 'pages': UrlSitemap([ - {'location': '/about/', 'priority': 0.9}, - ]), - }}), - - url(r'^judge-select2/', include([ - url(r'^profile/$', UserSelect2View.as_view(), name='profile_select2'), - url(r'^organization/$', OrganizationSelect2View.as_view(), name='organization_select2'), - url(r'^problem/$', ProblemSelect2View.as_view(), name='problem_select2'), - url(r'^contest/$', ContestSelect2View.as_view(), name='contest_select2'), - url(r'^comment/$', CommentSelect2View.as_view(), name='comment_select2'), - ])), - - url(r'^tasks/', include([ - url(r'^status/(?P[A-Za-z0-9-]*)$', tasks.task_status, name='task_status'), - url(r'^ajax_status$', tasks.task_status_ajax, name='task_status_ajax'), - url(r'^success$', tasks.demo_success), - url(r'^failure$', tasks.demo_failure), - url(r'^progress$', tasks.demo_progress), - ])), - - url(r'^about/', about.about, name='about'), - - url(r'^custom_checker_sample/', about.custom_checker_sample, name='custom_checker_sample'), - - url(r'^chat/', include([ - url(r'^(?P\d*)$', login_required(chat.ChatView.as_view()), name='chat'), - url(r'^delete/$', chat.delete_message, name='delete_chat_message'), - url(r'^post/$', chat.post_message, name='post_chat_message'), - url(r'^ajax$', chat.chat_message_ajax, name='chat_message_ajax'), - url(r'^online_status/ajax$', chat.online_status_ajax, name='online_status_ajax'), - url(r'^get_or_create_room$', chat.get_or_create_room, name='get_or_create_room'), - url(r'^update_last_seen$', chat.update_last_seen, name='update_last_seen'), - url(r'^online_status/user/ajax$', chat.user_online_status_ajax, name='user_online_status_ajax'), - url(r'^toggle_ignore/(?P\d+)$', chat.toggle_ignore, name='toggle_ignore'), - url(r'^get_unread_boxes$', chat.get_unread_boxes, name='get_unread_boxes'), - ])), - - url(r'^internal/', include([ - url(r'^problem$', internal.InternalProblem.as_view(), name='internal_problem'), - ])), - - url(r'^notifications/', + url( + r"^$", + blog.PostList.as_view(template_name="home.html", title=_("Home")), + kwargs={"page": 1}, + name="home", + ), + url(r"^500/$", exception), + url(r"^admin/", admin.site.urls), + url(r"^i18n/", include("django.conf.urls.i18n")), + url(r"^accounts/", include(register_patterns)), + url(r"^", include("social_django.urls")), + url( + r"^feed/", + include( + [ + url(r"^tickets/$", blog.TicketFeed.as_view(), name="ticket_feed"), + url(r"^comments/$", blog.CommentFeed.as_view(), name="comment_feed"), + ] + ), + ), + url(r"^problems/", paged_list_view(problem.ProblemList, "problem_list")), + url(r"^problems/random/$", problem.RandomProblem.as_view(), name="problem_random"), + url( + r"^problems/feed/", + paged_list_view(problem.ProblemFeed, "problem_feed", feed_type="for_you"), + ), + url( + r"^problems/feed/new/", + paged_list_view(problem.ProblemFeed, "problem_feed_new", feed_type="new"), + ), + url( + r"^problems/feed/volunteer/", + paged_list_view( + problem.ProblemFeed, "problem_feed_volunteer", feed_type="volunteer" + ), + ), + url( + r"^problem/(?P[^/]+)", + include( + [ + url(r"^$", problem.ProblemDetail.as_view(), name="problem_detail"), + url( + r"^/editorial$", + problem.ProblemSolution.as_view(), + name="problem_editorial", + ), + url(r"^/raw$", problem.ProblemRaw.as_view(), name="problem_raw"), + url(r"^/pdf$", problem.ProblemPdfView.as_view(), name="problem_pdf"), + url( + r"^/pdf/(?P[a-z-]+)$", + problem.ProblemPdfView.as_view(), + name="problem_pdf", + ), + url(r"^/clone", problem.ProblemClone.as_view(), name="problem_clone"), + url(r"^/submit$", problem.problem_submit, name="problem_submit"), + url( + r"^/resubmit/(?P\d+)$", + problem.problem_submit, + name="problem_submit", + ), + url( + r"^/rank/", + paged_list_view( + ranked_submission.RankedSubmissions, "ranked_submissions" + ), + ), + url( + r"^/submissions/", + paged_list_view( + submission.ProblemSubmissions, "chronological_submissions" + ), + ), + url( + r"^/submissions/(?P\w+)/", + paged_list_view( + submission.UserProblemSubmissions, "user_submissions" + ), + ), + url( + r"^/$", + lambda _, problem: HttpResponsePermanentRedirect( + reverse("problem_detail", args=[problem]) + ), + ), + url(r"^/test_data$", ProblemDataView.as_view(), name="problem_data"), + url(r"^/test_data/init$", problem_init_view, name="problem_data_init"), + url( + r"^/test_data/diff$", + ProblemSubmissionDiff.as_view(), + name="problem_submission_diff", + ), + url( + r"^/test_data/upload$", + ProblemZipUploadView.as_view(), + name="problem_zip_upload", + ), + url( + r"^/data/(?P.+)$", problem_data_file, name="problem_data_file" + ), + url( + r"^/tickets$", + ticket.ProblemTicketListView.as_view(), + name="problem_ticket_list", + ), + url( + r"^/tickets/new$", + ticket.NewProblemTicketView.as_view(), + name="new_problem_ticket", + ), + url(r"^/vote$", problem.Vote.as_view(), name="vote"), + url( + r"^/manage/submission", + include( + [ + url( + "^$", + problem_manage.ManageProblemSubmissionView.as_view(), + name="problem_manage_submissions", + ), + url( + "^/action$", + problem_manage.ActionSubmissionsView.as_view(), + name="problem_submissions_action", + ), + url( + "^/action/preview$", + problem_manage.PreviewActionSubmissionsView.as_view(), + name="problem_submissions_rejudge_preview", + ), + url( + "^/rejudge/success/(?P[A-Za-z0-9-]*)$", + problem_manage.rejudge_success, + name="problem_submissions_rejudge_success", + ), + url( + "^/rescore/all$", + problem_manage.RescoreAllSubmissionsView.as_view(), + name="problem_submissions_rescore_all", + ), + url( + "^/rescore/success/(?P[A-Za-z0-9-]*)$", + problem_manage.rescore_success, + name="problem_submissions_rescore_success", + ), + ] + ), + ), + ] + ), + ), + url( + r"^submissions/", paged_list_view(submission.AllSubmissions, "all_submissions") + ), + url( + r"^submissions/user/(?P\w+)/", + paged_list_view(submission.AllUserSubmissions, "all_user_submissions"), + ), + url( + r"^src/(?P\d+)$", + submission.SubmissionSource.as_view(), + name="submission_source", + ), + url( + r"^src/(?P\d+)/raw$", + submission.SubmissionSourceRaw.as_view(), + name="submission_source_raw", + ), + url( + r"^submission/(?P\d+)", + include( + [ + url( + r"^$", + submission.SubmissionStatus.as_view(), + name="submission_status", + ), + url(r"^/abort$", submission.abort_submission, name="submission_abort"), + url(r"^/html$", submission.single_submission), + ] + ), + ), + url( + r"^users/", + include( + [ + url(r"^$", user.users, name="user_list"), + url( + r"^(?P\d+)$", + lambda request, page: HttpResponsePermanentRedirect( + "%s?page=%s" % (reverse("user_list"), page) + ), + ), + url( + r"^find$", user.user_ranking_redirect, name="user_ranking_redirect" + ), + ] + ), + ), + url(r"^user$", user.UserAboutPage.as_view(), name="user_page"), + url(r"^edit/profile/$", user.edit_profile, name="user_edit_profile"), + url( + r"^user/(?P\w+)", + include( + [ + url(r"^$", user.UserAboutPage.as_view(), name="user_page"), + url( + r"^/solved", + include( + [ + url( + r"^$", + user.UserProblemsPage.as_view(), + name="user_problems", + ), + url( + r"/ajax$", + user.UserPerformancePointsAjax.as_view(), + name="user_pp_ajax", + ), + ] + ), + ), + url( + r"^/submissions/", + paged_list_view( + submission.AllUserSubmissions, "all_user_submissions_old" + ), + ), + url( + r"^/submissions/", + lambda _, user: HttpResponsePermanentRedirect( + reverse("all_user_submissions", args=[user]) + ), + ), + url( + r"^/$", + lambda _, user: HttpResponsePermanentRedirect( + reverse("user_page", args=[user]) + ), + ), + ] + ), + ), + url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"), + url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"), + url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"), + url( + r"^comments/(?P\d+)/", + include( + [ + url(r"^edit$", comment.CommentEdit.as_view(), name="comment_edit"), + url( + r"^history/ajax$", + comment.CommentRevisionAjax.as_view(), + name="comment_revision_ajax", + ), + url( + r"^edit/ajax$", + comment.CommentEditAjax.as_view(), + name="comment_edit_ajax", + ), + url( + r"^votes/ajax$", + comment.CommentVotesAjax.as_view(), + name="comment_votes_ajax", + ), + url( + r"^render$", + comment.CommentContent.as_view(), + name="comment_content", + ), + ] + ), + ), + url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")), + url( + r"^contests/(?P\d+)/(?P\d+)/$", + contests.ContestCalendar.as_view(), + name="contest_calendar", + ), + url( + r"^contests/tag/(?P[a-z-]+)", + include( + [ + url(r"^$", contests.ContestTagDetail.as_view(), name="contest_tag"), + url( + r"^/ajax$", + contests.ContestTagDetailAjax.as_view(), + name="contest_tag_ajax", + ), + ] + ), + ), + url( + r"^contest/(?P\w+)", + include( + [ + url(r"^$", contests.ContestDetail.as_view(), name="contest_view"), + url( + r"^/moss$", contests.ContestMossView.as_view(), name="contest_moss" + ), + url( + r"^/moss/delete$", + contests.ContestMossDelete.as_view(), + name="contest_moss_delete", + ), + url(r"^/clone$", contests.ContestClone.as_view(), name="contest_clone"), + url( + r"^/ranking/$", + contests.ContestRanking.as_view(), + name="contest_ranking", + ), + url( + r"^/ranking/ajax$", + contests.contest_ranking_ajax, + name="contest_ranking_ajax", + ), + url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"), + url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"), + url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"), + url( + r"^/rank/(?P\w+)/", + paged_list_view( + ranked_submission.ContestRankedSubmission, + "contest_ranked_submissions", + ), + ), + url( + r"^/submissions/(?P\w+)/(?P\w+)/", + paged_list_view( + submission.UserContestSubmissions, "contest_user_submissions" + ), + ), + url( + r"^/participations$", + contests.ContestParticipationList.as_view(), + name="contest_participation_own", + ), + url( + r"^/participations/(?P\w+)$", + contests.ContestParticipationList.as_view(), + name="contest_participation", + ), + url( + r"^/participation/disqualify$", + contests.ContestParticipationDisqualify.as_view(), + name="contest_participation_disqualify", + ), + url( + r"^/clarification$", + contests.NewContestClarificationView.as_view(), + name="new_contest_clarification", + ), + url( + r"^/clarification/ajax$", + contests.ContestClarificationAjax.as_view(), + name="contest_clarification_ajax", + ), + url( + r"^/$", + lambda _, contest: HttpResponsePermanentRedirect( + reverse("contest_view", args=[contest]) + ), + ), + ] + ), + ), + url( + r"^organizations/$", + organization.OrganizationList.as_view(), + name="organization_list", + ), + url( + r"^organization/(?P\d+)-(?P[\w-]*)", + include( + [ + url( + r"^$", + organization.OrganizationHome.as_view(), + name="organization_home", + ), + url( + r"^/users$", + organization.OrganizationUsers.as_view(), + name="organization_users", + ), + url( + r"^/join$", + organization.JoinOrganization.as_view(), + name="join_organization", + ), + url( + r"^/leave$", + organization.LeaveOrganization.as_view(), + name="leave_organization", + ), + url( + r"^/edit$", + organization.EditOrganization.as_view(), + name="edit_organization", + ), + url( + r"^/kick$", + organization.KickUserWidgetView.as_view(), + name="organization_user_kick", + ), + url( + r"^/request$", + organization.RequestJoinOrganization.as_view(), + name="request_organization", + ), + url( + r"^/request/(?P\d+)$", + organization.OrganizationRequestDetail.as_view(), + name="request_organization_detail", + ), + url( + r"^/requests/", + include( + [ + url( + r"^pending$", + organization.OrganizationRequestView.as_view(), + name="organization_requests_pending", + ), + url( + r"^log$", + organization.OrganizationRequestLog.as_view(), + name="organization_requests_log", + ), + url( + r"^approved$", + organization.OrganizationRequestLog.as_view( + states=("A",), tab="approved" + ), + name="organization_requests_approved", + ), + url( + r"^rejected$", + organization.OrganizationRequestLog.as_view( + states=("R",), tab="rejected" + ), + name="organization_requests_rejected", + ), + ] + ), + ), + url( + r"^/$", + lambda _, pk, slug: HttpResponsePermanentRedirect( + reverse("organization_home", args=[pk, slug]) + ), + ), + ] + ), + ), + url(r"^runtimes/$", language.LanguageList.as_view(), name="runtime_list"), + url(r"^runtimes/matrix/$", status.version_matrix, name="version_matrix"), + url(r"^status/$", status.status_all, name="status_all"), + url( + r"^api/", + include( + [ + url(r"^contest/list$", api.api_v1_contest_list), + url(r"^contest/info/(\w+)$", api.api_v1_contest_detail), + url(r"^problem/list$", api.api_v1_problem_list), + url(r"^problem/info/(\w+)$", api.api_v1_problem_info), + url(r"^user/list$", api.api_v1_user_list), + url(r"^user/info/(\w+)$", api.api_v1_user_info), + url(r"^user/submissions/(\w+)$", api.api_v1_user_submissions), + ] + ), + ), + url(r"^blog/", paged_list_view(blog.PostList, "blog_post_list")), + url(r"^post/(?P\d+)-(?P.*)$", blog.PostView.as_view(), name="blog_post"), + url(r"^license/(?P[-\w.]+)$", license.LicenseDetail.as_view(), name="license"), + url( + r"^mailgun/mail_activate/$", + mailgun.MailgunActivationView.as_view(), + name="mailgun_activate", + ), + url( + r"^widgets/", + include( + [ + url( + r"^contest_mode$", + contests.update_contest_mode, + name="contest_mode_ajax", + ), + url( + r"^rejudge$", widgets.rejudge_submission, name="submission_rejudge" + ), + url( + r"^single_submission$", + submission.single_submission_query, + name="submission_single_query", + ), + url( + r"^submission_testcases$", + submission.SubmissionTestCaseQuery.as_view(), + name="submission_testcases_query", + ), + url( + r"^detect_timezone$", + widgets.DetectTimezone.as_view(), + name="detect_timezone", + ), + url(r"^status-table$", status.status_table, name="status_table"), + url( + r"^template$", + problem.LanguageTemplateAjax.as_view(), + name="language_template_ajax", + ), + url( + r"^select2/", + include( + [ + url( + r"^user_search$", + UserSearchSelect2View.as_view(), + name="user_search_select2_ajax", + ), + url( + r"^user_search_chat$", + ChatUserSearchSelect2View.as_view(), + name="chat_user_search_select2_ajax", + ), + url( + r"^contest_users/(?P\w+)$", + ContestUserSearchSelect2View.as_view(), + name="contest_user_search_select2_ajax", + ), + url( + r"^ticket_user$", + TicketUserSelect2View.as_view(), + name="ticket_user_select2_ajax", + ), + url( + r"^ticket_assignee$", + AssigneeSelect2View.as_view(), + name="ticket_assignee_select2_ajax", + ), + ] + ), + ), + url( + r"^preview/", + include( + [ + url( + r"^problem$", + preview.ProblemMarkdownPreviewView.as_view(), + name="problem_preview", + ), + url( + r"^blog$", + preview.BlogMarkdownPreviewView.as_view(), + name="blog_preview", + ), + url( + r"^contest$", + preview.ContestMarkdownPreviewView.as_view(), + name="contest_preview", + ), + url( + r"^comment$", + preview.CommentMarkdownPreviewView.as_view(), + name="comment_preview", + ), + url( + r"^profile$", + preview.ProfileMarkdownPreviewView.as_view(), + name="profile_preview", + ), + url( + r"^organization$", + preview.OrganizationMarkdownPreviewView.as_view(), + name="organization_preview", + ), + url( + r"^solution$", + preview.SolutionMarkdownPreviewView.as_view(), + name="solution_preview", + ), + url( + r"^license$", + preview.LicenseMarkdownPreviewView.as_view(), + name="license_preview", + ), + url( + r"^ticket$", + preview.TicketMarkdownPreviewView.as_view(), + name="ticket_preview", + ), + ] + ), + ), + ] + ), + ), + url( + r"^feed/", + include( + [ + url(r"^problems/rss/$", ProblemFeed(), name="problem_rss"), + url(r"^problems/atom/$", AtomProblemFeed(), name="problem_atom"), + url(r"^comment/rss/$", CommentFeed(), name="comment_rss"), + url(r"^comment/atom/$", AtomCommentFeed(), name="comment_atom"), + url(r"^blog/rss/$", BlogFeed(), name="blog_rss"), + url(r"^blog/atom/$", AtomBlogFeed(), name="blog_atom"), + ] + ), + ), + url( + r"^stats/", + include( + [ + url( + "^language/", + include( + [ + url("^$", stats.language, name="language_stats"), + url( + "^data/all/$", + stats.language_data, + name="language_stats_data_all", + ), + url( + "^data/ac/$", + stats.ac_language_data, + name="language_stats_data_ac", + ), + url( + "^data/status/$", + stats.status_data, + name="stats_data_status", + ), + url( + "^data/ac_rate/$", + stats.ac_rate, + name="language_stats_data_ac_rate", + ), + ] + ), + ), + ] + ), + ), + url( + r"^tickets/", + include( + [ + url(r"^$", ticket.TicketList.as_view(), name="ticket_list"), + url(r"^ajax$", ticket.TicketListDataAjax.as_view(), name="ticket_ajax"), + ] + ), + ), + url( + r"^ticket/(?P\d+)", + include( + [ + url(r"^$", ticket.TicketView.as_view(), name="ticket"), + url( + r"^/ajax$", + ticket.TicketMessageDataAjax.as_view(), + name="ticket_message_ajax", + ), + url( + r"^/open$", + ticket.TicketStatusChangeView.as_view(open=True), + name="ticket_open", + ), + url( + r"^/close$", + ticket.TicketStatusChangeView.as_view(open=False), + name="ticket_close", + ), + url( + r"^/notes$", + ticket.TicketNotesEditView.as_view(), + name="ticket_notes", + ), + ] + ), + ), + url( + r"^sitemap\.xml$", + sitemap, + { + "sitemaps": { + "problem": ProblemSitemap, + "user": UserSitemap, + "home": HomePageSitemap, + "contest": ContestSitemap, + "organization": OrganizationSitemap, + "blog": BlogPostSitemap, + "solutions": SolutionSitemap, + "pages": UrlSitemap( + [ + {"location": "/about/", "priority": 0.9}, + ] + ), + } + }, + ), + url( + r"^judge-select2/", + include( + [ + url(r"^profile/$", UserSelect2View.as_view(), name="profile_select2"), + url( + r"^organization/$", + OrganizationSelect2View.as_view(), + name="organization_select2", + ), + url( + r"^problem/$", ProblemSelect2View.as_view(), name="problem_select2" + ), + url( + r"^contest/$", ContestSelect2View.as_view(), name="contest_select2" + ), + url( + r"^comment/$", CommentSelect2View.as_view(), name="comment_select2" + ), + ] + ), + ), + url( + r"^tasks/", + include( + [ + url( + r"^status/(?P[A-Za-z0-9-]*)$", + tasks.task_status, + name="task_status", + ), + url(r"^ajax_status$", tasks.task_status_ajax, name="task_status_ajax"), + url(r"^success$", tasks.demo_success), + url(r"^failure$", tasks.demo_failure), + url(r"^progress$", tasks.demo_progress), + ] + ), + ), + url(r"^about/", about.about, name="about"), + url( + r"^custom_checker_sample/", + about.custom_checker_sample, + name="custom_checker_sample", + ), + url( + r"^chat/", + include( + [ + url( + r"^(?P\d*)$", + login_required(chat.ChatView.as_view()), + name="chat", + ), + url(r"^delete/$", chat.delete_message, name="delete_chat_message"), + url(r"^post/$", chat.post_message, name="post_chat_message"), + url(r"^ajax$", chat.chat_message_ajax, name="chat_message_ajax"), + url( + r"^online_status/ajax$", + chat.online_status_ajax, + name="online_status_ajax", + ), + url( + r"^get_or_create_room$", + chat.get_or_create_room, + name="get_or_create_room", + ), + url( + r"^update_last_seen$", + chat.update_last_seen, + name="update_last_seen", + ), + url( + r"^online_status/user/ajax$", + chat.user_online_status_ajax, + name="user_online_status_ajax", + ), + url( + r"^toggle_ignore/(?P\d+)$", + chat.toggle_ignore, + name="toggle_ignore", + ), + url( + r"^get_unread_boxes$", + chat.get_unread_boxes, + name="get_unread_boxes", + ), + ] + ), + ), + url( + r"^internal/", + include( + [ + url( + r"^problem$", + internal.InternalProblem.as_view(), + name="internal_problem", + ), + ] + ), + ), + url( + r"^notifications/", login_required(notification.NotificationList.as_view()), - name='notification'), - - url(r'^import_users/', include([ - url(r'^$', user.ImportUsersView.as_view(), name='import_users'), - url(r'post_file/$', user.import_users_post_file, name='import_users_post_file'), - url(r'submit/$', user.import_users_submit, name='import_users_submit'), - url(r'sample/$', user.sample_import_users, name='import_users_sample') - ])), - - url(r'^volunteer/', include([ - url(r'^problem/vote$', volunteer.vote_problem, name='volunteer_problem_vote'), - ])), + name="notification", + ), + url( + r"^import_users/", + include( + [ + url(r"^$", user.ImportUsersView.as_view(), name="import_users"), + url( + r"post_file/$", + user.import_users_post_file, + name="import_users_post_file", + ), + url(r"submit/$", user.import_users_submit, name="import_users_submit"), + url(r"sample/$", user.sample_import_users, name="import_users_sample"), + ] + ), + ), + url( + r"^volunteer/", + include( + [ + url( + r"^problem/vote$", + volunteer.vote_problem, + name="volunteer_problem_vote", + ), + ] + ), + ), ] -favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png', - 'apple-touch-icon-57x57.png', 'apple-touch-icon-72x72.png', 'apple-touch-icon.png', 'mstile-70x70.png', - 'android-chrome-36x36.png', 'apple-touch-icon-precomposed.png', 'apple-touch-icon-76x76.png', - 'apple-touch-icon-60x60.png', 'android-chrome-96x96.png', 'mstile-144x144.png', 'mstile-150x150.png', - 'safari-pinned-tab.svg', 'android-chrome-144x144.png', 'apple-touch-icon-152x152.png', - 'favicon-96x96.png', - 'favicon-32x32.png', 'favicon-16x16.png', 'android-chrome-192x192.png', 'android-chrome-48x48.png', - 'mstile-310x150.png', 'apple-touch-icon-144x144.png', 'browserconfig.xml', 'manifest.json', - 'apple-touch-icon-120x120.png', 'mstile-310x310.png', 'reload.png'] +favicon_paths = [ + "apple-touch-icon-180x180.png", + "apple-touch-icon-114x114.png", + "android-chrome-72x72.png", + "apple-touch-icon-57x57.png", + "apple-touch-icon-72x72.png", + "apple-touch-icon.png", + "mstile-70x70.png", + "android-chrome-36x36.png", + "apple-touch-icon-precomposed.png", + "apple-touch-icon-76x76.png", + "apple-touch-icon-60x60.png", + "android-chrome-96x96.png", + "mstile-144x144.png", + "mstile-150x150.png", + "safari-pinned-tab.svg", + "android-chrome-144x144.png", + "apple-touch-icon-152x152.png", + "favicon-96x96.png", + "favicon-32x32.png", + "favicon-16x16.png", + "android-chrome-192x192.png", + "android-chrome-48x48.png", + "mstile-310x150.png", + "apple-touch-icon-144x144.png", + "browserconfig.xml", + "manifest.json", + "apple-touch-icon-120x120.png", + "mstile-310x310.png", + "reload.png", +] for favicon in favicon_paths: - urlpatterns.append(url(r'^%s$' % favicon, RedirectView.as_view( - url=static('icons/' + favicon) - ))) + urlpatterns.append( + url(r"^%s$" % favicon, RedirectView.as_view(url=static("icons/" + favicon))) + ) -handler404 = 'judge.views.error.error404' -handler403 = 'judge.views.error.error403' -handler500 = 'judge.views.error.error500' +handler404 = "judge.views.error.error404" +handler403 = "judge.views.error.error403" +handler500 = "judge.views.error.error500" -if 'newsletter' in settings.INSTALLED_APPS: - urlpatterns.append(url(r'^newsletter/', include('newsletter.urls'))) -if 'impersonate' in settings.INSTALLED_APPS: - urlpatterns.append(url(r'^impersonate/', include('impersonate.urls'))) +if "newsletter" in settings.INSTALLED_APPS: + urlpatterns.append(url(r"^newsletter/", include("newsletter.urls"))) +if "impersonate" in settings.INSTALLED_APPS: + urlpatterns.append(url(r"^impersonate/", include("impersonate.urls"))) diff --git a/dmoj/wsgi.py b/dmoj/wsgi.py index 6bec753..3cde2a4 100644 --- a/dmoj/wsgi.py +++ b/dmoj/wsgi.py @@ -1,5 +1,6 @@ import os -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") try: import MySQLdb # noqa: F401, imported for side effect @@ -8,5 +9,8 @@ except ImportError: pymysql.install_as_MySQLdb() -from django.core.wsgi import get_wsgi_application # noqa: E402, django must be imported here +from django.core.wsgi import ( + get_wsgi_application, +) # noqa: E402, django must be imported here + application = get_wsgi_application() diff --git a/dmoj/wsgi_async.py b/dmoj/wsgi_async.py index 69e105f..4a727c0 100644 --- a/dmoj/wsgi_async.py +++ b/dmoj/wsgi_async.py @@ -2,13 +2,17 @@ import os import gevent.monkey # noqa: I100, gevent must be imported here -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") gevent.monkey.patch_all() # noinspection PyUnresolvedReferences import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect -from django.core.wsgi import get_wsgi_application # noqa: E402, I100, I202, django must be imported here +from django.core.wsgi import ( + get_wsgi_application, +) # noqa: E402, I100, I202, django must be imported here + # noinspection PyUnresolvedReferences import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect + application = get_wsgi_application() diff --git a/dmoj_bridge_async.py b/dmoj_bridge_async.py index dee4112..f514d6b 100644 --- a/dmoj_bridge_async.py +++ b/dmoj_bridge_async.py @@ -2,19 +2,22 @@ import os import gevent.monkey # noqa: I100, gevent must be imported here -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") gevent.monkey.patch_all() # noinspection PyUnresolvedReferences import dmoj_install_pymysql # noqa: E402, F401, I100, I202, imported for side effect import django # noqa: E402, F401, I100, I202, django must be imported here + django.setup() # noinspection PyUnresolvedReferences import django_2_2_pymysql_patch # noqa: E402, I100, F401, I202, imported for side effect -from judge.bridge.daemon import judge_daemon # noqa: E402, I100, I202, django code must be imported here +from judge.bridge.daemon import ( + judge_daemon, +) # noqa: E402, I100, I202, django code must be imported here -if __name__ == '__main__': +if __name__ == "__main__": judge_daemon() diff --git a/dmoj_celery.py b/dmoj_celery.py index 58ea344..dc7996d 100644 --- a/dmoj_celery.py +++ b/dmoj_celery.py @@ -6,7 +6,7 @@ except ImportError: import dmoj_install_pymysql # noqa: F401, imported for side effect # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") # noinspection PyUnresolvedReferences import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect diff --git a/judge/__init__.py b/judge/__init__.py index 5c386cd..8aefa9d 100644 --- a/judge/__init__.py +++ b/judge/__init__.py @@ -1 +1 @@ -default_app_config = 'judge.apps.JudgeAppConfig' +default_app_config = "judge.apps.JudgeAppConfig" diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index c2e12f9..c9ad2f3 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -3,7 +3,12 @@ from django.contrib.admin.models import LogEntry from judge.admin.comments import CommentAdmin from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin -from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin +from judge.admin.interface import ( + BlogPostAdmin, + LicenseAdmin, + LogEntryAdmin, + NavigationBarAdmin, +) from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin from judge.admin.profile import ProfileAdmin @@ -12,10 +17,29 @@ from judge.admin.submission import SubmissionAdmin from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.ticket import TicketAdmin from judge.admin.volunteer import VolunteerProblemVoteAdmin -from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \ - ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \ - OrganizationRequest, Problem, ProblemGroup, ProblemPointsVote, ProblemType, Profile, Submission, Ticket, \ - VolunteerProblemVote +from judge.models import ( + BlogPost, + Comment, + CommentLock, + Contest, + ContestParticipation, + ContestTag, + Judge, + Language, + License, + MiscConfig, + NavigationBar, + Organization, + OrganizationRequest, + Problem, + ProblemGroup, + ProblemPointsVote, + ProblemType, + Profile, + Submission, + Ticket, + VolunteerProblemVote, +) admin.site.register(BlogPost, BlogPostAdmin) @@ -39,4 +63,4 @@ admin.site.register(ProblemType, ProblemTypeAdmin) admin.site.register(Profile, ProfileAdmin) admin.site.register(Submission, SubmissionAdmin) admin.site.register(Ticket, TicketAdmin) -admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin) \ No newline at end of file +admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin) diff --git a/judge/admin/comments.py b/judge/admin/comments.py index f71de4b..b9f63d6 100644 --- a/judge/admin/comments.py +++ b/judge/admin/comments.py @@ -11,53 +11,70 @@ from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidg class CommentForm(ModelForm): class Meta: widgets = { - 'author': AdminHeavySelect2Widget(data_view='profile_select2'), - 'parent': AdminHeavySelect2Widget(data_view='comment_select2'), + "author": AdminHeavySelect2Widget(data_view="profile_select2"), + "parent": AdminHeavySelect2Widget(data_view="comment_select2"), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview')) + widgets["body"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("comment_preview") + ) class CommentAdmin(VersionAdmin): fieldsets = ( - (None, {'fields': ('author', 'page', 'parent', 'score', 'hidden')}), - ('Content', {'fields': ('body',)}), + (None, {"fields": ("author", "page", "parent", "score", "hidden")}), + ("Content", {"fields": ("body",)}), ) - list_display = ['author', 'linked_page', 'time'] - search_fields = ['author__user__username', 'page', 'body'] - readonly_fields = ['score'] - actions = ['hide_comment', 'unhide_comment'] - list_filter = ['hidden'] + list_display = ["author", "linked_page", "time"] + search_fields = ["author__user__username", "page", "body"] + readonly_fields = ["score"] + actions = ["hide_comment", "unhide_comment"] + list_filter = ["hidden"] actions_on_top = True actions_on_bottom = True form = CommentForm - date_hierarchy = 'time' + date_hierarchy = "time" def get_queryset(self, request): - return Comment.objects.order_by('-time') + return Comment.objects.order_by("-time") def hide_comment(self, request, queryset): count = queryset.update(hidden=True) - self.message_user(request, ungettext('%d comment successfully hidden.', - '%d comments successfully hidden.', - count) % count) - hide_comment.short_description = _('Hide comments') + self.message_user( + request, + ungettext( + "%d comment successfully hidden.", + "%d comments successfully hidden.", + count, + ) + % count, + ) + + hide_comment.short_description = _("Hide comments") def unhide_comment(self, request, queryset): count = queryset.update(hidden=False) - self.message_user(request, ungettext('%d comment successfully unhidden.', - '%d comments successfully unhidden.', - count) % count) - unhide_comment.short_description = _('Unhide comments') + self.message_user( + request, + ungettext( + "%d comment successfully unhidden.", + "%d comments successfully unhidden.", + count, + ) + % count, + ) + + unhide_comment.short_description = _("Unhide comments") def linked_page(self, obj): link = obj.link if link is not None: return format_html('{1}', link, obj.page) else: - return format_html('{0}', obj.page) - linked_page.short_description = _('Associated page') - linked_page.admin_order_field = 'page' + return format_html("{0}", obj.page) + + linked_page.short_description = _("Associated page") + linked_page.admin_order_field = "page" def save_model(self, request, obj, form, change): super(CommentAdmin, self).save_model(request, obj, form, change) diff --git a/judge/admin/contest.py b/judge/admin/contest.py index bda5156..c9581fb 100644 --- a/judge/admin/contest.py +++ b/judge/admin/contest.py @@ -16,8 +16,14 @@ from reversion_compare.admin import CompareVersionAdmin from django_ace import AceWidget from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating from judge.ratings import rate_contest -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \ - AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + AdminPagedownWidget, + AdminSelect2MultipleWidget, + AdminSelect2Widget, + HeavyPreviewAdminPageDownWidget, +) class AdminHeavySelect2Widget(AdminHeavySelect2Widget): @@ -28,159 +34,252 @@ class AdminHeavySelect2Widget(AdminHeavySelect2Widget): class ContestTagForm(ModelForm): contests = ModelMultipleChoiceField( - label=_('Included contests'), + label=_("Included contests"), queryset=Contest.objects.all(), required=False, - widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2')) + widget=AdminHeavySelect2MultipleWidget(data_view="contest_select2"), + ) class ContestTagAdmin(admin.ModelAdmin): - fields = ('name', 'color', 'description', 'contests') - list_display = ('name', 'color') + fields = ("name", "color", "description", "contests") + list_display = ("name", "color") actions_on_top = True actions_on_bottom = True form = ContestTagForm if AdminPagedownWidget is not None: formfield_overrides = { - TextField: {'widget': AdminPagedownWidget}, + TextField: {"widget": AdminPagedownWidget}, } def save_model(self, request, obj, form, change): super(ContestTagAdmin, self).save_model(request, obj, form, change) - obj.contests.set(form.cleaned_data['contests']) + obj.contests.set(form.cleaned_data["contests"]) def get_form(self, request, obj=None, **kwargs): form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs) if obj is not None: - form.base_fields['contests'].initial = obj.contests.all() + form.base_fields["contests"].initial = obj.contests.all() return form class ContestProblemInlineForm(ModelForm): class Meta: - widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')} + widgets = {"problem": AdminHeavySelect2Widget(data_view="problem_select2")} class ContestProblemInline(admin.TabularInline): model = ContestProblem - verbose_name = _('Problem') - verbose_name_plural = 'Problems' - fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order', - 'rejudge_column') - readonly_fields = ('rejudge_column',) + verbose_name = _("Problem") + verbose_name_plural = "Problems" + fields = ( + "problem", + "points", + "partial", + "is_pretested", + "max_submissions", + "output_prefix_override", + "order", + "rejudge_column", + ) + readonly_fields = ("rejudge_column",) form = ContestProblemInlineForm def rejudge_column(self, obj): if obj.id is None: - return '' - return format_html('Rejudge', - reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id))) - rejudge_column.short_description = '' + return "" + return format_html( + 'Rejudge', + reverse("admin:judge_contest_rejudge", args=(obj.contest.id, obj.id)), + ) + + rejudge_column.short_description = "" class ContestForm(ModelForm): def __init__(self, *args, **kwargs): super(ContestForm, self).__init__(*args, **kwargs) - if 'rate_exclude' in self.fields: + if "rate_exclude" in self.fields: if self.instance and self.instance.id: - self.fields['rate_exclude'].queryset = \ - Profile.objects.filter(contest_history__contest=self.instance).distinct() + self.fields["rate_exclude"].queryset = Profile.objects.filter( + contest_history__contest=self.instance + ).distinct() else: - self.fields['rate_exclude'].queryset = Profile.objects.none() - self.fields['banned_users'].widget.can_add_related = False - self.fields['view_contest_scoreboard'].widget.can_add_related = False + self.fields["rate_exclude"].queryset = Profile.objects.none() + self.fields["banned_users"].widget.can_add_related = False + self.fields["view_contest_scoreboard"].widget.can_add_related = False def clean(self): cleaned_data = super(ContestForm, self).clean() - cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None) + cleaned_data["banned_users"].filter( + current_contest__contest=self.instance + ).update(current_contest=None) class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), - 'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), - 'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), - 'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'), - 'tags': AdminSelect2MultipleWidget, - 'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'view_contest_scoreboard': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), + "authors": AdminHeavySelect2MultipleWidget(data_view="profile_select2"), + "curators": AdminHeavySelect2MultipleWidget(data_view="profile_select2"), + "testers": AdminHeavySelect2MultipleWidget(data_view="profile_select2"), + "private_contestants": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "organizations": AdminHeavySelect2MultipleWidget( + data_view="organization_select2" + ), + "tags": AdminSelect2MultipleWidget, + "banned_users": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "view_contest_scoreboard": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview')) + widgets["description"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("contest_preview") + ) class ContestAdmin(CompareVersionAdmin): fieldsets = ( - (None, {'fields': ('key', 'name', 'authors', 'curators', 'testers')}), - (_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'scoreboard_visibility', - 'run_pretests_only', 'points_precision')}), - (_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}), - (_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}), - (_('Format'), {'fields': ('format_name', 'format_config', 'problem_label_script')}), - (_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}), - (_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private', - 'organizations', 'view_contest_scoreboard')}), - (_('Justice'), {'fields': ('banned_users',)}), + (None, {"fields": ("key", "name", "authors", "curators", "testers")}), + ( + _("Settings"), + { + "fields": ( + "is_visible", + "use_clarifications", + "hide_problem_tags", + "scoreboard_visibility", + "run_pretests_only", + "points_precision", + ) + }, + ), + (_("Scheduling"), {"fields": ("start_time", "end_time", "time_limit")}), + ( + _("Details"), + { + "fields": ( + "description", + "og_image", + "logo_override_image", + "tags", + "summary", + ) + }, + ), + ( + _("Format"), + {"fields": ("format_name", "format_config", "problem_label_script")}, + ), + ( + _("Rating"), + { + "fields": ( + "is_rated", + "rate_all", + "rating_floor", + "rating_ceiling", + "rate_exclude", + ) + }, + ), + ( + _("Access"), + { + "fields": ( + "access_code", + "is_private", + "private_contestants", + "is_organization_private", + "organizations", + "view_contest_scoreboard", + ) + }, + ), + (_("Justice"), {"fields": ("banned_users",)}), ) - list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count') - search_fields = ('key', 'name') + list_display = ( + "key", + "name", + "is_visible", + "is_rated", + "start_time", + "end_time", + "time_limit", + "user_count", + ) + search_fields = ("key", "name") inlines = [ContestProblemInline] actions_on_top = True actions_on_bottom = True form = ContestForm - change_list_template = 'admin/judge/contest/change_list.html' - filter_horizontal = ['rate_exclude'] - date_hierarchy = 'start_time' + change_list_template = "admin/judge/contest/change_list.html" + filter_horizontal = ["rate_exclude"] + date_hierarchy = "start_time" def get_actions(self, request): actions = super(ContestAdmin, self).get_actions(request) - if request.user.has_perm('judge.change_contest_visibility') or \ - request.user.has_perm('judge.create_private_contest'): - for action in ('make_visible', 'make_hidden'): + if request.user.has_perm( + "judge.change_contest_visibility" + ) or request.user.has_perm("judge.create_private_contest"): + for action in ("make_visible", "make_hidden"): actions[action] = self.get_action(action) return actions def get_queryset(self, request): queryset = Contest.objects.all() - if request.user.has_perm('judge.edit_all_contest'): + if request.user.has_perm("judge.edit_all_contest"): return queryset else: - return queryset.filter(Q(authors=request.profile) | Q(curators=request.profile)).distinct() + return queryset.filter( + Q(authors=request.profile) | Q(curators=request.profile) + ).distinct() def get_readonly_fields(self, request, obj=None): readonly = [] - if not request.user.has_perm('judge.contest_rating'): - readonly += ['is_rated', 'rate_all', 'rate_exclude'] - if not request.user.has_perm('judge.contest_access_code'): - readonly += ['access_code'] - if not request.user.has_perm('judge.create_private_contest'): - readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations'] - if not request.user.has_perm('judge.change_contest_visibility'): - readonly += ['is_visible'] - if not request.user.has_perm('judge.contest_problem_label'): - readonly += ['problem_label_script'] + if not request.user.has_perm("judge.contest_rating"): + readonly += ["is_rated", "rate_all", "rate_exclude"] + if not request.user.has_perm("judge.contest_access_code"): + readonly += ["access_code"] + if not request.user.has_perm("judge.create_private_contest"): + readonly += [ + "is_private", + "private_contestants", + "is_organization_private", + "organizations", + ] + if not request.user.has_perm("judge.change_contest_visibility"): + readonly += ["is_visible"] + if not request.user.has_perm("judge.contest_problem_label"): + readonly += ["problem_label_script"] return readonly def save_model(self, request, obj, form, change): # `is_visible` will not appear in `cleaned_data` if user cannot edit it - if form.cleaned_data.get('is_visible') and not request.user.has_perm('judge.change_contest_visibility'): - if not form.cleaned_data['is_private'] and not form.cleaned_data['is_organization_private']: + if form.cleaned_data.get("is_visible") and not request.user.has_perm( + "judge.change_contest_visibility" + ): + if ( + not form.cleaned_data["is_private"] + and not form.cleaned_data["is_organization_private"] + ): raise PermissionDenied - if not request.user.has_perm('judge.create_private_contest'): + if not request.user.has_perm("judge.create_private_contest"): raise PermissionDenied super().save_model(request, obj, form, change) # We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored self._rescored = False - if form.changed_data and any(f in form.changed_data for f in ('format_config', 'format_name')): + if form.changed_data and any( + f in form.changed_data for f in ("format_config", "format_name") + ): self._rescore(obj.key) self._rescored = True @@ -188,10 +287,10 @@ class ContestAdmin(CompareVersionAdmin): super().save_related(request, form, formsets, change) # Only rescored if we did not already do so in `save_model` if not self._rescored and any(formset.has_changed() for formset in formsets): - self._rescore(form.cleaned_data['key']) + self._rescore(form.cleaned_data["key"]) def has_change_permission(self, request, obj=None): - if not request.user.has_perm('judge.edit_own_contest'): + if not request.user.has_perm("judge.edit_own_contest"): return False if obj is None: return True @@ -199,76 +298,113 @@ class ContestAdmin(CompareVersionAdmin): def _rescore(self, contest_key): from judge.tasks import rescore_contest + transaction.on_commit(rescore_contest.s(contest_key).delay) def make_visible(self, request, queryset): - if not request.user.has_perm('judge.change_contest_visibility'): - queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True)) + if not request.user.has_perm("judge.change_contest_visibility"): + queryset = queryset.filter( + Q(is_private=True) | Q(is_organization_private=True) + ) count = queryset.update(is_visible=True) - self.message_user(request, ungettext('%d contest successfully marked as visible.', - '%d contests successfully marked as visible.', - count) % count) - make_visible.short_description = _('Mark contests as visible') + self.message_user( + request, + ungettext( + "%d contest successfully marked as visible.", + "%d contests successfully marked as visible.", + count, + ) + % count, + ) + + make_visible.short_description = _("Mark contests as visible") def make_hidden(self, request, queryset): - if not request.user.has_perm('judge.change_contest_visibility'): - queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True)) + if not request.user.has_perm("judge.change_contest_visibility"): + queryset = queryset.filter( + Q(is_private=True) | Q(is_organization_private=True) + ) count = queryset.update(is_visible=True) - self.message_user(request, ungettext('%d contest successfully marked as hidden.', - '%d contests successfully marked as hidden.', - count) % count) - make_hidden.short_description = _('Mark contests as hidden') + self.message_user( + request, + ungettext( + "%d contest successfully marked as hidden.", + "%d contests successfully marked as hidden.", + count, + ) + % count, + ) + + make_hidden.short_description = _("Mark contests as hidden") def get_urls(self): return [ - url(r'^rate/all/$', self.rate_all_view, name='judge_contest_rate_all'), - url(r'^(\d+)/rate/$', self.rate_view, name='judge_contest_rate'), - url(r'^(\d+)/judge/(\d+)/$', self.rejudge_view, name='judge_contest_rejudge'), + url(r"^rate/all/$", self.rate_all_view, name="judge_contest_rate_all"), + url(r"^(\d+)/rate/$", self.rate_view, name="judge_contest_rate"), + url( + r"^(\d+)/judge/(\d+)/$", self.rejudge_view, name="judge_contest_rejudge" + ), ] + super(ContestAdmin, self).get_urls() def rejudge_view(self, request, contest_id, problem_id): - queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission') + queryset = ContestSubmission.objects.filter( + problem_id=problem_id + ).select_related("submission") for model in queryset: model.submission.judge(rejudge=True) - self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.', - '%d submissions were successfully scheduled for rejudging.', - len(queryset)) % len(queryset)) - return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,))) + self.message_user( + request, + ungettext( + "%d submission was successfully scheduled for rejudging.", + "%d submissions were successfully scheduled for rejudging.", + len(queryset), + ) + % len(queryset), + ) + return HttpResponseRedirect( + reverse("admin:judge_contest_change", args=(contest_id,)) + ) def rate_all_view(self, request): - if not request.user.has_perm('judge.contest_rating'): + if not request.user.has_perm("judge.contest_rating"): raise PermissionDenied() with transaction.atomic(): with connection.cursor() as cursor: - cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table) + cursor.execute("TRUNCATE TABLE `%s`" % Rating._meta.db_table) Profile.objects.update(rating=None) - for contest in Contest.objects.filter(is_rated=True, end_time__lte=timezone.now()).order_by('end_time'): + for contest in Contest.objects.filter( + is_rated=True, end_time__lte=timezone.now() + ).order_by("end_time"): rate_contest(contest) - return HttpResponseRedirect(reverse('admin:judge_contest_changelist')) + return HttpResponseRedirect(reverse("admin:judge_contest_changelist")) def rate_view(self, request, id): - if not request.user.has_perm('judge.contest_rating'): + if not request.user.has_perm("judge.contest_rating"): raise PermissionDenied() contest = get_object_or_404(Contest, id=id) if not contest.is_rated or not contest.ended: raise Http404() with transaction.atomic(): contest.rate() - return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist'))) + return HttpResponseRedirect( + request.META.get("HTTP_REFERER", reverse("admin:judge_contest_changelist")) + ) def get_form(self, request, obj=None, **kwargs): form = super(ContestAdmin, self).get_form(request, obj, **kwargs) - if 'problem_label_script' in form.base_fields: + if "problem_label_script" in form.base_fields: # form.base_fields['problem_label_script'] does not exist when the user has only view permission # on the model. - form.base_fields['problem_label_script'].widget = AceWidget('lua', request.profile.ace_theme) - - perms = ('edit_own_contest', 'edit_all_contest') - form.base_fields['curators'].queryset = Profile.objects.filter( - Q(user__is_superuser=True) | - Q(user__groups__permissions__codename__in=perms) | - Q(user__user_permissions__codename__in=perms), + form.base_fields["problem_label_script"].widget = AceWidget( + "lua", request.profile.ace_theme + ) + + perms = ("edit_own_contest", "edit_all_contest") + form.base_fields["curators"].queryset = Profile.objects.filter( + Q(user__is_superuser=True) + | Q(user__groups__permissions__codename__in=perms) + | Q(user__user_permissions__codename__in=perms), ).distinct() return form @@ -276,29 +412,48 @@ class ContestAdmin(CompareVersionAdmin): class ContestParticipationForm(ModelForm): class Meta: widgets = { - 'contest': AdminSelect2Widget(), - 'user': AdminHeavySelect2Widget(data_view='profile_select2'), + "contest": AdminSelect2Widget(), + "user": AdminHeavySelect2Widget(data_view="profile_select2"), } class ContestParticipationAdmin(admin.ModelAdmin): - fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified') - list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime', 'tiebreaker') - actions = ['recalculate_results'] + fields = ("contest", "user", "real_start", "virtual", "is_disqualified") + list_display = ( + "contest", + "username", + "show_virtual", + "real_start", + "score", + "cumtime", + "tiebreaker", + ) + actions = ["recalculate_results"] actions_on_bottom = actions_on_top = True - search_fields = ('contest__key', 'contest__name', 'user__user__username') + search_fields = ("contest__key", "contest__name", "user__user__username") form = ContestParticipationForm - date_hierarchy = 'real_start' + date_hierarchy = "real_start" def get_queryset(self, request): - return super(ContestParticipationAdmin, self).get_queryset(request).only( - 'contest__name', 'contest__format_name', 'contest__format_config', - 'user__user__username', 'real_start', 'score', 'cumtime', 'tiebreaker', 'virtual', + return ( + super(ContestParticipationAdmin, self) + .get_queryset(request) + .only( + "contest__name", + "contest__format_name", + "contest__format_config", + "user__user__username", + "real_start", + "score", + "cumtime", + "tiebreaker", + "virtual", + ) ) def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) - if form.changed_data and 'is_disqualified' in form.changed_data: + if form.changed_data and "is_disqualified" in form.changed_data: obj.set_disqualified(obj.is_disqualified) def recalculate_results(self, request, queryset): @@ -306,17 +461,26 @@ class ContestParticipationAdmin(admin.ModelAdmin): for participation in queryset: participation.recompute_results() count += 1 - self.message_user(request, ungettext('%d participation recalculated.', - '%d participations recalculated.', - count) % count) - recalculate_results.short_description = _('Recalculate results') + self.message_user( + request, + ungettext( + "%d participation recalculated.", + "%d participations recalculated.", + count, + ) + % count, + ) + + recalculate_results.short_description = _("Recalculate results") def username(self, obj): return obj.user.username - username.short_description = _('username') - username.admin_order_field = 'user__user__username' + + username.short_description = _("username") + username.admin_order_field = "user__user__username" def show_virtual(self, obj): - return obj.virtual or '-' - show_virtual.short_description = _('virtual') - show_virtual.admin_order_field = 'virtual' + return obj.virtual or "-" + + show_virtual.short_description = _("virtual") + show_virtual.admin_order_field = "virtual" diff --git a/judge/admin/interface.py b/judge/admin/interface.py index fb57844..5ee4c38 100644 --- a/judge/admin/interface.py +++ b/judge/admin/interface.py @@ -11,23 +11,28 @@ from reversion_compare.admin import CompareVersionAdmin from judge.dblock import LockModel from judge.models import NavigationBar -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + HeavyPreviewAdminPageDownWidget, +) class NavigationBarAdmin(DraggableMPTTAdmin): - list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path') - fields = ('key', 'label', 'path', 'order', 'regex', 'parent') + list_display = DraggableMPTTAdmin.list_display + ("key", "linked_path") + fields = ("key", "label", "path", "order", "regex", "parent") list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set mptt_level_indent = 20 - sortable = 'order' + sortable = "order" def __init__(self, *args, **kwargs): super(NavigationBarAdmin, self).__init__(*args, **kwargs) self.__save_model_calls = 0 def linked_path(self, obj): - return format_html(u'{0}', obj.path) - linked_path.short_description = _('link path') + return format_html('{0}', obj.path) + + linked_path.short_description = _("link path") def save_model(self, request, obj, form, change): self.__save_model_calls += 1 @@ -36,7 +41,9 @@ class NavigationBarAdmin(DraggableMPTTAdmin): def changelist_view(self, request, extra_context=None): self.__save_model_calls = 0 with NavigationBar.objects.disable_mptt_updates(): - result = super(NavigationBarAdmin, self).changelist_view(request, extra_context) + result = super(NavigationBarAdmin, self).changelist_view( + request, extra_context + ) if self.__save_model_calls: with LockModel(write=(NavigationBar,)): NavigationBar.objects.rebuild() @@ -46,74 +53,105 @@ class NavigationBarAdmin(DraggableMPTTAdmin): class BlogPostForm(ModelForm): def __init__(self, *args, **kwargs): super(BlogPostForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False + self.fields["authors"].widget.can_add_related = False class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2', - attrs={'style': 'width: 100%'}), + "authors": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "organizations": AdminHeavySelect2MultipleWidget( + data_view="organization_select2", attrs={"style": "width: 100%"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview')) - widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview')) + widgets["content"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("blog_preview") + ) + widgets["summary"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("blog_preview") + ) class BlogPostAdmin(CompareVersionAdmin): fieldsets = ( - (None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on', - 'is_organization_private', 'organizations')}), - (_('Content'), {'fields': ('content', 'og_image')}), - (_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}), + ( + None, + { + "fields": ( + "title", + "slug", + "authors", + "visible", + "sticky", + "publish_on", + "is_organization_private", + "organizations", + ) + }, + ), + (_("Content"), {"fields": ("content", "og_image")}), + (_("Summary"), {"classes": ("collapse",), "fields": ("summary",)}), ) - prepopulated_fields = {'slug': ('title',)} - list_display = ('id', 'title', 'visible', 'sticky', 'publish_on') - list_display_links = ('id', 'title') - ordering = ('-publish_on',) + prepopulated_fields = {"slug": ("title",)} + list_display = ("id", "title", "visible", "sticky", "publish_on") + list_display_links = ("id", "title") + ordering = ("-publish_on",) form = BlogPostForm - date_hierarchy = 'publish_on' + date_hierarchy = "publish_on" def has_change_permission(self, request, obj=None): - return (request.user.has_perm('judge.edit_all_post') or - request.user.has_perm('judge.change_blogpost') and ( - obj is None or - obj.authors.filter(id=request.profile.id).exists())) + return ( + request.user.has_perm("judge.edit_all_post") + or request.user.has_perm("judge.change_blogpost") + and (obj is None or obj.authors.filter(id=request.profile.id).exists()) + ) class SolutionForm(ModelForm): def __init__(self, *args, **kwargs): super(SolutionForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False + self.fields["authors"].widget.can_add_related = False class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}), + "authors": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "problem": AdminHeavySelect2Widget( + data_view="problem_select2", attrs={"style": "width: 250px"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview')) + widgets["content"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("solution_preview") + ) class LicenseForm(ModelForm): class Meta: if HeavyPreviewAdminPageDownWidget is not None: - widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))} + widgets = { + "text": HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("license_preview") + ) + } class LicenseAdmin(admin.ModelAdmin): - fields = ('key', 'link', 'name', 'display', 'icon', 'text') - list_display = ('name', 'key') + fields = ("key", "link", "name", "display", "icon", "text") + list_display = ("name", "key") form = LicenseForm class UserListFilter(admin.SimpleListFilter): - title = _('user') - parameter_name = 'user' + title = _("user") + parameter_name = "user" def lookups(self, request, model_admin): - return User.objects.filter(is_staff=True).values_list('id', 'username') + return User.objects.filter(is_staff=True).values_list("id", "username") def queryset(self, request, queryset): if self.value(): @@ -122,10 +160,24 @@ class UserListFilter(admin.SimpleListFilter): class LogEntryAdmin(admin.ModelAdmin): - readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message') - list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link', 'diff_link') - search_fields = ('object_repr', 'change_message') - list_filter = (UserListFilter, 'content_type') + readonly_fields = ( + "user", + "content_type", + "object_id", + "object_repr", + "action_flag", + "change_message", + ) + list_display = ( + "__str__", + "action_time", + "user", + "content_type", + "object_link", + "diff_link", + ) + search_fields = ("object_repr", "change_message") + list_filter = (UserListFilter, "content_type") list_display_links = None actions = None @@ -144,25 +196,35 @@ class LogEntryAdmin(admin.ModelAdmin): else: ct = obj.content_type try: - link = format_html('{0}', obj.object_repr, - reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,))) + link = format_html( + '{0}', + obj.object_repr, + reverse( + "admin:%s_%s_change" % (ct.app_label, ct.model), + args=(obj.object_id,), + ), + ) except NoReverseMatch: link = obj.object_repr return link - object_link.admin_order_field = 'object_repr' - object_link.short_description = _('object') + + object_link.admin_order_field = "object_repr" + object_link.short_description = _("object") def diff_link(self, obj): if obj.is_deletion(): return None ct = obj.content_type try: - url = reverse('admin:%s_%s_history' % (ct.app_label, ct.model), args=(obj.object_id,)) - link = format_html('{0}', _('Diff'), url) + url = reverse( + "admin:%s_%s_history" % (ct.app_label, ct.model), args=(obj.object_id,) + ) + link = format_html('{0}', _("Diff"), url) except NoReverseMatch: link = None return link - diff_link.short_description = _('diff') + + diff_link.short_description = _("diff") def queryset(self, request): - return super().queryset(request).prefetch_related('content_type') + return super().queryset(request).prefetch_related("content_type") diff --git a/judge/admin/organization.py b/judge/admin/organization.py index f9d78d4..64a4da0 100644 --- a/judge/admin/organization.py +++ b/judge/admin/organization.py @@ -6,61 +6,88 @@ from django.utils.translation import gettext, gettext_lazy as _ from reversion.admin import VersionAdmin from judge.models import Organization -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + HeavyPreviewAdminPageDownWidget, +) class OrganizationForm(ModelForm): class Meta: widgets = { - 'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), - 'registrant': AdminHeavySelect2Widget(data_view='profile_select2'), + "admins": AdminHeavySelect2MultipleWidget(data_view="profile_select2"), + "registrant": AdminHeavySelect2Widget(data_view="profile_select2"), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview')) + widgets["about"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("organization_preview") + ) class OrganizationAdmin(VersionAdmin): - readonly_fields = ('creation_date',) - fields = ('name', 'slug', 'short_name', 'is_open', 'about', 'logo_override_image', 'slots', 'registrant', - 'creation_date', 'admins') - list_display = ('name', 'short_name', 'is_open', 'slots', 'registrant', 'show_public') - prepopulated_fields = {'slug': ('name',)} + readonly_fields = ("creation_date",) + fields = ( + "name", + "slug", + "short_name", + "is_open", + "about", + "logo_override_image", + "slots", + "registrant", + "creation_date", + "admins", + ) + list_display = ( + "name", + "short_name", + "is_open", + "slots", + "registrant", + "show_public", + ) + prepopulated_fields = {"slug": ("name",)} actions_on_top = True actions_on_bottom = True form = OrganizationForm def show_public(self, obj): - return format_html('{1}', - obj.get_absolute_url(), gettext('View on site')) + return format_html( + '{1}', + obj.get_absolute_url(), + gettext("View on site"), + ) - show_public.short_description = '' + show_public.short_description = "" def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields - if not request.user.has_perm('judge.organization_admin'): - return fields + ('registrant', 'admins', 'is_open', 'slots') + if not request.user.has_perm("judge.organization_admin"): + return fields + ("registrant", "admins", "is_open", "slots") return fields def get_queryset(self, request): queryset = Organization.objects.all() - if request.user.has_perm('judge.edit_all_organization'): + if request.user.has_perm("judge.edit_all_organization"): return queryset else: return queryset.filter(admins=request.profile.id) def has_change_permission(self, request, obj=None): - if not request.user.has_perm('judge.change_organization'): + if not request.user.has_perm("judge.change_organization"): return False - if request.user.has_perm('judge.edit_all_organization') or obj is None: + if request.user.has_perm("judge.edit_all_organization") or obj is None: return True return obj.admins.filter(id=request.profile.id).exists() class OrganizationRequestAdmin(admin.ModelAdmin): - list_display = ('username', 'organization', 'state', 'time') - readonly_fields = ('user', 'organization') + list_display = ("username", "organization", "state", "time") + readonly_fields = ("user", "organization") def username(self, obj): return obj.user.user.username - username.short_description = _('username') - username.admin_order_field = 'user__user__username' + + username.short_description = _("username") + username.admin_order_field = "user__user__username" diff --git a/judge/admin/problem.py b/judge/admin/problem.py index a8e6c35..c687655 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -14,45 +14,74 @@ from reversion.admin import VersionAdmin from reversion_compare.admin import CompareVersionAdmin -from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \ - CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget +from judge.models import ( + LanguageLimit, + Problem, + ProblemClarification, + ProblemTranslation, + Profile, + Solution, +) +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminSelect2MultipleWidget, + AdminSelect2Widget, + CheckboxSelectMultipleWithSelectAll, + HeavyPreviewAdminPageDownWidget, + HeavyPreviewPageDownWidget, +) class ProblemForm(ModelForm): - change_message = forms.CharField(max_length=256, label='Edit reason', required=False) + change_message = forms.CharField( + max_length=256, label="Edit reason", required=False + ) def __init__(self, *args, **kwargs): super(ProblemForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False - self.fields['curators'].widget.can_add_related = False - self.fields['testers'].widget.can_add_related = False - self.fields['banned_users'].widget.can_add_related = False - self.fields['change_message'].widget.attrs.update({ - 'placeholder': gettext('Describe the changes you made (optional)'), - }) + self.fields["authors"].widget.can_add_related = False + self.fields["curators"].widget.can_add_related = False + self.fields["testers"].widget.can_add_related = False + self.fields["banned_users"].widget.can_add_related = False + self.fields["change_message"].widget.attrs.update( + { + "placeholder": gettext("Describe the changes you made (optional)"), + } + ) class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2', - attrs={'style': 'width: 100%'}), - 'types': AdminSelect2MultipleWidget, - 'group': AdminSelect2Widget, + "authors": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "curators": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "testers": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "banned_users": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "organizations": AdminHeavySelect2MultipleWidget( + data_view="organization_select2", attrs={"style": "width: 100%"} + ), + "types": AdminSelect2MultipleWidget, + "group": AdminSelect2Widget, } if HeavyPreviewAdminPageDownWidget is not None: - widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview')) + widgets["description"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("problem_preview") + ) class ProblemCreatorListFilter(admin.SimpleListFilter): - title = parameter_name = 'creator' + title = parameter_name = "creator" def lookups(self, request, model_admin): - queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True) + queryset = Profile.objects.exclude(authored_problems=None).values_list( + "user__username", flat=True + ) return [(name, name) for name in queryset] def queryset(self, request, queryset): @@ -63,24 +92,28 @@ class ProblemCreatorListFilter(admin.SimpleListFilter): class LanguageLimitInlineForm(ModelForm): class Meta: - widgets = {'language': AdminSelect2Widget} + widgets = {"language": AdminSelect2Widget} class LanguageLimitInline(admin.TabularInline): model = LanguageLimit - fields = ('language', 'time_limit', 'memory_limit') + fields = ("language", "time_limit", "memory_limit") form = LanguageLimitInlineForm class ProblemClarificationForm(ModelForm): class Meta: if HeavyPreviewPageDownWidget is not None: - widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))} + widgets = { + "description": HeavyPreviewPageDownWidget( + preview=reverse_lazy("comment_preview") + ) + } class ProblemClarificationInline(admin.StackedInline): model = ProblemClarification - fields = ('description',) + fields = ("description",) form = ProblemClarificationForm extra = 0 @@ -88,20 +121,24 @@ class ProblemClarificationInline(admin.StackedInline): class ProblemSolutionForm(ModelForm): def __init__(self, *args, **kwargs): super(ProblemSolutionForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False + self.fields["authors"].widget.can_add_related = False class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), + "authors": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview')) + widgets["content"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("solution_preview") + ) class ProblemSolutionInline(admin.StackedInline): model = Solution - fields = ('is_public', 'publish_on', 'authors', 'content') + fields = ("is_public", "publish_on", "authors", "content") form = ProblemSolutionForm extra = 0 @@ -109,168 +146,250 @@ class ProblemSolutionInline(admin.StackedInline): class ProblemTranslationForm(ModelForm): class Meta: if HeavyPreviewAdminPageDownWidget is not None: - widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))} + widgets = { + "description": HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("problem_preview") + ) + } class ProblemTranslationInline(admin.StackedInline): model = ProblemTranslation - fields = ('language', 'name', 'description') + fields = ("language", "name", "description") form = ProblemTranslationForm extra = 0 class ProblemAdmin(CompareVersionAdmin): fieldsets = ( - (None, { - 'fields': ( - 'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers', - 'is_organization_private', 'organizations', 'description', 'license', - ), - }), - (_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}), - (_('Taxonomy'), {'fields': ('types', 'group')}), - (_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}), - (_('Limits'), {'fields': ('time_limit', 'memory_limit')}), - (_('Language'), {'fields': ('allowed_languages',)}), - (_('Justice'), {'fields': ('banned_users',)}), - (_('History'), {'fields': ('change_message',)}), + ( + None, + { + "fields": ( + "code", + "name", + "is_public", + "is_manually_managed", + "date", + "authors", + "curators", + "testers", + "is_organization_private", + "organizations", + "description", + "license", + ), + }, + ), + ( + _("Social Media"), + {"classes": ("collapse",), "fields": ("og_image", "summary")}, + ), + (_("Taxonomy"), {"fields": ("types", "group")}), + (_("Points"), {"fields": (("points", "partial"), "short_circuit")}), + (_("Limits"), {"fields": ("time_limit", "memory_limit")}), + (_("Language"), {"fields": ("allowed_languages",)}), + (_("Justice"), {"fields": ("banned_users",)}), + (_("History"), {"fields": ("change_message",)}), ) - list_display = ['code', 'name', 'show_authors', 'points', 'vote_cnt', 'vote_mean', 'vote_median', 'vote_std', 'is_public', 'show_public'] - ordering = ['code'] - search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username') - inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline] + list_display = [ + "code", + "name", + "show_authors", + "points", + "vote_cnt", + "vote_mean", + "vote_median", + "vote_std", + "is_public", + "show_public", + ] + ordering = ["code"] + search_fields = ( + "code", + "name", + "authors__user__username", + "curators__user__username", + ) + inlines = [ + LanguageLimitInline, + ProblemClarificationInline, + ProblemSolutionInline, + ProblemTranslationInline, + ] list_max_show_all = 1000 actions_on_top = True actions_on_bottom = True - list_filter = ('is_public', ProblemCreatorListFilter) + list_filter = ("is_public", ProblemCreatorListFilter) form = ProblemForm - date_hierarchy = 'date' + date_hierarchy = "date" def get_actions(self, request): actions = super(ProblemAdmin, self).get_actions(request) - if request.user.has_perm('judge.change_public_visibility'): - func, name, desc = self.get_action('make_public') + if request.user.has_perm("judge.change_public_visibility"): + func, name, desc = self.get_action("make_public") actions[name] = (func, name, desc) - func, name, desc = self.get_action('make_private') + func, name, desc = self.get_action("make_private") actions[name] = (func, name, desc) return actions def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields - if not request.user.has_perm('judge.change_public_visibility'): - fields += ('is_public',) - if not request.user.has_perm('judge.change_manually_managed'): - fields += ('is_manually_managed',) + if not request.user.has_perm("judge.change_public_visibility"): + fields += ("is_public",) + if not request.user.has_perm("judge.change_manually_managed"): + fields += ("is_manually_managed",) return fields def show_authors(self, obj): - return ', '.join(map(attrgetter('user.username'), obj.authors.all())) + return ", ".join(map(attrgetter("user.username"), obj.authors.all())) - show_authors.short_description = _('Authors') + show_authors.short_description = _("Authors") def show_public(self, obj): - return format_html('{0}', gettext('View on site'), obj.get_absolute_url()) + return format_html( + '{0}', gettext("View on site"), obj.get_absolute_url() + ) - show_public.short_description = '' + show_public.short_description = "" def _rescore(self, request, problem_id): from judge.tasks import rescore_problem + transaction.on_commit(rescore_problem.s(problem_id).delay) def make_public(self, request, queryset): count = queryset.update(is_public=True) - for problem_id in queryset.values_list('id', flat=True): + for problem_id in queryset.values_list("id", flat=True): self._rescore(request, problem_id) - self.message_user(request, ungettext('%d problem successfully marked as public.', - '%d problems successfully marked as public.', - count) % count) + self.message_user( + request, + ungettext( + "%d problem successfully marked as public.", + "%d problems successfully marked as public.", + count, + ) + % count, + ) - make_public.short_description = _('Mark problems as public') + make_public.short_description = _("Mark problems as public") def make_private(self, request, queryset): count = queryset.update(is_public=False) - for problem_id in queryset.values_list('id', flat=True): + for problem_id in queryset.values_list("id", flat=True): self._rescore(request, problem_id) - self.message_user(request, ungettext('%d problem successfully marked as private.', - '%d problems successfully marked as private.', - count) % count) + self.message_user( + request, + ungettext( + "%d problem successfully marked as private.", + "%d problems successfully marked as private.", + count, + ) + % count, + ) - make_private.short_description = _('Mark problems as private') + make_private.short_description = _("Mark problems as private") def get_queryset(self, request): - queryset = Problem.objects.prefetch_related('authors__user') + queryset = Problem.objects.prefetch_related("authors__user") queryset = queryset.annotate( - _vote_mean=Avg('problem_points_votes__points'), - _vote_std=StdDev('problem_points_votes__points'), - _vote_cnt=Count('problem_points_votes__points') + _vote_mean=Avg("problem_points_votes__points"), + _vote_std=StdDev("problem_points_votes__points"), + _vote_cnt=Count("problem_points_votes__points"), ) - if request.user.has_perm('judge.edit_all_problem'): + if request.user.has_perm("judge.edit_all_problem"): return queryset access = Q() - if request.user.has_perm('judge.edit_public_problem'): + if request.user.has_perm("judge.edit_public_problem"): access |= Q(is_public=True) - if request.user.has_perm('judge.edit_own_problem'): - access |= Q(authors__id=request.profile.id) | Q(curators__id=request.profile.id) + if request.user.has_perm("judge.edit_own_problem"): + access |= Q(authors__id=request.profile.id) | Q( + curators__id=request.profile.id + ) return queryset.filter(access).distinct() if access else queryset.none() def has_change_permission(self, request, obj=None): - if request.user.has_perm('judge.edit_all_problem') or obj is None: + if request.user.has_perm("judge.edit_all_problem") or obj is None: return True - if request.user.has_perm('judge.edit_public_problem') and obj.is_public: + if request.user.has_perm("judge.edit_public_problem") and obj.is_public: return True - if not request.user.has_perm('judge.edit_own_problem'): + if not request.user.has_perm("judge.edit_own_problem"): return False return obj.is_editor(request.profile) def formfield_for_manytomany(self, db_field, request=None, **kwargs): - if db_field.name == 'allowed_languages': - kwargs['widget'] = CheckboxSelectMultipleWithSelectAll() - return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs) + if db_field.name == "allowed_languages": + kwargs["widget"] = CheckboxSelectMultipleWithSelectAll() + return super(ProblemAdmin, self).formfield_for_manytomany( + db_field, request, **kwargs + ) def get_form(self, *args, **kwargs): form = super(ProblemAdmin, self).get_form(*args, **kwargs) - form.base_fields['authors'].queryset = Profile.objects.all() + form.base_fields["authors"].queryset = Profile.objects.all() return form def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) - if form.changed_data and any(f in form.changed_data for f in ('is_public', 'points', 'partial')): + if form.changed_data and any( + f in form.changed_data for f in ("is_public", "points", "partial") + ): self._rescore(request, obj.id) def construct_change_message(self, request, form, *args, **kwargs): - if form.cleaned_data.get('change_message'): - return form.cleaned_data['change_message'] - return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs) + if form.cleaned_data.get("change_message"): + return form.cleaned_data["change_message"] + return super(ProblemAdmin, self).construct_change_message( + request, form, *args, **kwargs + ) def vote_mean(self, obj): return round(obj._vote_mean, 1) if obj._vote_mean is not None else None - vote_mean.admin_order_field = '_vote_mean' + + vote_mean.admin_order_field = "_vote_mean" def vote_std(self, obj): return round(obj._vote_std, 1) if obj._vote_std is not None else None - vote_std.admin_order_field = '_vote_std' + + vote_std.admin_order_field = "_vote_std" def vote_cnt(self, obj): return obj._vote_cnt - vote_cnt.admin_order_field = '_vote_cnt' + + vote_cnt.admin_order_field = "_vote_cnt" def vote_median(self, obj): - votes = obj.problem_points_votes.values_list('points', flat=True) - return statistics.median(votes) if votes else None + votes = obj.problem_points_votes.values_list("points", flat=True) + return statistics.median(votes) if votes else None class ProblemPointsVoteAdmin(admin.ModelAdmin): - list_display = ('vote_points', 'voter', 'voter_rating', 'voter_point', 'problem_name', 'problem_code', 'problem_points') - search_fields = ('voter__user__username', 'problem__code', 'problem__name') - readonly_fields = ('voter', 'problem', 'problem_code', 'problem_points', 'voter_rating', 'voter_point') + list_display = ( + "vote_points", + "voter", + "voter_rating", + "voter_point", + "problem_name", + "problem_code", + "problem_points", + ) + search_fields = ("voter__user__username", "problem__code", "problem__name") + readonly_fields = ( + "voter", + "problem", + "problem_code", + "problem_points", + "voter_rating", + "voter_point", + ) def has_change_permission(self, request, obj=None): if obj is None: - return request.user.has_perm('judge.edit_own_problem') + return request.user.has_perm("judge.edit_own_problem") return obj.problem.is_editable_by(request.user) def lookup_allowed(self, key, value): @@ -278,29 +397,35 @@ class ProblemPointsVoteAdmin(admin.ModelAdmin): def problem_code(self, obj): return obj.problem.code - problem_code.short_description = _('Problem code') - problem_code.admin_order_field = 'problem__code' + + problem_code.short_description = _("Problem code") + problem_code.admin_order_field = "problem__code" def problem_points(self, obj): return obj.problem.points - problem_points.short_description = _('Points') - problem_points.admin_order_field = 'problem__points' + + problem_points.short_description = _("Points") + problem_points.admin_order_field = "problem__points" def problem_name(self, obj): return obj.problem.name - problem_name.short_description = _('Problem name') - problem_name.admin_order_field = 'problem__name' + + problem_name.short_description = _("Problem name") + problem_name.admin_order_field = "problem__name" def voter_rating(self, obj): return obj.voter.rating - voter_rating.short_description = _('Voter rating') - voter_rating.admin_order_field = 'voter__rating' + + voter_rating.short_description = _("Voter rating") + voter_rating.admin_order_field = "voter__rating" def voter_point(self, obj): return round(obj.voter.performance_points) - voter_point.short_description = _('Voter point') - voter_point.admin_order_field = 'voter__performance_points' + + voter_point.short_description = _("Voter point") + voter_point.admin_order_field = "voter__performance_points" def vote_points(self, obj): return obj.points - vote_points.short_description = _('Vote') \ No newline at end of file + + vote_points.short_description = _("Vote") diff --git a/judge/admin/profile.py b/judge/admin/profile.py index 02a1f71..d19a80b 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -12,30 +12,40 @@ from judge.widgets import AdminPagedownWidget, AdminSelect2Widget class ProfileForm(ModelForm): def __init__(self, *args, **kwargs): super(ProfileForm, self).__init__(*args, **kwargs) - if 'current_contest' in self.base_fields: + if "current_contest" in self.base_fields: # form.fields['current_contest'] does not exist when the user has only view permission on the model. - self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \ - .only('contest__name', 'user_id', 'virtual') - self.fields['current_contest'].label_from_instance = \ - lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name + self.fields[ + "current_contest" + ].queryset = self.instance.contest_history.select_related("contest").only( + "contest__name", "user_id", "virtual" + ) + self.fields["current_contest"].label_from_instance = ( + lambda obj: "%s v%d" % (obj.contest.name, obj.virtual) + if obj.virtual + else obj.contest.name + ) class Meta: widgets = { - 'timezone': AdminSelect2Widget, - 'language': AdminSelect2Widget, - 'ace_theme': AdminSelect2Widget, - 'current_contest': AdminSelect2Widget, + "timezone": AdminSelect2Widget, + "language": AdminSelect2Widget, + "ace_theme": AdminSelect2Widget, + "current_contest": AdminSelect2Widget, } if AdminPagedownWidget is not None: - widgets['about'] = AdminPagedownWidget + widgets["about"] = AdminPagedownWidget class TimezoneFilter(admin.SimpleListFilter): - title = _('timezone') - parameter_name = 'timezone' + title = _("timezone") + parameter_name = "timezone" def lookups(self, request, model_admin): - return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone') + return ( + Profile.objects.values_list("timezone", "timezone") + .distinct() + .order_by("timezone") + ) def queryset(self, request, queryset): if self.value() is None: @@ -44,75 +54,116 @@ class TimezoneFilter(admin.SimpleListFilter): class ProfileAdmin(VersionAdmin): - fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme', - 'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'is_banned_problem_voting', 'notes', 'is_totp_enabled', 'user_script', - 'current_contest') - readonly_fields = ('user',) - list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full', - 'date_joined', 'last_access', 'ip', 'show_public') - ordering = ('user__username',) - search_fields = ('user__username', 'ip', 'user__email') - list_filter = ('language', TimezoneFilter) - actions = ('recalculate_points',) + fields = ( + "user", + "display_rank", + "about", + "organizations", + "timezone", + "language", + "ace_theme", + "math_engine", + "last_access", + "ip", + "mute", + "is_unlisted", + "is_banned_problem_voting", + "notes", + "is_totp_enabled", + "user_script", + "current_contest", + ) + readonly_fields = ("user",) + list_display = ( + "admin_user_admin", + "email", + "is_totp_enabled", + "timezone_full", + "date_joined", + "last_access", + "ip", + "show_public", + ) + ordering = ("user__username",) + search_fields = ("user__username", "ip", "user__email") + list_filter = ("language", TimezoneFilter) + actions = ("recalculate_points",) actions_on_top = True actions_on_bottom = True form = ProfileForm def get_queryset(self, request): - return super(ProfileAdmin, self).get_queryset(request).select_related('user') + return super(ProfileAdmin, self).get_queryset(request).select_related("user") def get_fields(self, request, obj=None): - if request.user.has_perm('judge.totp'): + if request.user.has_perm("judge.totp"): fields = list(self.fields) - fields.insert(fields.index('is_totp_enabled') + 1, 'totp_key') + fields.insert(fields.index("is_totp_enabled") + 1, "totp_key") return tuple(fields) else: return self.fields def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields - if not request.user.has_perm('judge.totp'): - fields += ('is_totp_enabled',) + if not request.user.has_perm("judge.totp"): + fields += ("is_totp_enabled",) return fields def show_public(self, obj): - return format_html('{1}', - obj.get_absolute_url(), gettext('View on site')) - show_public.short_description = '' + return format_html( + '{1}', + obj.get_absolute_url(), + gettext("View on site"), + ) + + show_public.short_description = "" def admin_user_admin(self, obj): return obj.username - admin_user_admin.admin_order_field = 'user__username' - admin_user_admin.short_description = _('User') + + admin_user_admin.admin_order_field = "user__username" + admin_user_admin.short_description = _("User") def email(self, obj): return obj.user.email - email.admin_order_field = 'user__email' - email.short_description = _('Email') + + email.admin_order_field = "user__email" + email.short_description = _("Email") def timezone_full(self, obj): return obj.timezone - timezone_full.admin_order_field = 'timezone' - timezone_full.short_description = _('Timezone') + + timezone_full.admin_order_field = "timezone" + timezone_full.short_description = _("Timezone") def date_joined(self, obj): return obj.user.date_joined - date_joined.admin_order_field = 'user__date_joined' - date_joined.short_description = _('date joined') + + date_joined.admin_order_field = "user__date_joined" + date_joined.short_description = _("date joined") def recalculate_points(self, request, queryset): count = 0 for profile in queryset: profile.calculate_points() count += 1 - self.message_user(request, ungettext('%d user have scores recalculated.', - '%d users have scores recalculated.', - count) % count) - recalculate_points.short_description = _('Recalculate scores') + self.message_user( + request, + ungettext( + "%d user have scores recalculated.", + "%d users have scores recalculated.", + count, + ) + % count, + ) + + recalculate_points.short_description = _("Recalculate scores") def get_form(self, request, obj=None, **kwargs): form = super(ProfileAdmin, self).get_form(request, obj, **kwargs) - if 'user_script' in form.base_fields: + if "user_script" in form.base_fields: # form.base_fields['user_script'] does not exist when the user has only view permission on the model. - form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme) + form.base_fields["user_script"].widget = AceWidget( + "javascript", request.profile.ace_theme + ) return form diff --git a/judge/admin/runtime.py b/judge/admin/runtime.py index e9f44ec..791083a 100644 --- a/judge/admin/runtime.py +++ b/judge/admin/runtime.py @@ -16,41 +16,63 @@ from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget class LanguageForm(ModelForm): problems = ModelMultipleChoiceField( - label=_('Disallowed problems'), + label=_("Disallowed problems"), queryset=Problem.objects.all(), required=False, - help_text=_('These problems are NOT allowed to be submitted in this language'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + help_text=_("These problems are NOT allowed to be submitted in this language"), + widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"), + ) class Meta: if AdminPagedownWidget is not None: - widgets = {'description': AdminPagedownWidget} + widgets = {"description": AdminPagedownWidget} class LanguageAdmin(VersionAdmin): - fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'description', - 'template', 'problems') - list_display = ('key', 'name', 'common_name', 'info') + fields = ( + "key", + "name", + "short_name", + "common_name", + "ace", + "pygments", + "info", + "description", + "template", + "problems", + ) + list_display = ("key", "name", "common_name", "info") form = LanguageForm def save_model(self, request, obj, form, change): super(LanguageAdmin, self).save_model(request, obj, form, change) - obj.problem_set.set(Problem.objects.exclude(id__in=form.cleaned_data['problems'].values('id'))) + obj.problem_set.set( + Problem.objects.exclude(id__in=form.cleaned_data["problems"].values("id")) + ) def get_form(self, request, obj=None, **kwargs): - self.form.base_fields['problems'].initial = \ - Problem.objects.exclude(id__in=obj.problem_set.values('id')).values_list('pk', flat=True) if obj else [] + self.form.base_fields["problems"].initial = ( + Problem.objects.exclude(id__in=obj.problem_set.values("id")).values_list( + "pk", flat=True + ) + if obj + else [] + ) form = super(LanguageAdmin, self).get_form(request, obj, **kwargs) if obj is not None: - form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme) + form.base_fields["template"].widget = AceWidget( + obj.ace, request.profile.ace_theme + ) return form class GenerateKeyTextInput(TextInput): def render(self, name, value, attrs=None, renderer=None): text = super(TextInput, self).render(name, value, attrs) - return mark_safe(text + format_html( - '''\ + return mark_safe( + text + + format_html( + """\ Regenerate -''', name)) +""", + name, + ) + ) class JudgeAdminForm(ModelForm): class Meta: - widgets = {'auth_key': GenerateKeyTextInput} + widgets = {"auth_key": GenerateKeyTextInput} if AdminPagedownWidget is not None: - widgets['description'] = AdminPagedownWidget + widgets["description"] = AdminPagedownWidget class JudgeAdmin(VersionAdmin): form = JudgeAdminForm - readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems') - fieldsets = ( - (None, {'fields': ('name', 'auth_key', 'is_blocked')}), - (_('Description'), {'fields': ('description',)}), - (_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}), - (_('Capabilities'), {'fields': ('runtimes', 'problems')}), + readonly_fields = ( + "created", + "online", + "start_time", + "ping", + "load", + "last_ip", + "runtimes", + "problems", ) - list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip') - ordering = ['-online', 'name'] + fieldsets = ( + (None, {"fields": ("name", "auth_key", "is_blocked")}), + (_("Description"), {"fields": ("description",)}), + ( + _("Information"), + {"fields": ("created", "online", "last_ip", "start_time", "ping", "load")}, + ), + (_("Capabilities"), {"fields": ("runtimes", "problems")}), + ) + list_display = ("name", "online", "start_time", "ping", "load", "last_ip") + ordering = ["-online", "name"] def get_urls(self): - return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'), - url(r'^(\d+)/terminate/$', self.terminate_view, name='judge_judge_terminate')] + - super(JudgeAdmin, self).get_urls()) + return [ + url( + r"^(\d+)/disconnect/$", + self.disconnect_view, + name="judge_judge_disconnect", + ), + url( + r"^(\d+)/terminate/$", self.terminate_view, name="judge_judge_terminate" + ), + ] + super(JudgeAdmin, self).get_urls() def disconnect_judge(self, id, force=False): judge = get_object_or_404(Judge, id=id) judge.disconnect(force=force) - return HttpResponseRedirect(reverse('admin:judge_judge_changelist')) + return HttpResponseRedirect(reverse("admin:judge_judge_changelist")) def disconnect_view(self, request, id): return self.disconnect_judge(id) @@ -105,7 +149,7 @@ class JudgeAdmin(VersionAdmin): def get_readonly_fields(self, request, obj=None): if obj is not None and obj.online: - return self.readonly_fields + ('name',) + return self.readonly_fields + ("name",) return self.readonly_fields def has_delete_permission(self, request, obj=None): @@ -116,5 +160,5 @@ class JudgeAdmin(VersionAdmin): if AdminPagedownWidget is not None: formfield_overrides = { - TextField: {'widget': AdminPagedownWidget}, + TextField: {"widget": AdminPagedownWidget}, } diff --git a/judge/admin/submission.py b/judge/admin/submission.py index 2689a76..f621783 100644 --- a/judge/admin/submission.py +++ b/judge/admin/submission.py @@ -13,239 +13,367 @@ from django.utils.html import format_html from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext from django_ace import AceWidget -from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \ - SubmissionSource, SubmissionTestCase +from judge.models import ( + ContestParticipation, + ContestProblem, + ContestSubmission, + Profile, + Submission, + SubmissionSource, + SubmissionTestCase, +) from judge.utils.raw_sql import use_straight_join class SubmissionStatusFilter(admin.SimpleListFilter): - parameter_name = title = 'status' - __lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS + parameter_name = title = "status" + __lookups = ( + ("None", _("None")), + ("NotDone", _("Not done")), + ("EX", _("Exceptional")), + ) + Submission.STATUS __handles = set(map(itemgetter(0), Submission.STATUS)) def lookups(self, request, model_admin): return self.__lookups def queryset(self, request, queryset): - if self.value() == 'None': + if self.value() == "None": return queryset.filter(status=None) - elif self.value() == 'NotDone': - return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB']) - elif self.value() == 'EX': - return queryset.exclude(status__in=['D', 'CE', 'G', 'AB']) + elif self.value() == "NotDone": + return queryset.exclude(status__in=["D", "IE", "CE", "AB"]) + elif self.value() == "EX": + return queryset.exclude(status__in=["D", "CE", "G", "AB"]) elif self.value() in self.__handles: return queryset.filter(status=self.value()) class SubmissionResultFilter(admin.SimpleListFilter): - parameter_name = title = 'result' - __lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT + parameter_name = title = "result" + __lookups = (("None", _("None")), ("BAD", _("Unaccepted"))) + Submission.RESULT __handles = set(map(itemgetter(0), Submission.RESULT)) def lookups(self, request, model_admin): return self.__lookups def queryset(self, request, queryset): - if self.value() == 'None': + if self.value() == "None": return queryset.filter(result=None) - elif self.value() == 'BAD': - return queryset.exclude(result='AC') + elif self.value() == "BAD": + return queryset.exclude(result="AC") elif self.value() in self.__handles: return queryset.filter(result=self.value()) class SubmissionTestCaseInline(admin.TabularInline): - fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total') - readonly_fields = ('case', 'batch', 'total') + fields = ("case", "batch", "status", "time", "memory", "points", "total") + readonly_fields = ("case", "batch", "total") model = SubmissionTestCase can_delete = False max_num = 0 class ContestSubmissionInline(admin.StackedInline): - fields = ('problem', 'participation', 'points') + fields = ("problem", "participation", "points") model = ContestSubmission def get_formset(self, request, obj=None, **kwargs): - kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj) + kwargs["formfield_callback"] = partial( + self.formfield_for_dbfield, request=request, obj=obj + ) return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs) def formfield_for_dbfield(self, db_field, **kwargs): - submission = kwargs.pop('obj', None) + submission = kwargs.pop("obj", None) label = None if submission: - if db_field.name == 'participation': - kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user, - contest__problems=submission.problem) \ - .only('id', 'contest__name') + if db_field.name == "participation": + kwargs["queryset"] = ContestParticipation.objects.filter( + user=submission.user, contest__problems=submission.problem + ).only("id", "contest__name") def label(obj): return obj.contest.name - elif db_field.name == 'problem': - kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \ - .only('id', 'problem__name', 'contest__name') + + elif db_field.name == "problem": + kwargs["queryset"] = ContestProblem.objects.filter( + problem=submission.problem + ).only("id", "problem__name", "contest__name") def label(obj): - return pgettext('contest problem', '%(problem)s in %(contest)s') % { - 'problem': obj.problem.name, 'contest': obj.contest.name, + return pgettext("contest problem", "%(problem)s in %(contest)s") % { + "problem": obj.problem.name, + "contest": obj.contest.name, } - field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs) + + field = super(ContestSubmissionInline, self).formfield_for_dbfield( + db_field, **kwargs + ) if label is not None: field.label_from_instance = label return field class SubmissionSourceInline(admin.StackedInline): - fields = ('source',) + fields = ("source",) model = SubmissionSource can_delete = False extra = 0 def get_formset(self, request, obj=None, **kwargs): - kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace, - theme=request.profile.ace_theme) + kwargs.setdefault("widgets", {})["source"] = AceWidget( + mode=obj and obj.language.ace, theme=request.profile.ace_theme + ) return super().get_formset(request, obj, **kwargs) class SubmissionAdmin(admin.ModelAdmin): - readonly_fields = ('user', 'problem', 'date', 'judged_date') - fields = ('user', 'problem', 'date', 'judged_date', 'time', 'memory', 'points', 'language', 'status', 'result', - 'case_points', 'case_total', 'judged_on', 'error') - actions = ('judge', 'recalculate_score') - list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory', - 'points', 'language_column', 'status', 'result', 'judge_column') - list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter) - search_fields = ('problem__code', 'problem__name', 'user__user__username') + readonly_fields = ("user", "problem", "date", "judged_date") + fields = ( + "user", + "problem", + "date", + "judged_date", + "time", + "memory", + "points", + "language", + "status", + "result", + "case_points", + "case_total", + "judged_on", + "error", + ) + actions = ("judge", "recalculate_score") + list_display = ( + "id", + "problem_code", + "problem_name", + "user_column", + "execution_time", + "pretty_memory", + "points", + "language_column", + "status", + "result", + "judge_column", + ) + list_filter = ("language", SubmissionStatusFilter, SubmissionResultFilter) + search_fields = ("problem__code", "problem__name", "user__user__username") actions_on_top = True actions_on_bottom = True - inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline] + inlines = [ + SubmissionSourceInline, + SubmissionTestCaseInline, + ContestSubmissionInline, + ] def get_queryset(self, request): - queryset = Submission.objects.select_related('problem', 'user__user', 'language').only( - 'problem__code', 'problem__name', 'user__user__username', 'language__name', - 'time', 'memory', 'points', 'status', 'result', + queryset = Submission.objects.select_related( + "problem", "user__user", "language" + ).only( + "problem__code", + "problem__name", + "user__user__username", + "language__name", + "time", + "memory", + "points", + "status", + "result", ) use_straight_join(queryset) - if not request.user.has_perm('judge.edit_all_problem'): + if not request.user.has_perm("judge.edit_all_problem"): id = request.profile.id - queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct() + queryset = queryset.filter( + Q(problem__authors__id=id) | Q(problem__curators__id=id) + ).distinct() return queryset def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): - if not request.user.has_perm('judge.edit_own_problem'): + if not request.user.has_perm("judge.edit_own_problem"): return False - if request.user.has_perm('judge.edit_all_problem') or obj is None: + if request.user.has_perm("judge.edit_all_problem") or obj is None: return True return obj.problem.is_editor(request.profile) def lookup_allowed(self, key, value): - return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',) + return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ( + "problem__code", + ) def judge(self, request, queryset): - if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'): - self.message_user(request, gettext('You do not have the permission to rejudge submissions.'), - level=messages.ERROR) + if not request.user.has_perm( + "judge.rejudge_submission" + ) or not request.user.has_perm("judge.edit_own_problem"): + self.message_user( + request, + gettext("You do not have the permission to rejudge submissions."), + level=messages.ERROR, + ) return - queryset = queryset.order_by('id') - if not request.user.has_perm('judge.rejudge_submission_lot') and \ - queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT: - self.message_user(request, gettext('You do not have the permission to rejudge THAT many submissions.'), - level=messages.ERROR) + queryset = queryset.order_by("id") + if ( + not request.user.has_perm("judge.rejudge_submission_lot") + and queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT + ): + self.message_user( + request, + gettext( + "You do not have the permission to rejudge THAT many submissions." + ), + level=messages.ERROR, + ) return - if not request.user.has_perm('judge.edit_all_problem'): + if not request.user.has_perm("judge.edit_all_problem"): id = request.profile.id - queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)) + queryset = queryset.filter( + Q(problem__authors__id=id) | Q(problem__curators__id=id) + ) judged = len(queryset) for model in queryset: model.judge(rejudge=True, batch_rejudge=True) - self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.', - '%d submissions were successfully scheduled for rejudging.', - judged) % judged) - judge.short_description = _('Rejudge the selected submissions') + self.message_user( + request, + ungettext( + "%d submission was successfully scheduled for rejudging.", + "%d submissions were successfully scheduled for rejudging.", + judged, + ) + % judged, + ) + + judge.short_description = _("Rejudge the selected submissions") def recalculate_score(self, request, queryset): - if not request.user.has_perm('judge.rejudge_submission'): - self.message_user(request, gettext('You do not have the permission to rejudge submissions.'), - level=messages.ERROR) + if not request.user.has_perm("judge.rejudge_submission"): + self.message_user( + request, + gettext("You do not have the permission to rejudge submissions."), + level=messages.ERROR, + ) return - submissions = list(queryset.defer(None).select_related(None).select_related('problem') - .only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points')) + submissions = list( + queryset.defer(None) + .select_related(None) + .select_related("problem") + .only( + "points", + "case_points", + "case_total", + "problem__partial", + "problem__points", + ) + ) for submission in submissions: - submission.points = round(submission.case_points / submission.case_total * submission.problem.points - if submission.case_total else 0, 1) - if not submission.problem.partial and submission.points < submission.problem.points: + submission.points = round( + submission.case_points + / submission.case_total + * submission.problem.points + if submission.case_total + else 0, + 1, + ) + if ( + not submission.problem.partial + and submission.points < submission.problem.points + ): submission.points = 0 submission.save() submission.update_contest() - for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()): + for profile in Profile.objects.filter( + id__in=queryset.values_list("user_id", flat=True).distinct() + ): profile.calculate_points() - cache.delete('user_complete:%d' % profile.id) - cache.delete('user_attempted:%d' % profile.id) + cache.delete("user_complete:%d" % profile.id) + cache.delete("user_attempted:%d" % profile.id) for participation in ContestParticipation.objects.filter( - id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'): + id__in=queryset.values_list("contest__participation_id") + ).prefetch_related("contest"): participation.recompute_results() - self.message_user(request, ungettext('%d submission were successfully rescored.', - '%d submissions were successfully rescored.', - len(submissions)) % len(submissions)) - recalculate_score.short_description = _('Rescore the selected submissions') + self.message_user( + request, + ungettext( + "%d submission were successfully rescored.", + "%d submissions were successfully rescored.", + len(submissions), + ) + % len(submissions), + ) + + recalculate_score.short_description = _("Rescore the selected submissions") def problem_code(self, obj): return obj.problem.code - problem_code.short_description = _('Problem code') - problem_code.admin_order_field = 'problem__code' + + problem_code.short_description = _("Problem code") + problem_code.admin_order_field = "problem__code" def problem_name(self, obj): return obj.problem.name - problem_name.short_description = _('Problem name') - problem_name.admin_order_field = 'problem__name' + + problem_name.short_description = _("Problem name") + problem_name.admin_order_field = "problem__name" def user_column(self, obj): return obj.user.user.username - user_column.admin_order_field = 'user__user__username' - user_column.short_description = _('User') + + user_column.admin_order_field = "user__user__username" + user_column.short_description = _("User") def execution_time(self, obj): - return round(obj.time, 2) if obj.time is not None else 'None' - execution_time.short_description = _('Time') - execution_time.admin_order_field = 'time' + return round(obj.time, 2) if obj.time is not None else "None" + + execution_time.short_description = _("Time") + execution_time.admin_order_field = "time" def pretty_memory(self, obj): memory = obj.memory if memory is None: - return gettext('None') + return gettext("None") if memory < 1000: - return gettext('%d KB') % memory + return gettext("%d KB") % memory else: - return gettext('%.2f MB') % (memory / 1024) - pretty_memory.admin_order_field = 'memory' - pretty_memory.short_description = _('Memory') + return gettext("%.2f MB") % (memory / 1024) + + pretty_memory.admin_order_field = "memory" + pretty_memory.short_description = _("Memory") def language_column(self, obj): return obj.language.name - language_column.admin_order_field = 'language__name' - language_column.short_description = _('Language') + + language_column.admin_order_field = "language__name" + language_column.short_description = _("Language") def judge_column(self, obj): - return format_html('', obj.id) - judge_column.short_description = '' + return format_html( + '', + obj.id, + ) + + judge_column.short_description = "" def get_urls(self): return [ - url(r'^(\d+)/judge/$', self.judge_view, name='judge_submission_rejudge'), + url(r"^(\d+)/judge/$", self.judge_view, name="judge_submission_rejudge"), ] + super(SubmissionAdmin, self).get_urls() def judge_view(self, request, id): - if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'): + if not request.user.has_perm( + "judge.rejudge_submission" + ) or not request.user.has_perm("judge.edit_own_problem"): raise PermissionDenied() submission = get_object_or_404(Submission, id=id) - if not request.user.has_perm('judge.edit_all_problem') and \ - not submission.problem.is_editor(request.profile): + if not request.user.has_perm( + "judge.edit_all_problem" + ) and not submission.problem.is_editor(request.profile): raise PermissionDenied() submission.judge(rejudge=True) - return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/')) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) diff --git a/judge/admin/taxon.py b/judge/admin/taxon.py index c707144..4335efe 100644 --- a/judge/admin/taxon.py +++ b/judge/admin/taxon.py @@ -8,45 +8,51 @@ from judge.widgets import AdminHeavySelect2MultipleWidget class ProblemGroupForm(ModelForm): problems = ModelMultipleChoiceField( - label=_('Included problems'), + label=_("Included problems"), queryset=Problem.objects.all(), required=False, - help_text=_('These problems are included in this group of problems'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + help_text=_("These problems are included in this group of problems"), + widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"), + ) class ProblemGroupAdmin(admin.ModelAdmin): - fields = ('name', 'full_name', 'problems') + fields = ("name", "full_name", "problems") form = ProblemGroupForm def save_model(self, request, obj, form, change): super(ProblemGroupAdmin, self).save_model(request, obj, form, change) - obj.problem_set.set(form.cleaned_data['problems']) + obj.problem_set.set(form.cleaned_data["problems"]) obj.save() def get_form(self, request, obj=None, **kwargs): - self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else [] + self.form.base_fields["problems"].initial = ( + [o.pk for o in obj.problem_set.all()] if obj else [] + ) return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs) class ProblemTypeForm(ModelForm): problems = ModelMultipleChoiceField( - label=_('Included problems'), + label=_("Included problems"), queryset=Problem.objects.all(), required=False, - help_text=_('These problems are included in this type of problems'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + help_text=_("These problems are included in this type of problems"), + widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"), + ) class ProblemTypeAdmin(admin.ModelAdmin): - fields = ('name', 'full_name', 'problems') + fields = ("name", "full_name", "problems") form = ProblemTypeForm def save_model(self, request, obj, form, change): super(ProblemTypeAdmin, self).save_model(request, obj, form, change) - obj.problem_set.set(form.cleaned_data['problems']) + obj.problem_set.set(form.cleaned_data["problems"]) obj.save() def get_form(self, request, obj=None, **kwargs): - self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else [] + self.form.base_fields["problems"].initial = ( + [o.pk for o in obj.problem_set.all()] if obj else [] + ) return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs) diff --git a/judge/admin/ticket.py b/judge/admin/ticket.py index a5c5a5e..f7495dd 100644 --- a/judge/admin/ticket.py +++ b/judge/admin/ticket.py @@ -4,36 +4,56 @@ from django.forms import ModelForm from django.urls import reverse_lazy from judge.models import TicketMessage -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + HeavyPreviewAdminPageDownWidget, +) class TicketMessageForm(ModelForm): class Meta: widgets = { - 'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}), + "user": AdminHeavySelect2Widget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview')) + widgets["body"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("ticket_preview") + ) class TicketMessageInline(StackedInline): model = TicketMessage form = TicketMessageForm - fields = ('user', 'body') + fields = ("user", "body") class TicketForm(ModelForm): class Meta: widgets = { - 'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), + "user": AdminHeavySelect2Widget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "assignees": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), } class TicketAdmin(ModelAdmin): - fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes') - readonly_fields = ('time',) - list_display = ('title', 'user', 'time', 'linked_item') + fields = ( + "title", + "time", + "user", + "assignees", + "content_type", + "object_id", + "notes", + ) + readonly_fields = ("time",) + list_display = ("title", "user", "time", "linked_item") inlines = [TicketMessageInline] form = TicketForm - date_hierarchy = 'time' + date_hierarchy = "time" diff --git a/judge/admin/volunteer.py b/judge/admin/volunteer.py index 58ebc86..a26cfd6 100644 --- a/judge/admin/volunteer.py +++ b/judge/admin/volunteer.py @@ -5,14 +5,30 @@ from django.utils.translation import gettext, gettext_lazy as _, ungettext from judge.models import VolunteerProblemVote + class VolunteerProblemVoteAdmin(admin.ModelAdmin): - fields = ('voter', 'problem', 'time', 'thinking_points', 'knowledge_points', 'feedback') - readonly_fields = ('time', 'problem', 'voter') - list_display = ('voter', 'problem_link', 'time', 'thinking_points', 'knowledge_points', 'feedback') - date_hierarchy = 'time' + fields = ( + "voter", + "problem", + "time", + "thinking_points", + "knowledge_points", + "feedback", + ) + readonly_fields = ("time", "problem", "voter") + list_display = ( + "voter", + "problem_link", + "time", + "thinking_points", + "knowledge_points", + "feedback", + ) + date_hierarchy = "time" def problem_link(self, obj): - url = reverse('admin:judge_problem_change', args=(obj.problem.id,)) + url = reverse("admin:judge_problem_change", args=(obj.problem.id,)) return format_html(f"{obj.problem.code}") - problem_link.short_description = _('Problem') - problem_link.admin_order_field = 'problem__code' \ No newline at end of file + + problem_link.short_description = _("Problem") + problem_link.admin_order_field = "problem__code" diff --git a/judge/apps.py b/judge/apps.py index 96ec022..f223d52 100644 --- a/judge/apps.py +++ b/judge/apps.py @@ -4,8 +4,8 @@ from django.utils.translation import gettext_lazy class JudgeAppConfig(AppConfig): - name = 'judge' - verbose_name = gettext_lazy('Online Judge') + name = "judge" + verbose_name = gettext_lazy("Online Judge") def ready(self): # WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE, diff --git a/judge/bridge/base_handler.py b/judge/bridge/base_handler.py index c3fbd40..369a8e1 100644 --- a/judge/bridge/base_handler.py +++ b/judge/bridge/base_handler.py @@ -8,9 +8,9 @@ from netaddr import IPGlob, IPSet from judge.utils.unicode import utf8text -logger = logging.getLogger('judge.bridge') +logger = logging.getLogger("judge.bridge") -size_pack = struct.Struct('!I') +size_pack = struct.Struct("!I") assert size_pack.size == 4 MAX_ALLOWED_PACKET_SIZE = 8 * 1024 * 1024 @@ -20,7 +20,7 @@ def proxy_list(human_readable): globs = [] addrs = [] for item in human_readable: - if '*' in item or '-' in item: + if "*" in item or "-" in item: globs.append(IPGlob(item)) else: addrs.append(item) @@ -43,7 +43,7 @@ class RequestHandlerMeta(type): try: handler.handle() except BaseException: - logger.exception('Error in base packet handling') + logger.exception("Error in base packet handling") raise finally: handler.on_disconnect() @@ -70,8 +70,12 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): def read_sized_packet(self, size, initial=None): if size > MAX_ALLOWED_PACKET_SIZE: - logger.log(logging.WARNING if self._got_packet else logging.INFO, - 'Disconnecting client due to too-large message size (%d bytes): %s', size, self.client_address) + logger.log( + logging.WARNING if self._got_packet else logging.INFO, + "Disconnecting client due to too-large message size (%d bytes): %s", + size, + self.client_address, + ) raise Disconnect() buffer = [] @@ -86,7 +90,7 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): data = self.request.recv(remainder) remainder -= len(data) buffer.append(data) - self._on_packet(b''.join(buffer)) + self._on_packet(b"".join(buffer)) def parse_proxy_protocol(self, line): words = line.split() @@ -94,18 +98,18 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): if len(words) < 2: raise Disconnect() - if words[1] == b'TCP4': + if words[1] == b"TCP4": if len(words) != 6: raise Disconnect() self.client_address = (utf8text(words[2]), utf8text(words[4])) self.server_address = (utf8text(words[3]), utf8text(words[5])) - elif words[1] == b'TCP6': + elif words[1] == b"TCP6": self.client_address = (utf8text(words[2]), utf8text(words[4]), 0, 0) self.server_address = (utf8text(words[3]), utf8text(words[5]), 0, 0) - elif words[1] != b'UNKNOWN': + elif words[1] != b"UNKNOWN": raise Disconnect() - def read_size(self, buffer=b''): + def read_size(self, buffer=b""): while len(buffer) < size_pack.size: recv = self.request.recv(size_pack.size - len(buffer)) if not recv: @@ -113,9 +117,9 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): buffer += recv return size_pack.unpack(buffer)[0] - def read_proxy_header(self, buffer=b''): + def read_proxy_header(self, buffer=b""): # Max line length for PROXY protocol is 107, and we received 4 already. - while b'\r\n' not in buffer: + while b"\r\n" not in buffer: if len(buffer) > 107: raise Disconnect() data = self.request.recv(107) @@ -125,7 +129,7 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): return buffer def _on_packet(self, data): - decompressed = zlib.decompress(data).decode('utf-8') + decompressed = zlib.decompress(data).decode("utf-8") self._got_packet = True self.on_packet(decompressed) @@ -145,8 +149,10 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): try: tag = self.read_size() self._initial_tag = size_pack.pack(tag) - if self.client_address[0] in self.proxies and self._initial_tag == b'PROX': - proxy, _, remainder = self.read_proxy_header(self._initial_tag).partition(b'\r\n') + if self.client_address[0] in self.proxies and self._initial_tag == b"PROX": + proxy, _, remainder = self.read_proxy_header( + self._initial_tag + ).partition(b"\r\n") self.parse_proxy_protocol(proxy) while remainder: @@ -154,8 +160,8 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): self.read_sized_packet(self.read_size(remainder)) break - size = size_pack.unpack(remainder[:size_pack.size])[0] - remainder = remainder[size_pack.size:] + size = size_pack.unpack(remainder[: size_pack.size])[0] + remainder = remainder[size_pack.size :] if len(remainder) <= size: self.read_sized_packet(size, remainder) break @@ -171,25 +177,36 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): return except zlib.error: if self._got_packet: - logger.warning('Encountered zlib error during packet handling, disconnecting client: %s', - self.client_address, exc_info=True) + logger.warning( + "Encountered zlib error during packet handling, disconnecting client: %s", + self.client_address, + exc_info=True, + ) else: - logger.info('Potentially wrong protocol (zlib error): %s: %r', self.client_address, self._initial_tag, - exc_info=True) + logger.info( + "Potentially wrong protocol (zlib error): %s: %r", + self.client_address, + self._initial_tag, + exc_info=True, + ) except socket.timeout: if self._got_packet: - logger.info('Socket timed out: %s', self.client_address) + logger.info("Socket timed out: %s", self.client_address) self.on_timeout() else: - logger.info('Potentially wrong protocol: %s: %r', self.client_address, self._initial_tag) + logger.info( + "Potentially wrong protocol: %s: %r", + self.client_address, + self._initial_tag, + ) except socket.error as e: # When a gevent socket is shutdown, gevent cancels all waits, causing recv to raise cancel_wait_ex. - if e.__class__.__name__ == 'cancel_wait_ex': + if e.__class__.__name__ == "cancel_wait_ex": return raise def send(self, data): - compressed = zlib.compress(data.encode('utf-8')) + compressed = zlib.compress(data.encode("utf-8")) self.request.sendall(size_pack.pack(len(compressed)) + compressed) def close(self): diff --git a/judge/bridge/daemon.py b/judge/bridge/daemon.py index ce8a702..b9988ce 100644 --- a/judge/bridge/daemon.py +++ b/judge/bridge/daemon.py @@ -11,7 +11,7 @@ from judge.bridge.judge_list import JudgeList from judge.bridge.server import Server from judge.models import Judge, Submission -logger = logging.getLogger('judge.bridge') +logger = logging.getLogger("judge.bridge") def reset_judges(): @@ -20,12 +20,17 @@ def reset_judges(): def judge_daemon(): reset_judges() - Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS) \ - .update(status='IE', result='IE', error=None) + Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS).update( + status="IE", result="IE", error=None + ) judges = JudgeList() - judge_server = Server(settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges)) - django_server = Server(settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges)) + judge_server = Server( + settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges) + ) + django_server = Server( + settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges) + ) threading.Thread(target=django_server.serve_forever).start() threading.Thread(target=judge_server.serve_forever).start() @@ -33,7 +38,7 @@ def judge_daemon(): stop = threading.Event() def signal_handler(signum, _): - logger.info('Exiting due to %s', signal.Signals(signum).name) + logger.info("Exiting due to %s", signal.Signals(signum).name) stop.set() signal.signal(signal.SIGINT, signal_handler) diff --git a/judge/bridge/django_handler.py b/judge/bridge/django_handler.py index b284b68..f72aba1 100644 --- a/judge/bridge/django_handler.py +++ b/judge/bridge/django_handler.py @@ -4,8 +4,8 @@ import struct from judge.bridge.base_handler import Disconnect, ZlibPacketHandler -logger = logging.getLogger('judge.bridge') -size_pack = struct.Struct('!I') +logger = logging.getLogger("judge.bridge") +size_pack = struct.Struct("!I") class DjangoHandler(ZlibPacketHandler): @@ -13,47 +13,52 @@ class DjangoHandler(ZlibPacketHandler): super().__init__(request, client_address, server) self.handlers = { - 'submission-request': self.on_submission, - 'terminate-submission': self.on_termination, - 'disconnect-judge': self.on_disconnect_request, + "submission-request": self.on_submission, + "terminate-submission": self.on_termination, + "disconnect-judge": self.on_disconnect_request, } self.judges = judges def send(self, data): - super().send(json.dumps(data, separators=(',', ':'))) + super().send(json.dumps(data, separators=(",", ":"))) def on_packet(self, packet): packet = json.loads(packet) try: - result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet) + result = self.handlers.get(packet.get("name", None), self.on_malformed)( + packet + ) except Exception: - logger.exception('Error in packet handling (Django-facing)') - result = {'name': 'bad-request'} + logger.exception("Error in packet handling (Django-facing)") + result = {"name": "bad-request"} self.send(result) raise Disconnect() def on_submission(self, data): - id = data['submission-id'] - problem = data['problem-id'] - language = data['language'] - source = data['source'] - judge_id = data['judge-id'] - priority = data['priority'] + id = data["submission-id"] + problem = data["problem-id"] + language = data["language"] + source = data["source"] + judge_id = data["judge-id"] + priority = data["priority"] if not self.judges.check_priority(priority): - return {'name': 'bad-request'} + return {"name": "bad-request"} self.judges.judge(id, problem, language, source, judge_id, priority) - return {'name': 'submission-received', 'submission-id': id} + return {"name": "submission-received", "submission-id": id} def on_termination(self, data): - return {'name': 'submission-received', 'judge-aborted': self.judges.abort(data['submission-id'])} + return { + "name": "submission-received", + "judge-aborted": self.judges.abort(data["submission-id"]), + } def on_disconnect_request(self, data): - judge_id = data['judge-id'] - force = data['force'] + judge_id = data["judge-id"] + force = data["force"] self.judges.disconnect(judge_id, force=force) def on_malformed(self, packet): - logger.error('Malformed packet: %s', packet) + logger.error("Malformed packet: %s", packet) def on_close(self): self._to_kill = False diff --git a/judge/bridge/echo_test_client.py b/judge/bridge/echo_test_client.py index 8fec692..801d34e 100644 --- a/judge/bridge/echo_test_client.py +++ b/judge/bridge/echo_test_client.py @@ -4,7 +4,7 @@ import struct import time import zlib -size_pack = struct.Struct('!I') +size_pack = struct.Struct("!I") def open_connection(): @@ -13,69 +13,70 @@ def open_connection(): def zlibify(data): - data = zlib.compress(data.encode('utf-8')) + data = zlib.compress(data.encode("utf-8")) return size_pack.pack(len(data)) + data def dezlibify(data, skip_head=True): if skip_head: - data = data[size_pack.size:] - return zlib.decompress(data).decode('utf-8') + data = data[size_pack.size :] + return zlib.decompress(data).decode("utf-8") def main(): global host, port import argparse + parser = argparse.ArgumentParser() - parser.add_argument('-l', '--host', default='localhost') - parser.add_argument('-p', '--port', default=9999, type=int) + parser.add_argument("-l", "--host", default="localhost") + parser.add_argument("-p", "--port", default=9999, type=int) args = parser.parse_args() host, port = args.host, args.port - print('Opening idle connection:', end=' ') + print("Opening idle connection:", end=" ") s1 = open_connection() - print('Success') - print('Opening hello world connection:', end=' ') + print("Success") + print("Opening hello world connection:", end=" ") s2 = open_connection() - print('Success') - print('Sending Hello, World!', end=' ') - s2.sendall(zlibify('Hello, World!')) - print('Success') - print('Testing blank connection:', end=' ') + print("Success") + print("Sending Hello, World!", end=" ") + s2.sendall(zlibify("Hello, World!")) + print("Success") + print("Testing blank connection:", end=" ") s3 = open_connection() s3.close() - print('Success') + print("Success") result = dezlibify(s2.recv(1024)) - assert result == 'Hello, World!' + assert result == "Hello, World!" print(result) s2.close() - print('Large random data test:', end=' ') + print("Large random data test:", end=" ") s4 = open_connection() - data = os.urandom(1000000).decode('iso-8859-1') - print('Generated', end=' ') + data = os.urandom(1000000).decode("iso-8859-1") + print("Generated", end=" ") s4.sendall(zlibify(data)) - print('Sent', end=' ') - result = b'' + print("Sent", end=" ") + result = b"" while len(result) < size_pack.size: result += s4.recv(1024) - size = size_pack.unpack(result[:size_pack.size])[0] - result = result[size_pack.size:] + size = size_pack.unpack(result[: size_pack.size])[0] + result = result[size_pack.size :] while len(result) < size: result += s4.recv(1024) - print('Received', end=' ') + print("Received", end=" ") assert dezlibify(result, False) == data - print('Success') + print("Success") s4.close() - print('Test malformed connection:', end=' ') + print("Test malformed connection:", end=" ") s5 = open_connection() - s5.sendall(data[:100000].encode('utf-8')) + s5.sendall(data[:100000].encode("utf-8")) s5.close() - print('Success') - print('Waiting for timeout to close idle connection:', end=' ') + print("Success") + print("Waiting for timeout to close idle connection:", end=" ") time.sleep(6) - print('Done') + print("Done") s1.close() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/judge/bridge/echo_test_server.py b/judge/bridge/echo_test_server.py index 59e21fa..2d84e85 100644 --- a/judge/bridge/echo_test_server.py +++ b/judge/bridge/echo_test_server.py @@ -3,19 +3,22 @@ from judge.bridge.base_handler import ZlibPacketHandler class EchoPacketHandler(ZlibPacketHandler): def on_connect(self): - print('New client:', self.client_address) + print("New client:", self.client_address) self.timeout = 5 def on_timeout(self): - print('Inactive client:', self.client_address) + print("Inactive client:", self.client_address) def on_packet(self, data): self.timeout = None - print('Data from %s: %r' % (self.client_address, data[:30] if len(data) > 30 else data)) + print( + "Data from %s: %r" + % (self.client_address, data[:30] if len(data) > 30 else data) + ) self.send(data) def on_disconnect(self): - print('Closed client:', self.client_address) + print("Closed client:", self.client_address) def main(): @@ -23,9 +26,9 @@ def main(): from judge.bridge.server import Server parser = argparse.ArgumentParser() - parser.add_argument('-l', '--host', action='append') - parser.add_argument('-p', '--port', type=int, action='append') - parser.add_argument('-P', '--proxy', action='append') + parser.add_argument("-l", "--host", action="append") + parser.add_argument("-p", "--port", type=int, action="append") + parser.add_argument("-P", "--proxy", action="append") args = parser.parse_args() class Handler(EchoPacketHandler): @@ -35,5 +38,5 @@ def main(): server.serve_forever() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/judge/bridge/judge_handler.py b/judge/bridge/judge_handler.py index 86f53b4..a6c3b10 100644 --- a/judge/bridge/judge_handler.py +++ b/judge/bridge/judge_handler.py @@ -14,19 +14,30 @@ from django.db.models import F from judge import event_poster as event from judge.bridge.base_handler import ZlibPacketHandler, proxy_list from judge.caching import finished_submission -from judge.models import Judge, Language, LanguageLimit, Problem, RuntimeVersion, Submission, SubmissionTestCase +from judge.models import ( + Judge, + Language, + LanguageLimit, + Problem, + RuntimeVersion, + Submission, + SubmissionTestCase, +) -logger = logging.getLogger('judge.bridge') -json_log = logging.getLogger('judge.json.bridge') +logger = logging.getLogger("judge.bridge") +json_log = logging.getLogger("judge.json.bridge") UPDATE_RATE_LIMIT = 5 UPDATE_RATE_TIME = 0.5 -SubmissionData = namedtuple('SubmissionData', 'time memory short_circuit pretests_only contest_no attempt_no user_id') +SubmissionData = namedtuple( + "SubmissionData", + "time memory short_circuit pretests_only contest_no attempt_no user_id", +) def _ensure_connection(): try: - db.connection.cursor().execute('SELECT 1').fetchall() + db.connection.cursor().execute("SELECT 1").fetchall() except Exception: db.connection.close() @@ -39,19 +50,19 @@ class JudgeHandler(ZlibPacketHandler): self.judges = judges self.handlers = { - 'grading-begin': self.on_grading_begin, - 'grading-end': self.on_grading_end, - 'compile-error': self.on_compile_error, - 'compile-message': self.on_compile_message, - 'batch-begin': self.on_batch_begin, - 'batch-end': self.on_batch_end, - 'test-case-status': self.on_test_case, - 'internal-error': self.on_internal_error, - 'submission-terminated': self.on_submission_terminated, - 'submission-acknowledged': self.on_submission_acknowledged, - 'ping-response': self.on_ping_response, - 'supported-problems': self.on_supported_problems, - 'handshake': self.on_handshake, + "grading-begin": self.on_grading_begin, + "grading-end": self.on_grading_end, + "compile-error": self.on_compile_error, + "compile-message": self.on_compile_message, + "batch-begin": self.on_batch_begin, + "batch-end": self.on_batch_end, + "test-case-status": self.on_test_case, + "internal-error": self.on_internal_error, + "submission-terminated": self.on_submission_terminated, + "submission-acknowledged": self.on_submission_acknowledged, + "ping-response": self.on_ping_response, + "supported-problems": self.on_supported_problems, + "handshake": self.on_handshake, } self._working = False self._no_response_job = None @@ -78,22 +89,38 @@ class JudgeHandler(ZlibPacketHandler): def on_connect(self): self.timeout = 15 - logger.info('Judge connected from: %s', self.client_address) - json_log.info(self._make_json_log(action='connect')) + logger.info("Judge connected from: %s", self.client_address) + json_log.info(self._make_json_log(action="connect")) def on_disconnect(self): self._stop_ping.set() if self._working: - logger.error('Judge %s disconnected while handling submission %s', self.name, self._working) + logger.error( + "Judge %s disconnected while handling submission %s", + self.name, + self._working, + ) self.judges.remove(self) if self.name is not None: self._disconnected() - logger.info('Judge disconnected from: %s with name %s', self.client_address, self.name) + logger.info( + "Judge disconnected from: %s with name %s", self.client_address, self.name + ) - json_log.info(self._make_json_log(action='disconnect', info='judge disconnected')) + json_log.info( + self._make_json_log(action="disconnect", info="judge disconnected") + ) if self._working: - Submission.objects.filter(id=self._working).update(status='IE', result='IE', error='') - json_log.error(self._make_json_log(sub=self._working, action='close', info='IE due to shutdown on grading')) + Submission.objects.filter(id=self._working).update( + status="IE", result="IE", error="" + ) + json_log.error( + self._make_json_log( + sub=self._working, + action="close", + info="IE due to shutdown on grading", + ) + ) def _authenticate(self, id, key): try: @@ -104,7 +131,11 @@ class JudgeHandler(ZlibPacketHandler): result = hmac.compare_digest(judge.auth_key, key) if not result: - json_log.warning(self._make_json_log(action='auth', judge=id, info='judge failed authentication')) + json_log.warning( + self._make_json_log( + action="auth", judge=id, info="judge failed authentication" + ) + ) return result def _connected(self): @@ -119,15 +150,29 @@ class JudgeHandler(ZlibPacketHandler): versions = [] for lang in judge.runtimes.all(): versions += [ - RuntimeVersion(language=lang, name=name, version='.'.join(map(str, version)), priority=idx, judge=judge) + RuntimeVersion( + language=lang, + name=name, + version=".".join(map(str, version)), + priority=idx, + judge=judge, + ) for idx, (name, version) in enumerate(self.executors[lang.key]) ] RuntimeVersion.objects.bulk_create(versions) judge.last_ip = self.client_address[0] judge.save() - self.judge_address = '[%s]:%s' % (self.client_address[0], self.client_address[1]) - json_log.info(self._make_json_log(action='auth', info='judge successfully authenticated', - executors=list(self.executors.keys()))) + self.judge_address = "[%s]:%s" % ( + self.client_address[0], + self.client_address[1], + ) + json_log.info( + self._make_json_log( + action="auth", + info="judge successfully authenticated", + executors=list(self.executors.keys()), + ) + ) def _disconnected(self): Judge.objects.filter(id=self.judge.id).update(online=False) @@ -135,40 +180,50 @@ class JudgeHandler(ZlibPacketHandler): def _update_ping(self): try: - Judge.objects.filter(name=self.name).update(ping=self.latency, load=self.load) + Judge.objects.filter(name=self.name).update( + ping=self.latency, load=self.load + ) except Exception as e: # What can I do? I don't want to tie this to MySQL. - if e.__class__.__name__ == 'OperationalError' and e.__module__ == '_mysql_exceptions' and e.args[0] == 2006: + if ( + e.__class__.__name__ == "OperationalError" + and e.__module__ == "_mysql_exceptions" + and e.args[0] == 2006 + ): db.connection.close() def send(self, data): - super().send(json.dumps(data, separators=(',', ':'))) + super().send(json.dumps(data, separators=(",", ":"))) def on_handshake(self, packet): - if 'id' not in packet or 'key' not in packet: - logger.warning('Malformed handshake: %s', self.client_address) + if "id" not in packet or "key" not in packet: + logger.warning("Malformed handshake: %s", self.client_address) self.close() return - if not self._authenticate(packet['id'], packet['key']): - logger.warning('Authentication failure: %s', self.client_address) + if not self._authenticate(packet["id"], packet["key"]): + logger.warning("Authentication failure: %s", self.client_address) self.close() return self.timeout = 60 - self._problems = packet['problems'] + self._problems = packet["problems"] self.problems = dict(self._problems) - self.executors = packet['executors'] - self.name = packet['id'] + self.executors = packet["executors"] + self.name = packet["id"] - self.send({'name': 'handshake-success'}) - logger.info('Judge authenticated: %s (%s)', self.client_address, packet['id']) + self.send({"name": "handshake-success"}) + logger.info("Judge authenticated: %s (%s)", self.client_address, packet["id"]) self.judges.register(self) threading.Thread(target=self._ping_thread).start() self._connected() def can_judge(self, problem, executor, judge_id=None): - return problem in self.problems and executor in self.executors and (not judge_id or self.name == judge_id) + return ( + problem in self.problems + and executor in self.executors + and (not judge_id or self.name == judge_id) + ) @property def working(self): @@ -178,25 +233,60 @@ class JudgeHandler(ZlibPacketHandler): _ensure_connection() try: - pid, time, memory, short_circuit, lid, is_pretested, sub_date, uid, part_virtual, part_id = ( - Submission.objects.filter(id=submission) - .values_list('problem__id', 'problem__time_limit', 'problem__memory_limit', - 'problem__short_circuit', 'language__id', 'is_pretested', 'date', 'user__id', - 'contest__participation__virtual', 'contest__participation__id')).get() + ( + pid, + time, + memory, + short_circuit, + lid, + is_pretested, + sub_date, + uid, + part_virtual, + part_id, + ) = ( + Submission.objects.filter(id=submission).values_list( + "problem__id", + "problem__time_limit", + "problem__memory_limit", + "problem__short_circuit", + "language__id", + "is_pretested", + "date", + "user__id", + "contest__participation__virtual", + "contest__participation__id", + ) + ).get() except Submission.DoesNotExist: - logger.error('Submission vanished: %s', submission) - json_log.error(self._make_json_log( - sub=self._working, action='request', - info='submission vanished when fetching info', - )) + logger.error("Submission vanished: %s", submission) + json_log.error( + self._make_json_log( + sub=self._working, + action="request", + info="submission vanished when fetching info", + ) + ) return - attempt_no = Submission.objects.filter(problem__id=pid, contest__participation__id=part_id, user__id=uid, - date__lt=sub_date).exclude(status__in=('CE', 'IE')).count() + 1 + attempt_no = ( + Submission.objects.filter( + problem__id=pid, + contest__participation__id=part_id, + user__id=uid, + date__lt=sub_date, + ) + .exclude(status__in=("CE", "IE")) + .count() + + 1 + ) try: - time, memory = (LanguageLimit.objects.filter(problem__id=pid, language__id=lid) - .values_list('time_limit', 'memory_limit').get()) + time, memory = ( + LanguageLimit.objects.filter(problem__id=pid, language__id=lid) + .values_list("time_limit", "memory_limit") + .get() + ) except LanguageLimit.DoesNotExist: pass @@ -215,139 +305,184 @@ class JudgeHandler(ZlibPacketHandler): # Yank the power out. self.close() else: - self.send({'name': 'disconnect'}) + self.send({"name": "disconnect"}) def submit(self, id, problem, language, source): data = self.get_related_submission_data(id) self._working = id self._no_response_job = threading.Timer(20, self._kill_if_no_response) - self.send({ - 'name': 'submission-request', - 'submission-id': id, - 'problem-id': problem, - 'language': language, - 'source': source, - 'time-limit': data.time, - 'memory-limit': data.memory, - 'short-circuit': data.short_circuit, - 'meta': { - 'pretests-only': data.pretests_only, - 'in-contest': data.contest_no, - 'attempt-no': data.attempt_no, - 'user': data.user_id, - }, - }) + self.send( + { + "name": "submission-request", + "submission-id": id, + "problem-id": problem, + "language": language, + "source": source, + "time-limit": data.time, + "memory-limit": data.memory, + "short-circuit": data.short_circuit, + "meta": { + "pretests-only": data.pretests_only, + "in-contest": data.contest_no, + "attempt-no": data.attempt_no, + "user": data.user_id, + }, + } + ) def _kill_if_no_response(self): - logger.error('Judge failed to acknowledge submission: %s: %s', self.name, self._working) + logger.error( + "Judge failed to acknowledge submission: %s: %s", self.name, self._working + ) self.close() def on_timeout(self): if self.name: - logger.warning('Judge seems dead: %s: %s', self.name, self._working) + logger.warning("Judge seems dead: %s: %s", self.name, self._working) def malformed_packet(self, exception): - logger.exception('Judge sent malformed packet: %s', self.name) + logger.exception("Judge sent malformed packet: %s", self.name) super(JudgeHandler, self).malformed_packet(exception) def on_submission_processing(self, packet): _ensure_connection() - id = packet['submission-id'] - if Submission.objects.filter(id=id).update(status='P', judged_on=self.judge): - event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'processing'}) - self._post_update_submission(id, 'processing') - json_log.info(self._make_json_log(packet, action='processing')) + id = packet["submission-id"] + if Submission.objects.filter(id=id).update(status="P", judged_on=self.judge): + event.post("sub_%s" % Submission.get_id_secret(id), {"type": "processing"}) + self._post_update_submission(id, "processing") + json_log.info(self._make_json_log(packet, action="processing")) else: - logger.warning('Unknown submission: %s', id) - json_log.error(self._make_json_log(packet, action='processing', info='unknown submission')) + logger.warning("Unknown submission: %s", id) + json_log.error( + self._make_json_log( + packet, action="processing", info="unknown submission" + ) + ) def on_submission_wrong_acknowledge(self, packet, expected, got): - json_log.error(self._make_json_log(packet, action='processing', info='wrong-acknowledge', expected=expected)) - Submission.objects.filter(id=expected).update(status='IE', result='IE', error=None) - Submission.objects.filter(id=got, status='QU').update(status='IE', result='IE', error=None) + json_log.error( + self._make_json_log( + packet, action="processing", info="wrong-acknowledge", expected=expected + ) + ) + Submission.objects.filter(id=expected).update( + status="IE", result="IE", error=None + ) + Submission.objects.filter(id=got, status="QU").update( + status="IE", result="IE", error=None + ) def on_submission_acknowledged(self, packet): - if not packet.get('submission-id', None) == self._working: - logger.error('Wrong acknowledgement: %s: %s, expected: %s', self.name, packet.get('submission-id', None), - self._working) - self.on_submission_wrong_acknowledge(packet, self._working, packet.get('submission-id', None)) + if not packet.get("submission-id", None) == self._working: + logger.error( + "Wrong acknowledgement: %s: %s, expected: %s", + self.name, + packet.get("submission-id", None), + self._working, + ) + self.on_submission_wrong_acknowledge( + packet, self._working, packet.get("submission-id", None) + ) self.close() - logger.info('Submission acknowledged: %d', self._working) + logger.info("Submission acknowledged: %d", self._working) if self._no_response_job: self._no_response_job.cancel() self._no_response_job = None self.on_submission_processing(packet) def abort(self): - self.send({'name': 'terminate-submission'}) + self.send({"name": "terminate-submission"}) def get_current_submission(self): return self._working or None def ping(self): - self.send({'name': 'ping', 'when': time.time()}) + self.send({"name": "ping", "when": time.time()}) def on_packet(self, data): try: try: data = json.loads(data) - if 'name' not in data: + if "name" not in data: raise ValueError except ValueError: self.on_malformed(data) else: - handler = self.handlers.get(data['name'], self.on_malformed) + handler = self.handlers.get(data["name"], self.on_malformed) handler(data) except Exception: - logger.exception('Error in packet handling (Judge-side): %s', self.name) + logger.exception("Error in packet handling (Judge-side): %s", self.name) self._packet_exception() # You can't crash here because you aren't so sure about the judges # not being malicious or simply malforms. THIS IS A SERVER! def _packet_exception(self): - json_log.exception(self._make_json_log(sub=self._working, info='packet processing exception')) + json_log.exception( + self._make_json_log(sub=self._working, info="packet processing exception") + ) def _submission_is_batch(self, id): if not Submission.objects.filter(id=id).update(batch=True): - logger.warning('Unknown submission: %s', id) + logger.warning("Unknown submission: %s", id) def on_supported_problems(self, packet): - logger.info('%s: Updated problem list', self.name) - self._problems = packet['problems'] + logger.info("%s: Updated problem list", self.name) + self._problems = packet["problems"] self.problems = dict(self._problems) if not self.working: self.judges.update_problems(self) - self.judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys()))) - json_log.info(self._make_json_log(action='update-problems', count=len(self.problems))) + self.judge.problems.set( + Problem.objects.filter(code__in=list(self.problems.keys())) + ) + json_log.info( + self._make_json_log(action="update-problems", count=len(self.problems)) + ) def on_grading_begin(self, packet): - logger.info('%s: Grading has begun on: %s', self.name, packet['submission-id']) + logger.info("%s: Grading has begun on: %s", self.name, packet["submission-id"]) self.batch_id = None - if Submission.objects.filter(id=packet['submission-id']).update( - status='G', is_pretested=packet['pretested'], - current_testcase=1, points=0, - batch=False, judged_date=timezone.now()): - SubmissionTestCase.objects.filter(submission_id=packet['submission-id']).delete() - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'grading-begin'}) - self._post_update_submission(packet['submission-id'], 'grading-begin') - json_log.info(self._make_json_log(packet, action='grading-begin')) + if Submission.objects.filter(id=packet["submission-id"]).update( + status="G", + is_pretested=packet["pretested"], + current_testcase=1, + points=0, + batch=False, + judged_date=timezone.now(), + ): + SubmissionTestCase.objects.filter( + submission_id=packet["submission-id"] + ).delete() + event.post( + "sub_%s" % Submission.get_id_secret(packet["submission-id"]), + {"type": "grading-begin"}, + ) + self._post_update_submission(packet["submission-id"], "grading-begin") + json_log.info(self._make_json_log(packet, action="grading-begin")) else: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='grading-begin', info='unknown submission')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, action="grading-begin", info="unknown submission" + ) + ) def on_grading_end(self, packet): - logger.info('%s: Grading has ended on: %s', self.name, packet['submission-id']) + logger.info("%s: Grading has ended on: %s", self.name, packet["submission-id"]) self._free_self(packet) self.batch_id = None try: - submission = Submission.objects.get(id=packet['submission-id']) + submission = Submission.objects.get(id=packet["submission-id"]) except Submission.DoesNotExist: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='grading-end', info='unknown submission')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, action="grading-end", info="unknown submission" + ) + ) return time = 0 @@ -355,7 +490,7 @@ class JudgeHandler(ZlibPacketHandler): points = 0.0 total = 0 status = 0 - status_codes = ['SC', 'AC', 'WA', 'MLE', 'TLE', 'IR', 'RTE', 'OLE'] + status_codes = ["SC", "AC", "WA", "MLE", "TLE", "IR", "RTE", "OLE"] batches = {} # batch number: (points, total) for case in SubmissionTestCase.objects.filter(submission=submission): @@ -388,19 +523,29 @@ class JudgeHandler(ZlibPacketHandler): if not problem.partial and sub_points != problem.points: sub_points = 0 - submission.status = 'D' + submission.status = "D" submission.time = time submission.memory = memory submission.points = sub_points submission.result = status_codes[status] submission.save() - json_log.info(self._make_json_log( - packet, action='grading-end', time=time, memory=memory, - points=sub_points, total=problem.points, result=submission.result, - case_points=points, case_total=total, user=submission.user_id, - problem=problem.code, finish=True, - )) + json_log.info( + self._make_json_log( + packet, + action="grading-end", + time=time, + memory=memory, + points=sub_points, + total=problem.points, + result=submission.result, + case_points=points, + case_total=total, + user=submission.user_id, + problem=problem.code, + finish=True, + ) + ) submission.user._updating_stats_only = True submission.user.calculate_points() @@ -410,143 +555,254 @@ class JudgeHandler(ZlibPacketHandler): finished_submission(submission) - event.post('sub_%s' % submission.id_secret, { - 'type': 'grading-end', - 'time': time, - 'memory': memory, - 'points': float(points), - 'total': float(problem.points), - 'result': submission.result, - }) - if hasattr(submission, 'contest'): + event.post( + "sub_%s" % submission.id_secret, + { + "type": "grading-end", + "time": time, + "memory": memory, + "points": float(points), + "total": float(problem.points), + "result": submission.result, + }, + ) + if hasattr(submission, "contest"): participation = submission.contest.participation - event.post('contest_%d' % participation.contest_id, {'type': 'update'}) - self._post_update_submission(submission.id, 'grading-end', done=True) + event.post("contest_%d" % participation.contest_id, {"type": "update"}) + self._post_update_submission(submission.id, "grading-end", done=True) def on_compile_error(self, packet): - logger.info('%s: Submission failed to compile: %s', self.name, packet['submission-id']) + logger.info( + "%s: Submission failed to compile: %s", self.name, packet["submission-id"] + ) self._free_self(packet) - if Submission.objects.filter(id=packet['submission-id']).update(status='CE', result='CE', error=packet['log']): - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), { - 'type': 'compile-error', - 'log': packet['log'], - }) - self._post_update_submission(packet['submission-id'], 'compile-error', done=True) - json_log.info(self._make_json_log(packet, action='compile-error', log=packet['log'], - finish=True, result='CE')) + if Submission.objects.filter(id=packet["submission-id"]).update( + status="CE", result="CE", error=packet["log"] + ): + event.post( + "sub_%s" % Submission.get_id_secret(packet["submission-id"]), + { + "type": "compile-error", + "log": packet["log"], + }, + ) + self._post_update_submission( + packet["submission-id"], "compile-error", done=True + ) + json_log.info( + self._make_json_log( + packet, + action="compile-error", + log=packet["log"], + finish=True, + result="CE", + ) + ) else: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='compile-error', info='unknown submission', - log=packet['log'], finish=True, result='CE')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, + action="compile-error", + info="unknown submission", + log=packet["log"], + finish=True, + result="CE", + ) + ) def on_compile_message(self, packet): - logger.info('%s: Submission generated compiler messages: %s', self.name, packet['submission-id']) + logger.info( + "%s: Submission generated compiler messages: %s", + self.name, + packet["submission-id"], + ) - if Submission.objects.filter(id=packet['submission-id']).update(error=packet['log']): - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'compile-message'}) - json_log.info(self._make_json_log(packet, action='compile-message', log=packet['log'])) + if Submission.objects.filter(id=packet["submission-id"]).update( + error=packet["log"] + ): + event.post( + "sub_%s" % Submission.get_id_secret(packet["submission-id"]), + {"type": "compile-message"}, + ) + json_log.info( + self._make_json_log(packet, action="compile-message", log=packet["log"]) + ) else: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='compile-message', info='unknown submission', - log=packet['log'])) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, + action="compile-message", + info="unknown submission", + log=packet["log"], + ) + ) def on_internal_error(self, packet): try: - raise ValueError('\n\n' + packet['message']) + raise ValueError("\n\n" + packet["message"]) except ValueError: - logger.exception('Judge %s failed while handling submission %s', self.name, packet['submission-id']) + logger.exception( + "Judge %s failed while handling submission %s", + self.name, + packet["submission-id"], + ) self._free_self(packet) - id = packet['submission-id'] - if Submission.objects.filter(id=id).update(status='IE', result='IE', error=packet['message']): - event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'}) - self._post_update_submission(id, 'internal-error', done=True) - json_log.info(self._make_json_log(packet, action='internal-error', message=packet['message'], - finish=True, result='IE')) + id = packet["submission-id"] + if Submission.objects.filter(id=id).update( + status="IE", result="IE", error=packet["message"] + ): + event.post( + "sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"} + ) + self._post_update_submission(id, "internal-error", done=True) + json_log.info( + self._make_json_log( + packet, + action="internal-error", + message=packet["message"], + finish=True, + result="IE", + ) + ) else: - logger.warning('Unknown submission: %s', id) - json_log.error(self._make_json_log(packet, action='internal-error', info='unknown submission', - message=packet['message'], finish=True, result='IE')) + logger.warning("Unknown submission: %s", id) + json_log.error( + self._make_json_log( + packet, + action="internal-error", + info="unknown submission", + message=packet["message"], + finish=True, + result="IE", + ) + ) def on_submission_terminated(self, packet): - logger.info('%s: Submission aborted: %s', self.name, packet['submission-id']) + logger.info("%s: Submission aborted: %s", self.name, packet["submission-id"]) self._free_self(packet) - if Submission.objects.filter(id=packet['submission-id']).update(status='AB', result='AB'): - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'aborted-submission'}) - self._post_update_submission(packet['submission-id'], 'terminated', done=True) - json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB')) + if Submission.objects.filter(id=packet["submission-id"]).update( + status="AB", result="AB" + ): + event.post( + "sub_%s" % Submission.get_id_secret(packet["submission-id"]), + {"type": "aborted-submission"}, + ) + self._post_update_submission( + packet["submission-id"], "terminated", done=True + ) + json_log.info( + self._make_json_log(packet, action="aborted", finish=True, result="AB") + ) else: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='aborted', info='unknown submission', - finish=True, result='AB')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, + action="aborted", + info="unknown submission", + finish=True, + result="AB", + ) + ) def on_batch_begin(self, packet): - logger.info('%s: Batch began on: %s', self.name, packet['submission-id']) + logger.info("%s: Batch began on: %s", self.name, packet["submission-id"]) self.in_batch = True if self.batch_id is None: self.batch_id = 0 - self._submission_is_batch(packet['submission-id']) + self._submission_is_batch(packet["submission-id"]) self.batch_id += 1 - json_log.info(self._make_json_log(packet, action='batch-begin', batch=self.batch_id)) + json_log.info( + self._make_json_log(packet, action="batch-begin", batch=self.batch_id) + ) def on_batch_end(self, packet): self.in_batch = False - logger.info('%s: Batch ended on: %s', self.name, packet['submission-id']) - json_log.info(self._make_json_log(packet, action='batch-end', batch=self.batch_id)) + logger.info("%s: Batch ended on: %s", self.name, packet["submission-id"]) + json_log.info( + self._make_json_log(packet, action="batch-end", batch=self.batch_id) + ) - def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length): - logger.info('%s: %d test case(s) completed on: %s', self.name, len(packet['cases']), packet['submission-id']) + def on_test_case( + self, + packet, + max_feedback=SubmissionTestCase._meta.get_field("feedback").max_length, + ): + logger.info( + "%s: %d test case(s) completed on: %s", + self.name, + len(packet["cases"]), + packet["submission-id"], + ) - id = packet['submission-id'] - updates = packet['cases'] - max_position = max(map(itemgetter('position'), updates)) - sum_points = sum(map(itemgetter('points'), updates)) + id = packet["submission-id"] + updates = packet["cases"] + max_position = max(map(itemgetter("position"), updates)) + sum_points = sum(map(itemgetter("points"), updates)) - - if not Submission.objects.filter(id=id).update(current_testcase=max_position + 1, points=F('points') + sum_points): - logger.warning('Unknown submission: %s', id) - json_log.error(self._make_json_log(packet, action='test-case', info='unknown submission')) + if not Submission.objects.filter(id=id).update( + current_testcase=max_position + 1, points=F("points") + sum_points + ): + logger.warning("Unknown submission: %s", id) + json_log.error( + self._make_json_log( + packet, action="test-case", info="unknown submission" + ) + ) return bulk_test_case_updates = [] for result in updates: - test_case = SubmissionTestCase(submission_id=id, case=result['position']) - status = result['status'] + test_case = SubmissionTestCase(submission_id=id, case=result["position"]) + status = result["status"] if status & 4: - test_case.status = 'TLE' + test_case.status = "TLE" elif status & 8: - test_case.status = 'MLE' + test_case.status = "MLE" elif status & 64: - test_case.status = 'OLE' + test_case.status = "OLE" elif status & 2: - test_case.status = 'RTE' + test_case.status = "RTE" elif status & 16: - test_case.status = 'IR' + test_case.status = "IR" elif status & 1: - test_case.status = 'WA' + test_case.status = "WA" elif status & 32: - test_case.status = 'SC' + test_case.status = "SC" else: - test_case.status = 'AC' - test_case.time = result['time'] - test_case.memory = result['memory'] - test_case.points = result['points'] - test_case.total = result['total-points'] + test_case.status = "AC" + test_case.time = result["time"] + test_case.memory = result["memory"] + test_case.points = result["points"] + test_case.total = result["total-points"] test_case.batch = self.batch_id if self.in_batch else None - test_case.feedback = (result.get('feedback') or '')[:max_feedback] - test_case.extended_feedback = result.get('extended-feedback') or '' - test_case.output = result['output'] + test_case.feedback = (result.get("feedback") or "")[:max_feedback] + test_case.extended_feedback = result.get("extended-feedback") or "" + test_case.output = result["output"] bulk_test_case_updates.append(test_case) - json_log.info(self._make_json_log( - packet, action='test-case', case=test_case.case, batch=test_case.batch, - time=test_case.time, memory=test_case.memory, feedback=test_case.feedback, - extended_feedback=test_case.extended_feedback, output=test_case.output, - points=test_case.points, total=test_case.total, status=test_case.status, - )) + json_log.info( + self._make_json_log( + packet, + action="test-case", + case=test_case.case, + batch=test_case.batch, + time=test_case.time, + memory=test_case.memory, + feedback=test_case.feedback, + extended_feedback=test_case.extended_feedback, + output=test_case.output, + points=test_case.points, + total=test_case.total, + status=test_case.status, + ) + ) do_post = True @@ -563,29 +819,34 @@ class JudgeHandler(ZlibPacketHandler): self.update_counter[id] = (1, time.monotonic()) if do_post: - event.post('sub_%s' % Submission.get_id_secret(id), { - 'type': 'test-case', - 'id': max_position, - }) - self._post_update_submission(id, state='test-case') + event.post( + "sub_%s" % Submission.get_id_secret(id), + { + "type": "test-case", + "id": max_position, + }, + ) + self._post_update_submission(id, state="test-case") SubmissionTestCase.objects.bulk_create(bulk_test_case_updates) def on_malformed(self, packet): - logger.error('%s: Malformed packet: %s', self.name, packet) - json_log.exception(self._make_json_log(sub=self._working, info='malformed json packet')) + logger.error("%s: Malformed packet: %s", self.name, packet) + json_log.exception( + self._make_json_log(sub=self._working, info="malformed json packet") + ) def on_ping_response(self, packet): end = time.time() - self._ping_average.append(end - packet['when']) - self._time_delta.append((end + packet['when']) / 2 - packet['time']) + self._ping_average.append(end - packet["when"]) + self._time_delta.append((end + packet["when"]) / 2 - packet["time"]) self.latency = sum(self._ping_average) / len(self._ping_average) self.time_delta = sum(self._time_delta) / len(self._time_delta) - self.load = packet['load'] + self.load = packet["load"] self._update_ping() def _free_self(self, packet): - self.judges.on_judge_free(self, packet['submission-id']) + self.judges.on_judge_free(self, packet["submission-id"]) def _ping_thread(self): try: @@ -594,19 +855,19 @@ class JudgeHandler(ZlibPacketHandler): if self._stop_ping.wait(10): break except Exception: - logger.exception('Ping error in %s', self.name) + logger.exception("Ping error in %s", self.name) self.close() raise def _make_json_log(self, packet=None, sub=None, **kwargs): data = { - 'judge': self.name, - 'address': self.judge_address, + "judge": self.name, + "address": self.judge_address, } if sub is None and packet is not None: - sub = packet.get('submission-id') + sub = packet.get("submission-id") if sub is not None: - data['submission'] = sub + data["submission"] = sub data.update(kwargs) return json.dumps(data) @@ -614,17 +875,31 @@ class JudgeHandler(ZlibPacketHandler): if self._submission_cache_id == id: data = self._submission_cache else: - self._submission_cache = data = Submission.objects.filter(id=id).values( - 'problem__is_public', 'contest_object__key', - 'user_id', 'problem_id', 'status', 'language__key', - ).get() + self._submission_cache = data = ( + Submission.objects.filter(id=id) + .values( + "problem__is_public", + "contest_object__key", + "user_id", + "problem_id", + "status", + "language__key", + ) + .get() + ) self._submission_cache_id = id - if data['problem__is_public']: - event.post('submissions', { - 'type': 'done-submission' if done else 'update-submission', - 'state': state, 'id': id, - 'contest': data['contest_object__key'], - 'user': data['user_id'], 'problem': data['problem_id'], - 'status': data['status'], 'language': data['language__key'], - }) + if data["problem__is_public"]: + event.post( + "submissions", + { + "type": "done-submission" if done else "update-submission", + "state": state, + "id": id, + "contest": data["contest_object__key"], + "user": data["user_id"], + "problem": data["problem_id"], + "status": data["status"], + "language": data["language__key"], + }, + ) diff --git a/judge/bridge/judge_list.py b/judge/bridge/judge_list.py index 828bb83..a670667 100644 --- a/judge/bridge/judge_list.py +++ b/judge/bridge/judge_list.py @@ -8,9 +8,9 @@ try: except ImportError: from pyllist import dllist -logger = logging.getLogger('judge.bridge') +logger = logging.getLogger("judge.bridge") -PriorityMarker = namedtuple('PriorityMarker', 'priority') +PriorityMarker = namedtuple("PriorityMarker", "priority") class JudgeList(object): @@ -18,7 +18,9 @@ class JudgeList(object): def __init__(self): self.queue = dllist() - self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)] + self.priority = [ + self.queue.append(PriorityMarker(i)) for i in range(self.priorities) + ] self.judges = set() self.node_map = {} self.submission_map = {} @@ -32,11 +34,19 @@ class JudgeList(object): id, problem, language, source, judge_id = node.value if judge.can_judge(problem, language, judge_id): self.submission_map[id] = judge - logger.info('Dispatched queued submission %d: %s', id, judge.name) + logger.info( + "Dispatched queued submission %d: %s", id, judge.name + ) try: judge.submit(id, problem, language, source) except Exception: - logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name) + logger.exception( + "Failed to dispatch %d (%s, %s) to %s", + id, + problem, + language, + judge.name, + ) self.judges.remove(judge) return self.queue.remove(node) @@ -76,14 +86,14 @@ class JudgeList(object): def on_judge_free(self, judge, submission): with self.lock: - logger.info('Judge available after grading %d: %s', submission, judge.name) + logger.info("Judge available after grading %d: %s", submission, judge.name) del self.submission_map[submission] judge._working = False self._handle_free_judge(judge) def abort(self, submission): with self.lock: - logger.info('Abort request: %d', submission) + logger.info("Abort request: %d", submission) try: self.submission_map[submission].abort() return True @@ -108,21 +118,33 @@ class JudgeList(object): return candidates = [ - judge for judge in self.judges if not judge.working and judge.can_judge(problem, language, judge_id) + judge + for judge in self.judges + if not judge.working and judge.can_judge(problem, language, judge_id) ] if judge_id: - logger.info('Specified judge %s is%savailable', judge_id, ' ' if candidates else ' not ') + logger.info( + "Specified judge %s is%savailable", + judge_id, + " " if candidates else " not ", + ) else: - logger.info('Free judges: %d', len(candidates)) + logger.info("Free judges: %d", len(candidates)) if candidates: # Schedule the submission on the judge reporting least load. - judge = min(candidates, key=attrgetter('load')) - logger.info('Dispatched submission %d to: %s', id, judge.name) + judge = min(candidates, key=attrgetter("load")) + logger.info("Dispatched submission %d to: %s", id, judge.name) self.submission_map[id] = judge try: judge.submit(id, problem, language, source) except Exception: - logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name) + logger.exception( + "Failed to dispatch %d (%s, %s) to %s", + id, + problem, + language, + judge.name, + ) self.judges.discard(judge) return self.judge(id, problem, language, source, judge_id, priority) else: @@ -130,4 +152,4 @@ class JudgeList(object): (id, problem, language, source, judge_id), self.priority[priority], ) - logger.info('Queued submission: %d', id) + logger.info("Queued submission: %d", id) diff --git a/judge/bridge/server.py b/judge/bridge/server.py index cc83f84..4e67310 100644 --- a/judge/bridge/server.py +++ b/judge/bridge/server.py @@ -12,7 +12,9 @@ class Server: self._shutdown = threading.Event() def serve_forever(self): - threads = [threading.Thread(target=server.serve_forever) for server in self.servers] + threads = [ + threading.Thread(target=server.serve_forever) for server in self.servers + ] for thread in threads: thread.daemon = True thread.start() diff --git a/judge/caching.py b/judge/caching.py index 7f0a687..99bbf81 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -2,9 +2,9 @@ from django.core.cache import cache def finished_submission(sub): - keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id] - if hasattr(sub, 'contest'): + keys = ["user_complete:%d" % sub.user_id, "user_attempted:%s" % sub.user_id] + if hasattr(sub, "contest"): participation = sub.contest.participation - keys += ['contest_complete:%d' % participation.id] - keys += ['contest_attempted:%d' % participation.id] + keys += ["contest_complete:%d" % participation.id] + keys += ["contest_attempted:%d" % participation.id] cache.delete_many(keys) diff --git a/judge/comments.py b/judge/comments.py index 7408e68..2b7579a 100644 --- a/judge/comments.py +++ b/judge/comments.py @@ -7,7 +7,11 @@ from django.db.models import Count from django.db.models.expressions import F, Value from django.db.models.functions import Coalesce from django.forms import ModelForm -from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect +from django.http import ( + HttpResponseForbidden, + HttpResponseNotFound, + HttpResponseRedirect, +) from django.urls import reverse_lazy from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ @@ -24,50 +28,55 @@ from judge.widgets import HeavyPreviewPageDownWidget from judge.jinja2.reference import get_user_from_text - def add_mention_notifications(comment): user_referred = get_user_from_text(comment.body).exclude(id=comment.author.id) for user in user_referred: - notification_ref = Notification(owner=user, - comment=comment, - category='Mention') + notification_ref = Notification(owner=user, comment=comment, category="Mention") notification_ref.save() -def del_mention_notifications(comment): - query = { - 'comment': comment, - 'category': 'Mention' - } - Notification.objects.filter(**query).delete() +def del_mention_notifications(comment): + query = {"comment": comment, "category": "Mention"} + Notification.objects.filter(**query).delete() class CommentForm(ModelForm): class Meta: model = Comment - fields = ['body', 'parent'] + fields = ["body", "parent"] widgets = { - 'parent': forms.HiddenInput(), + "parent": forms.HiddenInput(), } if HeavyPreviewPageDownWidget is not None: - widgets['body'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'), - preview_timeout=1000, hide_preview_button=True) + widgets["body"] = HeavyPreviewPageDownWidget( + preview=reverse_lazy("comment_preview"), + preview_timeout=1000, + hide_preview_button=True, + ) def __init__(self, request, *args, **kwargs): self.request = request super(CommentForm, self).__init__(*args, **kwargs) - self.fields['body'].widget.attrs.update({'placeholder': _('Comment body')}) + self.fields["body"].widget.attrs.update({"placeholder": _("Comment body")}) def clean(self): if self.request is not None and self.request.user.is_authenticated: profile = self.request.profile if profile.mute: - raise ValidationError(_('Your part is silent, little toad.')) - elif (not self.request.user.is_staff and - not profile.submission_set.filter(points=F('problem__points')).exists()): - raise ValidationError(_('You need to have solved at least one problem ' - 'before your voice can be heard.')) + raise ValidationError(_("Your part is silent, little toad.")) + elif ( + not self.request.user.is_staff + and not profile.submission_set.filter( + points=F("problem__points") + ).exists() + ): + raise ValidationError( + _( + "You need to have solved at least one problem " + "before your voice can be heard." + ) + ) return super(CommentForm, self).clean() @@ -80,10 +89,12 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): return self.comment_page def is_comment_locked(self): - if self.request.user.has_perm('judge.override_comment_lock'): + if self.request.user.has_perm("judge.override_comment_lock"): return False - return (CommentLock.objects.filter(page=self.get_comment_page()).exists() - or (self.request.in_contest and self.request.participation.contest.use_clarifications)) + return CommentLock.objects.filter(page=self.get_comment_page()).exists() or ( + self.request.in_contest + and self.request.participation.contest.use_clarifications + ) @method_decorator(login_required) def post(self, request, *args, **kwargs): @@ -93,14 +104,16 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): if self.is_comment_locked(): return HttpResponseForbidden() - parent = request.POST.get('parent') + parent = request.POST.get("parent") if parent: try: parent = int(parent) except ValueError: return HttpResponseNotFound() else: - if not Comment.objects.filter(hidden=False, id=parent, page=page).exists(): + if not Comment.objects.filter( + hidden=False, id=parent, page=page + ).exists(): return HttpResponseNotFound() form = CommentForm(request, request.POST) @@ -109,21 +122,22 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): comment.author = request.profile comment.page = page - - with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision(): + with LockModel( + write=(Comment, Revision, Version), read=(ContentType,) + ), revisions.create_revision(): revisions.set_user(request.user) - revisions.set_comment(_('Posted comment')) + revisions.set_comment(_("Posted comment")) comment.save() # add notification for reply if comment.parent and comment.parent.author != comment.author: - notification_rep = Notification(owner=comment.parent.author, - comment=comment, - category='Reply') + notification_rep = Notification( + owner=comment.parent.author, comment=comment, category="Reply" + ) notification_rep.save() add_mention_notifications(comment) - + return HttpResponseRedirect(request.path) context = self.get_context_data(object=self.object, comment_form=form) @@ -131,25 +145,40 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): self.object = self.get_object() - return self.render_to_response(self.get_context_data( - object=self.object, - comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}), - )) + return self.render_to_response( + self.get_context_data( + object=self.object, + comment_form=CommentForm( + request, initial={"page": self.get_comment_page(), "parent": None} + ), + ) + ) def get_context_data(self, **kwargs): context = super(CommentedDetailView, self).get_context_data(**kwargs) queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page()) - context['has_comments'] = queryset.exists() - context['comment_lock'] = self.is_comment_locked() - queryset = queryset.select_related('author__user').defer('author__about').annotate(revisions=Count('versions')) + context["has_comments"] = queryset.exists() + context["comment_lock"] = self.is_comment_locked() + queryset = ( + queryset.select_related("author__user") + .defer("author__about") + .annotate(revisions=Count("versions")) + ) if self.request.user.is_authenticated: - queryset = queryset.annotate(vote_score=Coalesce(RawSQLColumn(CommentVote, 'score'), Value(0))) + queryset = queryset.annotate( + vote_score=Coalesce(RawSQLColumn(CommentVote, "score"), Value(0)) + ) profile = self.request.profile - unique_together_left_join(queryset, CommentVote, 'comment', 'voter', profile.id) - context['is_new_user'] = (not self.request.user.is_staff and - not profile.submission_set.filter(points=F('problem__points')).exists()) - context['comment_list'] = queryset - context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD + unique_together_left_join( + queryset, CommentVote, "comment", "voter", profile.id + ) + context["is_new_user"] = ( + not self.request.user.is_staff + and not profile.submission_set.filter( + points=F("problem__points") + ).exists() + ) + context["comment_list"] = queryset + context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD return context - diff --git a/judge/contest_format/atcoder.py b/judge/contest_format/atcoder.py index 28b188d..bb3b30a 100644 --- a/judge/contest_format/atcoder.py +++ b/judge/contest_format/atcoder.py @@ -14,14 +14,14 @@ from judge.timezone import from_database_time from judge.utils.timedelta import nice_repr -@register_contest_format('atcoder') +@register_contest_format("atcoder") class AtCoderContestFormat(DefaultContestFormat): - name = gettext_lazy('AtCoder') - config_defaults = {'penalty': 5} - config_validators = {'penalty': lambda x: x >= 0} - ''' + name = gettext_lazy("AtCoder") + config_defaults = {"penalty": 5} + config_validators = {"penalty": lambda x: x >= 0} + """ penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5. - ''' + """ @classmethod def validate(cls, config): @@ -29,7 +29,9 @@ class AtCoderContestFormat(DefaultContestFormat): return if not isinstance(config, dict): - raise ValidationError('AtCoder-styled contest expects no config or dict as config') + raise ValidationError( + "AtCoder-styled contest expects no config or dict as config" + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -37,7 +39,9 @@ class AtCoderContestFormat(DefaultContestFormat): if not isinstance(value, type(cls.config_defaults[key])): raise ValidationError('invalid type for config key "%s"' % key) if not cls.config_validators[key](value): - raise ValidationError('invalid value "%s" for config key "%s"' % (value, key)) + raise ValidationError( + 'invalid value "%s" for config key "%s"' % (value, key) + ) def __init__(self, contest, config): self.config = self.config_defaults.copy() @@ -51,7 +55,8 @@ class AtCoderContestFormat(DefaultContestFormat): format_data = {} with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT MAX(cs.points) as `score`, ( SELECT MIN(csub.date) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -62,21 +67,27 @@ class AtCoderContestFormat(DefaultContestFormat): judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_submission sub ON (sub.id = cs.submission_id) GROUP BY cp.id - ''', (participation.id, participation.id)) + """, + (participation.id, participation.id), + ) for score, time, prob in cursor.fetchall(): time = from_database_time(time) dt = (time - participation.start).total_seconds() # Compute penalty - if self.config['penalty']: + if self.config["penalty"]: # An IE can have a submission result of `None` - subs = participation.submissions.exclude(submission__result__isnull=True) \ - .exclude(submission__result__in=['IE', 'CE']) \ - .filter(problem_id=prob) + subs = ( + participation.submissions.exclude( + submission__result__isnull=True + ) + .exclude(submission__result__in=["IE", "CE"]) + .filter(problem_id=prob) + ) if score: prev = subs.filter(submission__date__lte=time).count() - 1 - penalty += prev * self.config['penalty'] * 60 + penalty += prev * self.config["penalty"] * 60 else: # We should always display the penalty, even if the user has a score of 0 prev = subs.count() @@ -86,7 +97,7 @@ class AtCoderContestFormat(DefaultContestFormat): if score: cumtime = max(cumtime, dt) - format_data[str(prob)] = {'time': dt, 'points': score, 'penalty': prev} + format_data[str(prob)] = {"time": dt, "points": score, "penalty": prev} points += score participation.cumtime = cumtime + penalty @@ -98,17 +109,38 @@ class AtCoderContestFormat(DefaultContestFormat): def display_user_problem(self, participation, contest_problem): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: - penalty = format_html(' ({penalty})', - penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else '' + penalty = ( + format_html( + ' ({penalty})', + penalty=floatformat(format_data["penalty"]), + ) + if format_data["penalty"] + else "" + ) return format_html( '{points}{penalty}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), - points=floatformat(format_data['points']), + state=( + ( + "pretest-" + if self.contest.run_pretests_only + and contest_problem.is_pretested + else "" + ) + + self.best_solution_state( + format_data["points"], contest_problem.points + ) + ), + url=reverse( + "contest_user_submissions", + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), + points=floatformat(format_data["points"]), penalty=penalty, - time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: return mark_safe('') diff --git a/judge/contest_format/base.py b/judge/contest_format/base.py index d4e6e6f..9e573d7 100644 --- a/judge/contest_format/base.py +++ b/judge/contest_format/base.py @@ -93,7 +93,7 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)): @classmethod def best_solution_state(cls, points, total): if not points: - return 'failed-score' + return "failed-score" if points == total: - return 'full-score' - return 'partial-score' + return "full-score" + return "partial-score" diff --git a/judge/contest_format/default.py b/judge/contest_format/default.py index 532102d..11c8f37 100644 --- a/judge/contest_format/default.py +++ b/judge/contest_format/default.py @@ -13,14 +13,16 @@ from judge.contest_format.registry import register_contest_format from judge.utils.timedelta import nice_repr -@register_contest_format('default') +@register_contest_format("default") class DefaultContestFormat(BaseContestFormat): - name = gettext_lazy('Default') + name = gettext_lazy("Default") @classmethod def validate(cls, config): if config is not None and (not isinstance(config, dict) or config): - raise ValidationError('default contest expects no config or empty dict as config') + raise ValidationError( + "default contest expects no config or empty dict as config" + ) def __init__(self, contest, config): super(DefaultContestFormat, self).__init__(contest, config) @@ -30,14 +32,18 @@ class DefaultContestFormat(BaseContestFormat): points = 0 format_data = {} - for result in participation.submissions.values('problem_id').annotate( - time=Max('submission__date'), points=Max('points'), + for result in participation.submissions.values("problem_id").annotate( + time=Max("submission__date"), + points=Max("points"), ): - dt = (result['time'] - participation.start).total_seconds() - if result['points']: + dt = (result["time"] - participation.start).total_seconds() + if result["points"]: cumtime += dt - format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']} - points += result['points'] + format_data[str(result["problem_id"])] = { + "time": dt, + "points": result["points"], + } + points += result["points"] participation.cumtime = max(cumtime, 0) participation.score = points @@ -49,30 +55,50 @@ class DefaultContestFormat(BaseContestFormat): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: return format_html( - u'{points}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), - points=floatformat(format_data['points'], -self.contest.points_precision), - time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), + '{points}
{time}
', + state=( + ( + "pretest-" + if self.contest.run_pretests_only + and contest_problem.is_pretested + else "" + ) + + self.best_solution_state( + format_data["points"], contest_problem.points + ) + ), + url=reverse( + "contest_user_submissions", + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), + points=floatformat( + format_data["points"], -self.contest.points_precision + ), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: return mark_safe('') def display_participation_result(self, participation): return format_html( - u'{points}
{cumtime}
', + '{points}
{cumtime}
', points=floatformat(participation.score, -self.contest.points_precision), - cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'), + cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday"), ) def get_problem_breakdown(self, participation, contest_problems): - return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems] + return [ + (participation.format_data or {}).get(str(contest_problem.id)) + for contest_problem in contest_problems + ] def get_contest_problem_label_script(self): - return ''' + return """ function(n) return tostring(math.floor(n + 1)) end - ''' \ No newline at end of file + """ diff --git a/judge/contest_format/ecoo.py b/judge/contest_format/ecoo.py index 1199826..012be13 100644 --- a/judge/contest_format/ecoo.py +++ b/judge/contest_format/ecoo.py @@ -14,17 +14,21 @@ from judge.timezone import from_database_time from judge.utils.timedelta import nice_repr -@register_contest_format('ecoo') +@register_contest_format("ecoo") class ECOOContestFormat(DefaultContestFormat): - name = gettext_lazy('ECOO') - config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5} - config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0} - ''' + name = gettext_lazy("ECOO") + config_defaults = {"cumtime": False, "first_ac_bonus": 10, "time_bonus": 5} + config_validators = { + "cumtime": lambda x: True, + "first_ac_bonus": lambda x: x >= 0, + "time_bonus": lambda x: x >= 0, + } + """ cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False. first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10. time_bonus: Number of minutes to award an extra point for submitting before the contest end. Specify 0 to disable. Defaults to 5. - ''' + """ @classmethod def validate(cls, config): @@ -32,7 +36,9 @@ class ECOOContestFormat(DefaultContestFormat): return if not isinstance(config, dict): - raise ValidationError('ECOO-styled contest expects no config or dict as config') + raise ValidationError( + "ECOO-styled contest expects no config or dict as config" + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -40,7 +46,9 @@ class ECOOContestFormat(DefaultContestFormat): if not isinstance(value, type(cls.config_defaults[key])): raise ValidationError('invalid type for config key "%s"' % key) if not cls.config_validators[key](value): - raise ValidationError('invalid value "%s" for config key "%s"' % (value, key)) + raise ValidationError( + 'invalid value "%s" for config key "%s"' % (value, key) + ) def __init__(self, contest, config): self.config = self.config_defaults.copy() @@ -53,7 +61,8 @@ class ECOOContestFormat(DefaultContestFormat): format_data = {} with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT ( SELECT MAX(ccs.points) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -69,25 +78,31 @@ class ECOOContestFormat(DefaultContestFormat): judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_submission sub ON (sub.id = cs.submission_id) GROUP BY cp.id - ''', (participation.id, participation.id, participation.id)) + """, + (participation.id, participation.id, participation.id), + ) for score, time, prob, subs, max_score in cursor.fetchall(): time = from_database_time(time) dt = (time - participation.start).total_seconds() - if self.config['cumtime']: + if self.config["cumtime"]: cumtime += dt bonus = 0 if score > 0: # First AC bonus if subs == 1 and score == max_score: - bonus += self.config['first_ac_bonus'] + bonus += self.config["first_ac_bonus"] # Time bonus - if self.config['time_bonus']: - bonus += (participation.end_time - time).total_seconds() // 60 // self.config['time_bonus'] + if self.config["time_bonus"]: + bonus += ( + (participation.end_time - time).total_seconds() + // 60 + // self.config["time_bonus"] + ) points += bonus - format_data[str(prob)] = {'time': dt, 'points': score, 'bonus': bonus} + format_data[str(prob)] = {"time": dt, "points": score, "bonus": bonus} points += score participation.cumtime = cumtime @@ -99,25 +114,47 @@ class ECOOContestFormat(DefaultContestFormat): def display_user_problem(self, participation, contest_problem): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: - bonus = format_html(' +{bonus}', - bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else '' + bonus = ( + format_html( + " +{bonus}", bonus=floatformat(format_data["bonus"]) + ) + if format_data["bonus"] + else "" + ) return format_html( '{points}{bonus}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), - points=floatformat(format_data['points']), + state=( + ( + "pretest-" + if self.contest.run_pretests_only + and contest_problem.is_pretested + else "" + ) + + self.best_solution_state( + format_data["points"], contest_problem.points + ) + ), + url=reverse( + "contest_user_submissions", + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), + points=floatformat(format_data["points"]), bonus=bonus, - time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: - return mark_safe('') + return mark_safe("") def display_participation_result(self, participation): return format_html( '{points}
{cumtime}
', points=floatformat(participation.score), - cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '', + cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday") + if self.config["cumtime"] + else "", ) diff --git a/judge/contest_format/icpc.py b/judge/contest_format/icpc.py index e858a6c..8596628 100644 --- a/judge/contest_format/icpc.py +++ b/judge/contest_format/icpc.py @@ -14,14 +14,14 @@ from judge.timezone import from_database_time from judge.utils.timedelta import nice_repr -@register_contest_format('icpc') +@register_contest_format("icpc") class ICPCContestFormat(DefaultContestFormat): - name = gettext_lazy('ICPC') - config_defaults = {'penalty': 20} - config_validators = {'penalty': lambda x: x >= 0} - ''' + name = gettext_lazy("ICPC") + config_defaults = {"penalty": 20} + config_validators = {"penalty": lambda x: x >= 0} + """ penalty: Number of penalty minutes each incorrect submission adds. Defaults to 20. - ''' + """ @classmethod def validate(cls, config): @@ -29,7 +29,9 @@ class ICPCContestFormat(DefaultContestFormat): return if not isinstance(config, dict): - raise ValidationError('ICPC-styled contest expects no config or dict as config') + raise ValidationError( + "ICPC-styled contest expects no config or dict as config" + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -37,7 +39,9 @@ class ICPCContestFormat(DefaultContestFormat): if not isinstance(value, type(cls.config_defaults[key])): raise ValidationError('invalid type for config key "%s"' % key) if not cls.config_validators[key](value): - raise ValidationError('invalid value "%s" for config key "%s"' % (value, key)) + raise ValidationError( + 'invalid value "%s" for config key "%s"' % (value, key) + ) def __init__(self, contest, config): self.config = self.config_defaults.copy() @@ -52,7 +56,8 @@ class ICPCContestFormat(DefaultContestFormat): format_data = {} with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT MAX(cs.points) as `points`, ( SELECT MIN(csub.date) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -63,21 +68,27 @@ class ICPCContestFormat(DefaultContestFormat): judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_submission sub ON (sub.id = cs.submission_id) GROUP BY cp.id - ''', (participation.id, participation.id)) + """, + (participation.id, participation.id), + ) for points, time, prob in cursor.fetchall(): time = from_database_time(time) dt = (time - participation.start).total_seconds() # Compute penalty - if self.config['penalty']: + if self.config["penalty"]: # An IE can have a submission result of `None` - subs = participation.submissions.exclude(submission__result__isnull=True) \ - .exclude(submission__result__in=['IE', 'CE']) \ - .filter(problem_id=prob) + subs = ( + participation.submissions.exclude( + submission__result__isnull=True + ) + .exclude(submission__result__in=["IE", "CE"]) + .filter(problem_id=prob) + ) if points: prev = subs.filter(submission__date__lte=time).count() - 1 - penalty += prev * self.config['penalty'] * 60 + penalty += prev * self.config["penalty"] * 60 else: # We should always display the penalty, even if the user has a score of 0 prev = subs.count() @@ -88,7 +99,7 @@ class ICPCContestFormat(DefaultContestFormat): cumtime += dt last = max(last, dt) - format_data[str(prob)] = {'time': dt, 'points': points, 'penalty': prev} + format_data[str(prob)] = {"time": dt, "points": points, "penalty": prev} score += points participation.cumtime = max(0, cumtime + penalty) @@ -100,23 +111,44 @@ class ICPCContestFormat(DefaultContestFormat): def display_user_problem(self, participation, contest_problem): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: - penalty = format_html(' ({penalty})', - penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else '' + penalty = ( + format_html( + ' ({penalty})', + penalty=floatformat(format_data["penalty"]), + ) + if format_data["penalty"] + else "" + ) return format_html( '{points}{penalty}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), - points=floatformat(format_data['points']), + state=( + ( + "pretest-" + if self.contest.run_pretests_only + and contest_problem.is_pretested + else "" + ) + + self.best_solution_state( + format_data["points"], contest_problem.points + ) + ), + url=reverse( + "contest_user_submissions", + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), + points=floatformat(format_data["points"]), penalty=penalty, - time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: - return mark_safe('') + return mark_safe("") def get_contest_problem_label_script(self): - return ''' + return """ function(n) n = n + 1 ret = "" @@ -126,4 +158,4 @@ class ICPCContestFormat(DefaultContestFormat): end return ret end - ''' \ No newline at end of file + """ diff --git a/judge/contest_format/ioi.py b/judge/contest_format/ioi.py index dba337c..2113fab 100644 --- a/judge/contest_format/ioi.py +++ b/judge/contest_format/ioi.py @@ -14,13 +14,13 @@ from judge.timezone import from_database_time from judge.utils.timedelta import nice_repr -@register_contest_format('ioi') +@register_contest_format("ioi") class IOIContestFormat(DefaultContestFormat): - name = gettext_lazy('IOI') - config_defaults = {'cumtime': False} - ''' + name = gettext_lazy("IOI") + config_defaults = {"cumtime": False} + """ cumtime: Specify True if time penalties are to be computed. Defaults to False. - ''' + """ @classmethod def validate(cls, config): @@ -28,7 +28,9 @@ class IOIContestFormat(DefaultContestFormat): return if not isinstance(config, dict): - raise ValidationError('IOI-styled contest expects no config or dict as config') + raise ValidationError( + "IOI-styled contest expects no config or dict as config" + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -47,7 +49,8 @@ class IOIContestFormat(DefaultContestFormat): format_data = {} with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT MAX(cs.points) as `score`, ( SELECT MIN(csub.date) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -58,17 +61,21 @@ class IOIContestFormat(DefaultContestFormat): judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_submission sub ON (sub.id = cs.submission_id) GROUP BY cp.id - ''', (participation.id, participation.id)) + """, + (participation.id, participation.id), + ) for score, time, prob in cursor.fetchall(): - if self.config['cumtime']: - dt = (from_database_time(time) - participation.start).total_seconds() + if self.config["cumtime"]: + dt = ( + from_database_time(time) - participation.start + ).total_seconds() if score: cumtime += dt else: dt = 0 - format_data[str(prob)] = {'time': dt, 'points': score} + format_data[str(prob)] = {"time": dt, "points": score} points += score participation.cumtime = max(cumtime, 0) @@ -82,12 +89,29 @@ class IOIContestFormat(DefaultContestFormat): if format_data: return format_html( '{points}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), - points=floatformat(format_data['points']), - time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '', + state=( + ( + "pretest-" + if self.contest.run_pretests_only + and contest_problem.is_pretested + else "" + ) + + self.best_solution_state( + format_data["points"], contest_problem.points + ) + ), + url=reverse( + "contest_user_submissions", + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), + points=floatformat(format_data["points"]), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday") + if self.config["cumtime"] + else "", ) else: return mark_safe('') @@ -96,5 +120,7 @@ class IOIContestFormat(DefaultContestFormat): return format_html( '{points}
{cumtime}
', points=floatformat(participation.score), - cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '', + cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday") + if self.config["cumtime"] + else "", ) diff --git a/judge/dblock.py b/judge/dblock.py index d4d5184..5b7feab 100644 --- a/judge/dblock.py +++ b/judge/dblock.py @@ -5,19 +5,21 @@ from django.db import connection, transaction class LockModel(object): def __init__(self, write, read=()): - self.tables = ', '.join(chain( - ('`%s` WRITE' % model._meta.db_table for model in write), - ('`%s` READ' % model._meta.db_table for model in read), - )) + self.tables = ", ".join( + chain( + ("`%s` WRITE" % model._meta.db_table for model in write), + ("`%s` READ" % model._meta.db_table for model in read), + ) + ) self.cursor = connection.cursor() def __enter__(self): - self.cursor.execute('LOCK TABLES ' + self.tables) + self.cursor.execute("LOCK TABLES " + self.tables) def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: transaction.commit() else: transaction.rollback() - self.cursor.execute('UNLOCK TABLES') + self.cursor.execute("UNLOCK TABLES") self.cursor.close() diff --git a/judge/event_poster.py b/judge/event_poster.py index 29100bd..e7c57fd 100644 --- a/judge/event_poster.py +++ b/judge/event_poster.py @@ -1,6 +1,6 @@ from django.conf import settings -__all__ = ['last', 'post'] +__all__ = ["last", "post"] if not settings.EVENT_DAEMON_USE: real = False @@ -10,9 +10,12 @@ if not settings.EVENT_DAEMON_USE: def last(): return 0 -elif hasattr(settings, 'EVENT_DAEMON_AMQP'): + +elif hasattr(settings, "EVENT_DAEMON_AMQP"): from .event_poster_amqp import last, post + real = True else: from .event_poster_ws import last, post + real = True diff --git a/judge/event_poster_amqp.py b/judge/event_poster_amqp.py index 74f6331..959d882 100644 --- a/judge/event_poster_amqp.py +++ b/judge/event_poster_amqp.py @@ -6,7 +6,7 @@ import pika from django.conf import settings from pika.exceptions import AMQPError -__all__ = ['EventPoster', 'post', 'last'] +__all__ = ["EventPoster", "post", "last"] class EventPoster(object): @@ -15,14 +15,19 @@ class EventPoster(object): self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE def _connect(self): - self._conn = pika.BlockingConnection(pika.URLParameters(settings.EVENT_DAEMON_AMQP)) + self._conn = pika.BlockingConnection( + pika.URLParameters(settings.EVENT_DAEMON_AMQP) + ) self._chan = self._conn.channel() def post(self, channel, message, tries=0): try: id = int(time() * 1000000) - self._chan.basic_publish(self._exchange, '', - json.dumps({'id': id, 'channel': channel, 'message': message})) + self._chan.basic_publish( + self._exchange, + "", + json.dumps({"id": id, "channel": channel, "message": message}), + ) return id except AMQPError: if tries > 10: @@ -35,7 +40,7 @@ _local = threading.local() def _get_poster(): - if 'poster' not in _local.__dict__: + if "poster" not in _local.__dict__: _local.poster = EventPoster() return _local.poster diff --git a/judge/event_poster_ws.py b/judge/event_poster_ws.py index fba4052..8daddab 100644 --- a/judge/event_poster_ws.py +++ b/judge/event_poster_ws.py @@ -5,7 +5,7 @@ import threading from django.conf import settings from websocket import WebSocketException, create_connection -__all__ = ['EventPostingError', 'EventPoster', 'post', 'last'] +__all__ = ["EventPostingError", "EventPoster", "post", "last"] _local = threading.local() @@ -20,19 +20,23 @@ class EventPoster(object): def _connect(self): self._conn = create_connection(settings.EVENT_DAEMON_POST) if settings.EVENT_DAEMON_KEY is not None: - self._conn.send(json.dumps({'command': 'auth', 'key': settings.EVENT_DAEMON_KEY})) + self._conn.send( + json.dumps({"command": "auth", "key": settings.EVENT_DAEMON_KEY}) + ) resp = json.loads(self._conn.recv()) - if resp['status'] == 'error': - raise EventPostingError(resp['code']) + if resp["status"] == "error": + raise EventPostingError(resp["code"]) def post(self, channel, message, tries=0): try: - self._conn.send(json.dumps({'command': 'post', 'channel': channel, 'message': message})) + self._conn.send( + json.dumps({"command": "post", "channel": channel, "message": message}) + ) resp = json.loads(self._conn.recv()) - if resp['status'] == 'error': - raise EventPostingError(resp['code']) + if resp["status"] == "error": + raise EventPostingError(resp["code"]) else: - return resp['id'] + return resp["id"] except WebSocketException: if tries > 10: raise @@ -43,10 +47,10 @@ class EventPoster(object): try: self._conn.send('{"command": "last-msg"}') resp = json.loads(self._conn.recv()) - if resp['status'] == 'error': - raise EventPostingError(resp['code']) + if resp["status"] == "error": + raise EventPostingError(resp["code"]) else: - return resp['id'] + return resp["id"] except WebSocketException: if tries > 10: raise @@ -55,7 +59,7 @@ class EventPoster(object): def _get_poster(): - if 'poster' not in _local.__dict__: + if "poster" not in _local.__dict__: _local.poster = EventPoster() return _local.poster diff --git a/judge/feed.py b/judge/feed.py index 6d8d825..d665dd6 100644 --- a/judge/feed.py +++ b/judge/feed.py @@ -12,27 +12,35 @@ import re # https://lsimons.wordpress.com/2011/03/17/stripping-illegal-characters-out-of-xml-in-python/ -def escape_xml_illegal_chars(val, replacement='?'): - _illegal_xml_chars_RE = re.compile(u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]') +def escape_xml_illegal_chars(val, replacement="?"): + _illegal_xml_chars_RE = re.compile( + "[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]" + ) return _illegal_xml_chars_RE.sub(replacement, val) + class ProblemFeed(Feed): - title = 'Recently Added %s Problems' % settings.SITE_NAME - link = '/' - description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME + title = "Recently Added %s Problems" % settings.SITE_NAME + link = "/" + description = ( + "The latest problems added on the %s website" % settings.SITE_LONG_NAME + ) def items(self): - return Problem.objects.filter(is_public=True, is_organization_private=False).defer('description')\ - .order_by('-date', '-id')[:25] + return ( + Problem.objects.filter(is_public=True, is_organization_private=False) + .defer("description") + .order_by("-date", "-id")[:25] + ) def item_title(self, problem): return problem.name def item_description(self, problem): - key = 'problem_feed:%d' % problem.id + key = "problem_feed:%d" % problem.id desc = cache.get(key) if desc is None: - desc = str(markdown(problem.description, 'problem'))[:500] + '...' + desc = str(markdown(problem.description, "problem"))[:500] + "..." desc = escape_xml_illegal_chars(desc) cache.set(key, desc, 86400) return desc @@ -49,21 +57,21 @@ class AtomProblemFeed(ProblemFeed): class CommentFeed(Feed): - title = 'Latest %s Comments' % settings.SITE_NAME - link = '/' - description = 'The latest comments on the %s website' % settings.SITE_LONG_NAME + title = "Latest %s Comments" % settings.SITE_NAME + link = "/" + description = "The latest comments on the %s website" % settings.SITE_LONG_NAME def items(self): return Comment.most_recent(AnonymousUser(), 25) def item_title(self, comment): - return '%s -> %s' % (comment.author.user.username, comment.page_title) + return "%s -> %s" % (comment.author.user.username, comment.page_title) def item_description(self, comment): - key = 'comment_feed:%d' % comment.id + key = "comment_feed:%d" % comment.id desc = cache.get(key) if desc is None: - desc = str(markdown(comment.body, 'comment')) + desc = str(markdown(comment.body, "comment")) desc = escape_xml_illegal_chars(desc) cache.set(key, desc, 86400) return desc @@ -80,21 +88,23 @@ class AtomCommentFeed(CommentFeed): class BlogFeed(Feed): - title = 'Latest %s Blog Posts' % settings.SITE_NAME - link = '/' - description = 'The latest blog posts from the %s' % settings.SITE_LONG_NAME + title = "Latest %s Blog Posts" % settings.SITE_NAME + link = "/" + description = "The latest blog posts from the %s" % settings.SITE_LONG_NAME def items(self): - return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on') + return BlogPost.objects.filter( + visible=True, publish_on__lte=timezone.now() + ).order_by("-sticky", "-publish_on") def item_title(self, post): return post.title def item_description(self, post): - key = 'blog_feed:%d' % post.id + key = "blog_feed:%d" % post.id summary = cache.get(key) if summary is None: - summary = str(markdown(post.summary or post.content, 'blog')) + summary = str(markdown(post.summary or post.content, "blog")) summary = escape_xml_illegal_chars(summary) cache.set(key, summary, 86400) return summary diff --git a/judge/forms.py b/judge/forms.py index d704fc5..a6166bc 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -12,159 +12,211 @@ from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django_ace import AceWidget -from judge.models import Contest, Language, Organization, PrivateMessage, Problem, ProblemPointsVote, Profile, Submission +from judge.models import ( + Contest, + Language, + Organization, + PrivateMessage, + Problem, + ProblemPointsVote, + Profile, + Submission, +) from judge.utils.subscription import newsletter_id -from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \ - Select2Widget +from judge.widgets import ( + HeavyPreviewPageDownWidget, + MathJaxPagedownWidget, + PagedownWidget, + Select2MultipleWidget, + Select2Widget, +) -def fix_unicode(string, unsafe=tuple('\u202a\u202b\u202d\u202e')): - return string + (sum(k in unsafe for k in string) - string.count('\u202c')) * '\u202c' +def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")): + return ( + string + (sum(k in unsafe for k in string) - string.count("\u202c")) * "\u202c" + ) class ProfileForm(ModelForm): if newsletter_id is not None: - newsletter = forms.BooleanField(label=_('Subscribe to contest updates'), initial=False, required=False) - test_site = forms.BooleanField(label=_('Enable experimental features'), initial=False, required=False) + newsletter = forms.BooleanField( + label=_("Subscribe to contest updates"), initial=False, required=False + ) + test_site = forms.BooleanField( + label=_("Enable experimental features"), initial=False, required=False + ) class Meta: model = Profile - fields = ['about', 'organizations', 'timezone', 'language', 'ace_theme', 'user_script'] + fields = [ + "about", + "organizations", + "timezone", + "language", + "ace_theme", + "user_script", + ] widgets = { - 'user_script': AceWidget(theme='github'), - 'timezone': Select2Widget(attrs={'style': 'width:200px'}), - 'language': Select2Widget(attrs={'style': 'width:200px'}), - 'ace_theme': Select2Widget(attrs={'style': 'width:200px'}), + "user_script": AceWidget(theme="github"), + "timezone": Select2Widget(attrs={"style": "width:200px"}), + "language": Select2Widget(attrs={"style": "width:200px"}), + "ace_theme": Select2Widget(attrs={"style": "width:200px"}), } has_math_config = bool(settings.MATHOID_URL) if has_math_config: - fields.append('math_engine') - widgets['math_engine'] = Select2Widget(attrs={'style': 'width:200px'}) + fields.append("math_engine") + widgets["math_engine"] = Select2Widget(attrs={"style": "width:200px"}) if HeavyPreviewPageDownWidget is not None: - widgets['about'] = HeavyPreviewPageDownWidget( - preview=reverse_lazy('profile_preview'), - attrs={'style': 'max-width:700px;min-width:700px;width:700px'}, + widgets["about"] = HeavyPreviewPageDownWidget( + preview=reverse_lazy("profile_preview"), + attrs={"style": "max-width:700px;min-width:700px;width:700px"}, ) def clean(self): - organizations = self.cleaned_data.get('organizations') or [] + organizations = self.cleaned_data.get("organizations") or [] max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT if sum(org.is_open for org in organizations) > max_orgs: raise ValidationError( - _('You may not be part of more than {count} public organizations.').format(count=max_orgs)) + _( + "You may not be part of more than {count} public organizations." + ).format(count=max_orgs) + ) return self.cleaned_data def __init__(self, *args, **kwargs): - user = kwargs.pop('user', None) + user = kwargs.pop("user", None) super(ProfileForm, self).__init__(*args, **kwargs) - if not user.has_perm('judge.edit_all_organization'): - self.fields['organizations'].queryset = Organization.objects.filter( + if not user.has_perm("judge.edit_all_organization"): + self.fields["organizations"].queryset = Organization.objects.filter( Q(is_open=True) | Q(id__in=user.profile.organizations.all()), ) class ProblemSubmitForm(ModelForm): - source = CharField(max_length=65536, widget=AceWidget(theme='twilight', no_ace_media=True)) + source = CharField( + max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True) + ) judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False) def __init__(self, *args, judge_choices=(), **kwargs): super(ProblemSubmitForm, self).__init__(*args, **kwargs) - self.fields['language'].empty_label = None - self.fields['language'].label_from_instance = attrgetter('display_name') - self.fields['language'].queryset = Language.objects.filter(judges__online=True).distinct() + self.fields["language"].empty_label = None + self.fields["language"].label_from_instance = attrgetter("display_name") + self.fields["language"].queryset = Language.objects.filter( + judges__online=True + ).distinct() if judge_choices: - self.fields['judge'].widget = Select2Widget( - attrs={'style': 'width: 150px', 'data-placeholder': _('Any judge')}, + self.fields["judge"].widget = Select2Widget( + attrs={"style": "width: 150px", "data-placeholder": _("Any judge")}, ) - self.fields['judge'].choices = judge_choices + self.fields["judge"].choices = judge_choices class Meta: model = Submission - fields = ['language'] + fields = ["language"] class EditOrganizationForm(ModelForm): class Meta: model = Organization - fields = ['about', 'logo_override_image', 'admins'] - widgets = {'admins': Select2MultipleWidget()} + fields = ["about", "logo_override_image", "admins"] + widgets = {"admins": Select2MultipleWidget()} if HeavyPreviewPageDownWidget is not None: - widgets['about'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('organization_preview')) + widgets["about"] = HeavyPreviewPageDownWidget( + preview=reverse_lazy("organization_preview") + ) class NewMessageForm(ModelForm): class Meta: model = PrivateMessage - fields = ['title', 'content'] + fields = ["title", "content"] widgets = {} if PagedownWidget is not None: - widgets['content'] = MathJaxPagedownWidget() + widgets["content"] = MathJaxPagedownWidget() class CustomAuthenticationForm(AuthenticationForm): def __init__(self, *args, **kwargs): super(CustomAuthenticationForm, self).__init__(*args, **kwargs) - self.fields['username'].widget.attrs.update({'placeholder': _('Username')}) - self.fields['password'].widget.attrs.update({'placeholder': _('Password')}) + self.fields["username"].widget.attrs.update({"placeholder": _("Username")}) + self.fields["password"].widget.attrs.update({"placeholder": _("Password")}) - self.has_google_auth = self._has_social_auth('GOOGLE_OAUTH2') - self.has_facebook_auth = self._has_social_auth('FACEBOOK') - self.has_github_auth = self._has_social_auth('GITHUB_SECURE') + self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2") + self.has_facebook_auth = self._has_social_auth("FACEBOOK") + self.has_github_auth = self._has_social_auth("GITHUB_SECURE") def _has_social_auth(self, key): - return (getattr(settings, 'SOCIAL_AUTH_%s_KEY' % key, None) and - getattr(settings, 'SOCIAL_AUTH_%s_SECRET' % key, None)) + return getattr(settings, "SOCIAL_AUTH_%s_KEY" % key, None) and getattr( + settings, "SOCIAL_AUTH_%s_SECRET" % key, None + ) class NoAutoCompleteCharField(forms.CharField): def widget_attrs(self, widget): attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget) - attrs['autocomplete'] = 'off' + attrs["autocomplete"] = "off" return attrs class TOTPForm(Form): TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES - totp_token = NoAutoCompleteCharField(validators=[ - RegexValidator('^[0-9]{6}$', _('Two Factor Authentication tokens must be 6 decimal digits.')), - ]) + totp_token = NoAutoCompleteCharField( + validators=[ + RegexValidator( + "^[0-9]{6}$", + _("Two Factor Authentication tokens must be 6 decimal digits."), + ), + ] + ) def __init__(self, *args, **kwargs): - self.totp_key = kwargs.pop('totp_key') + self.totp_key = kwargs.pop("totp_key") super(TOTPForm, self).__init__(*args, **kwargs) def clean_totp_token(self): - if not pyotp.TOTP(self.totp_key).verify(self.cleaned_data['totp_token'], valid_window=self.TOLERANCE): - raise ValidationError(_('Invalid Two Factor Authentication token.')) + if not pyotp.TOTP(self.totp_key).verify( + self.cleaned_data["totp_token"], valid_window=self.TOLERANCE + ): + raise ValidationError(_("Invalid Two Factor Authentication token.")) class ProblemCloneForm(Form): - code = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))]) + code = CharField( + max_length=20, + validators=[ + RegexValidator("^[a-z0-9]+$", _("Problem code must be ^[a-z0-9]+$")) + ], + ) def clean_code(self): - code = self.cleaned_data['code'] + code = self.cleaned_data["code"] if Problem.objects.filter(code=code).exists(): - raise ValidationError(_('Problem with code already exists.')) + raise ValidationError(_("Problem with code already exists.")) return code class ContestCloneForm(Form): - key = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))]) + key = CharField( + max_length=20, + validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))], + ) def clean_key(self): - key = self.cleaned_data['key'] + key = self.cleaned_data["key"] if Contest.objects.filter(key=key).exists(): - raise ValidationError(_('Contest with key already exists.')) + raise ValidationError(_("Contest with key already exists.")) return key class ProblemPointsVoteForm(ModelForm): class Meta: model = ProblemPointsVote - fields = ['points'] \ No newline at end of file + fields = ["points"] diff --git a/judge/fulltext.py b/judge/fulltext.py index 5b9f7d3..209a87e 100644 --- a/judge/fulltext.py +++ b/judge/fulltext.py @@ -5,10 +5,10 @@ from django.db.models.query import QuerySet class SearchQuerySet(QuerySet): - DEFAULT = '' - BOOLEAN = ' IN BOOLEAN MODE' - NATURAL_LANGUAGE = ' IN NATURAL LANGUAGE MODE' - QUERY_EXPANSION = ' WITH QUERY EXPANSION' + DEFAULT = "" + BOOLEAN = " IN BOOLEAN MODE" + NATURAL_LANGUAGE = " IN NATURAL LANGUAGE MODE" + QUERY_EXPANSION = " WITH QUERY EXPANSION" def __init__(self, fields=None, **kwargs): super(SearchQuerySet, self).__init__(**kwargs) @@ -25,20 +25,26 @@ class SearchQuerySet(QuerySet): # Get the table name and column names from the model # in `table_name`.`column_name` style columns = [meta.get_field(name).column for name in self._search_fields] - full_names = ['%s.%s' % - (connection.ops.quote_name(meta.db_table), - connection.ops.quote_name(column)) - for column in columns] + full_names = [ + "%s.%s" + % ( + connection.ops.quote_name(meta.db_table), + connection.ops.quote_name(column), + ) + for column in columns + ] # Create the MATCH...AGAINST expressions - fulltext_columns = ', '.join(full_names) - match_expr = ('MATCH(%s) AGAINST (%%s%s)' % (fulltext_columns, mode)) + fulltext_columns = ", ".join(full_names) + match_expr = "MATCH(%s) AGAINST (%%s%s)" % (fulltext_columns, mode) # Add the extra SELECT and WHERE options - return self.extra(select={'relevance': match_expr}, - select_params=[query], - where=[match_expr], - params=[query]) + return self.extra( + select={"relevance": match_expr}, + select_params=[query], + where=[match_expr], + params=[query], + ) class SearchManager(models.Manager): diff --git a/judge/highlight_code.py b/judge/highlight_code.py index 308a58e..ccef8d4 100644 --- a/judge/highlight_code.py +++ b/judge/highlight_code.py @@ -1,10 +1,10 @@ from django.utils.html import escape, mark_safe -__all__ = ['highlight_code'] +__all__ = ["highlight_code"] def _make_pre_code(code): - return mark_safe('
' + escape(code) + '
') + return mark_safe("
" + escape(code) + "
") def _wrap_code(inner): @@ -20,19 +20,28 @@ try: import pygments.formatters.html import pygments.util except ImportError: + def highlight_code(code, language, cssclass=None): return _make_pre_code(code) + else: + class HtmlCodeFormatter(pygments.formatters.HtmlFormatter): def wrap(self, source, outfile): return self._wrap_div(self._wrap_pre(_wrap_code(source))) - def highlight_code(code, language, cssclass='codehilite', linenos=True): + def highlight_code(code, language, cssclass="codehilite", linenos=True): try: lexer = pygments.lexers.get_lexer_by_name(language) except pygments.util.ClassNotFound: return _make_pre_code(code) if linenos: - return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos='table'))) - return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass))) + return mark_safe( + pygments.highlight( + code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos="table") + ) + ) + return mark_safe( + pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass)) + ) diff --git a/judge/jinja2/__init__.py b/judge/jinja2/__init__.py index 61ec4fd..93d0ed5 100644 --- a/judge/jinja2/__init__.py +++ b/judge/jinja2/__init__.py @@ -8,19 +8,33 @@ from statici18n.templatetags.statici18n import inlinei18n from judge.highlight_code import highlight_code from judge.user_translations import gettext -from . import (camo, chat, datetime, filesize, gravatar, language, markdown, rating, reference, render, social, - spaceless, submission, timedelta) +from . import ( + camo, + chat, + datetime, + filesize, + gravatar, + language, + markdown, + rating, + reference, + render, + social, + spaceless, + submission, + timedelta, +) from . import registry -registry.function('str', str) -registry.filter('str', str) -registry.filter('json', json.dumps) -registry.filter('highlight', highlight_code) -registry.filter('urlquote', urlquote) -registry.filter('roundfloat', round) -registry.function('inlinei18n', inlinei18n) -registry.function('mptt_tree', get_cached_trees) -registry.function('user_trans', gettext) +registry.function("str", str) +registry.filter("str", str) +registry.filter("json", json.dumps) +registry.filter("highlight", highlight_code) +registry.filter("urlquote", urlquote) +registry.filter("roundfloat", round) +registry.function("inlinei18n", inlinei18n) +registry.function("mptt_tree", get_cached_trees) +registry.function("user_trans", gettext) @registry.function diff --git a/judge/jinja2/camo.py b/judge/jinja2/camo.py index a60da79..1baeeb2 100644 --- a/judge/jinja2/camo.py +++ b/judge/jinja2/camo.py @@ -1,8 +1,9 @@ from judge.utils.camo import client as camo_client from . import registry + @registry.filter def camo(url): if camo_client is None: return url - return camo_client.rewrite_url(url) \ No newline at end of file + return camo_client.rewrite_url(url) diff --git a/judge/jinja2/chat.py b/judge/jinja2/chat.py index 07fec5b..564a748 100644 --- a/judge/jinja2/chat.py +++ b/judge/jinja2/chat.py @@ -1,6 +1,7 @@ from . import registry from chat_box.utils import encrypt_url + @registry.function def chat_param(request_profile, profile): - return encrypt_url(request_profile.id, profile.id) \ No newline at end of file + return encrypt_url(request_profile.id, profile.id) diff --git a/judge/jinja2/datetime.py b/judge/jinja2/datetime.py index 76c6bfc..eb0ec41 100644 --- a/judge/jinja2/datetime.py +++ b/judge/jinja2/datetime.py @@ -10,7 +10,7 @@ from . import registry def localtime_wrapper(func): @functools.wraps(func) def wrapper(datetime, *args, **kwargs): - if getattr(datetime, 'convert_to_local_time', True): + if getattr(datetime, "convert_to_local_time", True): datetime = localtime(datetime) return func(datetime, *args, **kwargs) @@ -22,6 +22,6 @@ registry.filter(localtime_wrapper(time)) @registry.function -@registry.render_with('widgets/relative-time.html') -def relative_time(time, format=_('N j, Y, g:i a'), rel=_('{time}'), abs=_('on {time}')): - return {'time': time, 'format': format, 'rel_format': rel, 'abs_format': abs} +@registry.render_with("widgets/relative-time.html") +def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("on {time}")): + return {"time": time, "format": format, "rel_format": rel, "abs_format": abs} diff --git a/judge/jinja2/filesize.py b/judge/jinja2/filesize.py index 7b27fde..0cb0fef 100644 --- a/judge/jinja2/filesize.py +++ b/judge/jinja2/filesize.py @@ -13,24 +13,28 @@ def _format_size(bytes, callback): PB = 1 << 50 if bytes < KB: - return callback('', bytes) + return callback("", bytes) elif bytes < MB: - return callback('K', bytes / KB) + return callback("K", bytes / KB) elif bytes < GB: - return callback('M', bytes / MB) + return callback("M", bytes / MB) elif bytes < TB: - return callback('G', bytes / GB) + return callback("G", bytes / GB) elif bytes < PB: - return callback('T', bytes / TB) + return callback("T", bytes / TB) else: - return callback('P', bytes / PB) + return callback("P", bytes / PB) @registry.filter def kbdetailformat(bytes): - return avoid_wrapping(_format_size(bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x))) + return avoid_wrapping( + _format_size( + bytes * 1024, lambda x, y: ["%d %sB", "%.2f %sB"][bool(x)] % (y, x) + ) + ) @registry.filter def kbsimpleformat(kb): - return _format_size(kb * 1024, lambda x, y: '%.0f%s' % (y, x or 'B')) + return _format_size(kb * 1024, lambda x, y: "%.0f%s" % (y, x or "B")) diff --git a/judge/jinja2/gravatar.py b/judge/jinja2/gravatar.py index 35b3fb8..2848d92 100644 --- a/judge/jinja2/gravatar.py +++ b/judge/jinja2/gravatar.py @@ -17,9 +17,13 @@ def gravatar(email, size=80, default=None): elif isinstance(email, AbstractUser): email = email.email - gravatar_url = '//www.gravatar.com/avatar/' + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + '?' - args = {'d': 'identicon', 's': str(size)} + gravatar_url = ( + "//www.gravatar.com/avatar/" + + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + + "?" + ) + args = {"d": "identicon", "s": str(size)} if default: - args['f'] = 'y' + args["f"] = "y" gravatar_url += urlencode(args) return gravatar_url diff --git a/judge/jinja2/language.py b/judge/jinja2/language.py index 344568a..dda4456 100644 --- a/judge/jinja2/language.py +++ b/judge/jinja2/language.py @@ -3,7 +3,7 @@ from django.utils import translation from . import registry -@registry.function('language_info') +@registry.function("language_info") def get_language_info(language): # ``language`` is either a language code string or a sequence # with the language code as its first item @@ -13,6 +13,6 @@ def get_language_info(language): return translation.get_language_info(str(language)) -@registry.function('language_info_list') +@registry.function("language_info_list") def get_language_info_list(langs): return [get_language_info(lang) for lang in langs] diff --git a/judge/jinja2/markdown/__init__.py b/judge/jinja2/markdown/__init__.py index 3cc5940..57af581 100644 --- a/judge/jinja2/markdown/__init__.py +++ b/judge/jinja2/markdown/__init__.py @@ -12,22 +12,28 @@ from lxml.etree import ParserError, XMLSyntaxError from judge.highlight_code import highlight_code from judge.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor from judge.jinja2.markdown.math import MathInlineGrammar, MathInlineLexer, MathRenderer -from judge.jinja2.markdown.spoiler import SpoilerInlineGrammar, SpoilerInlineLexer, SpoilerRenderer +from judge.jinja2.markdown.spoiler import ( + SpoilerInlineGrammar, + SpoilerInlineLexer, + SpoilerRenderer, +) from judge.utils.camo import client as camo_client from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer from .. import registry -logger = logging.getLogger('judge.html') +logger = logging.getLogger("judge.html") NOFOLLOW_WHITELIST = settings.NOFOLLOW_EXCLUDED class CodeSafeInlineGrammar(mistune.InlineGrammar): - double_emphasis = re.compile(r'^\*{2}([\s\S]+?)()\*{2}(?!\*)') # **word** - emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word* + double_emphasis = re.compile(r"^\*{2}([\s\S]+?)()\*{2}(?!\*)") # **word** + emphasis = re.compile(r"^\*((?:\*\*|[^\*])+?)()\*(?!\*)") # *word* -class AwesomeInlineGrammar(MathInlineGrammar, SpoilerInlineGrammar, CodeSafeInlineGrammar): +class AwesomeInlineGrammar( + MathInlineGrammar, SpoilerInlineGrammar, CodeSafeInlineGrammar +): pass @@ -37,8 +43,8 @@ class AwesomeInlineLexer(MathInlineLexer, SpoilerInlineLexer, mistune.InlineLexe class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer): def __init__(self, *args, **kwargs): - self.nofollow = kwargs.pop('nofollow', True) - self.texoid = TexoidRenderer() if kwargs.pop('texoid', False) else None + self.nofollow = kwargs.pop("nofollow", True) + self.texoid = TexoidRenderer() if kwargs.pop("texoid", False) else None self.parser = HTMLParser() super(AwesomeRenderer, self).__init__(*args, **kwargs) @@ -51,18 +57,18 @@ class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer): else: if url.netloc and url.netloc not in NOFOLLOW_WHITELIST: return ' rel="nofollow"' - return '' + return "" def autolink(self, link, is_email=False): text = link = mistune.escape(link) if is_email: - link = 'mailto:%s' % link + link = "mailto:%s" % link return '%s' % (link, self._link_rel(link), text) def table(self, header, body): return ( '\n%s\n' - '\n%s\n
\n' + "\n%s\n\n" ) % (header, body) def link(self, link, title, text): @@ -70,40 +76,53 @@ class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer): if not title: return '%s' % (link, self._link_rel(link), text) title = mistune.escape(title, quote=True) - return '%s' % (link, title, self._link_rel(link), text) + return '%s' % ( + link, + title, + self._link_rel(link), + text, + ) def block_code(self, code, lang=None): if not lang: - return '\n
%s
\n' % mistune.escape(code).rstrip() + return "\n
%s
\n" % mistune.escape(code).rstrip() return highlight_code(code, lang) def block_html(self, html): - if self.texoid and html.startswith('')] - latex = html[html.index('>') + 1:html.rindex('<')] + if self.texoid and html.startswith("")] + latex = html[html.index(">") + 1 : html.rindex("<")] latex = self.parser.unescape(latex) result = self.texoid.get_result(latex) if not result: - return '
%s
' % mistune.escape(latex, smart_amp=False) - elif 'error' not in result: - img = ('''') % { - 'svg': result['svg'], 'png': result['png'], - 'width': result['meta']['width'], 'height': result['meta']['height'], - 'tail': ' /' if self.options.get('use_xhtml') else '', + return "
%s
" % mistune.escape(latex, smart_amp=False) + elif "error" not in result: + img = ( + '''' + ) % { + "svg": result["svg"], + "png": result["png"], + "width": result["meta"]["width"], + "height": result["meta"]["height"], + "tail": " /" if self.options.get("use_xhtml") else "", } - style = ['max-width: 100%', - 'height: %s' % result['meta']['height'], - 'max-height: %s' % result['meta']['height'], - 'width: %s' % result['meta']['height']] - if 'inline' in attr: - tag = 'span' + style = [ + "max-width: 100%", + "height: %s" % result["meta"]["height"], + "max-height: %s" % result["meta"]["height"], + "width: %s" % result["meta"]["height"], + ] + if "inline" in attr: + tag = "span" else: - tag = 'div' - style += ['text-align: center'] - return '<%s style="%s">%s' % (tag, ';'.join(style), img, tag) + tag = "div" + style += ["text-align: center"] + return '<%s style="%s">%s' % (tag, ";".join(style), img, tag) else: - return '
%s
' % mistune.escape(result['error'], smart_amp=False) + return "
%s
" % mistune.escape( + result["error"], smart_amp=False + ) return super(AwesomeRenderer, self).block_html(html) def header(self, text, level, *args, **kwargs): @@ -113,30 +132,41 @@ class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer): @registry.filter def markdown(value, style, math_engine=None, lazy_load=False): styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE) - escape = styles.get('safe_mode', True) - nofollow = styles.get('nofollow', True) - texoid = TEXOID_ENABLED and styles.get('texoid', False) - math = hasattr(settings, 'MATHOID_URL') and styles.get('math', False) + escape = styles.get("safe_mode", True) + nofollow = styles.get("nofollow", True) + texoid = TEXOID_ENABLED and styles.get("texoid", False) + math = hasattr(settings, "MATHOID_URL") and styles.get("math", False) post_processors = [] - if styles.get('use_camo', False) and camo_client is not None: + if styles.get("use_camo", False) and camo_client is not None: post_processors.append(camo_client.update_tree) if lazy_load: post_processors.append(lazy_load_processor) - renderer = AwesomeRenderer(escape=escape, nofollow=nofollow, texoid=texoid, - math=math and math_engine is not None, math_engine=math_engine) - markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer, - parse_block_html=1, parse_inline_html=1) + renderer = AwesomeRenderer( + escape=escape, + nofollow=nofollow, + texoid=texoid, + math=math and math_engine is not None, + math_engine=math_engine, + ) + markdown = mistune.Markdown( + renderer=renderer, + inline=AwesomeInlineLexer, + parse_block_html=1, + parse_inline_html=1, + ) result = markdown(value) if post_processors: try: tree = html.fromstring(result, parser=html.HTMLParser(recover=True)) except (XMLSyntaxError, ParserError) as e: - if result and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'): - logger.exception('Failed to parse HTML string') - tree = html.Element('div') + if result and ( + not isinstance(e, ParserError) or e.args[0] != "Document is empty" + ): + logger.exception("Failed to parse HTML string") + tree = html.Element("div") for processor in post_processors: processor(tree) - result = html.tostring(tree, encoding='unicode') + result = html.tostring(tree, encoding="unicode") return Markup(result) diff --git a/judge/jinja2/markdown/lazy_load.py b/judge/jinja2/markdown/lazy_load.py index cf9849c..56f5347 100644 --- a/judge/jinja2/markdown/lazy_load.py +++ b/judge/jinja2/markdown/lazy_load.py @@ -5,16 +5,16 @@ from lxml import html def lazy_load(tree): - blank = static('blank.gif') - for img in tree.xpath('.//img'): - src = img.get('src', '') - if src.startswith('data') or '-math' in img.get('class', ''): + blank = static("blank.gif") + for img in tree.xpath(".//img"): + src = img.get("src", "") + if src.startswith("data") or "-math" in img.get("class", ""): continue - noscript = html.Element('noscript') + noscript = html.Element("noscript") copy = deepcopy(img) - copy.tail = '' + copy.tail = "" noscript.append(copy) img.addprevious(noscript) - img.set('data-src', src) - img.set('src', blank) - img.set('class', img.get('class') + ' unveil' if img.get('class') else 'unveil') + img.set("data-src", src) + img.set("src", blank) + img.set("class", img.get("class") + " unveil" if img.get("class") else "unveil") diff --git a/judge/jinja2/markdown/math.py b/judge/jinja2/markdown/math.py index 08883c5..1b5f4ce 100644 --- a/judge/jinja2/markdown/math.py +++ b/judge/jinja2/markdown/math.py @@ -6,13 +6,13 @@ from django.conf import settings from judge.utils.mathoid import MathoidMathParser -mistune._pre_tags.append('latex') +mistune._pre_tags.append("latex") class MathInlineGrammar(mistune.InlineGrammar): - block_math = re.compile(r'^\$\$(.*?)\$\$|^\\\[(.*?)\\\]', re.DOTALL) - math = re.compile(r'^~(.*?)~|^\\\((.*?)\\\)', re.DOTALL) - text = re.compile(r'^[\s\S]+?(?=[\\%s' % (tag, extra, text, tag) + extra = m.group(2) or "" + html = "<%s%s>%s" % (tag, extra, text, tag) else: html = m.group(0) return self.renderer.inline_html(html) @@ -50,18 +52,18 @@ class MathInlineLexer(mistune.InlineLexer): class MathRenderer(mistune.Renderer): def __init__(self, *args, **kwargs): - if kwargs.pop('math', False) and settings.MATHOID_URL != False: - self.mathoid = MathoidMathParser(kwargs.pop('math_engine', None) or 'svg') + if kwargs.pop("math", False) and settings.MATHOID_URL != False: + self.mathoid = MathoidMathParser(kwargs.pop("math_engine", None) or "svg") else: self.mathoid = None super(MathRenderer, self).__init__(*args, **kwargs) def block_math(self, math): if self.mathoid is None or not math: - return r'\[%s\]' % mistune.escape(str(math)) + return r"\[%s\]" % mistune.escape(str(math)) return self.mathoid.display_math(math) def math(self, math): if self.mathoid is None or not math: - return r'\(%s\)' % mistune.escape(str(math)) - return self.mathoid.inline_math(math) \ No newline at end of file + return r"\(%s\)" % mistune.escape(str(math)) + return self.mathoid.inline_math(math) diff --git a/judge/jinja2/markdown/spoiler.py b/judge/jinja2/markdown/spoiler.py index 5df7950..8717b8d 100644 --- a/judge/jinja2/markdown/spoiler.py +++ b/judge/jinja2/markdown/spoiler.py @@ -3,25 +3,28 @@ import mistune class SpoilerInlineGrammar(mistune.InlineGrammar): - spoiler = re.compile(r'^\|\|(.+?)\s+([\s\S]+?)\s*\|\|') + spoiler = re.compile(r"^\|\|(.+?)\s+([\s\S]+?)\s*\|\|") class SpoilerInlineLexer(mistune.InlineLexer): grammar_class = SpoilerInlineGrammar def __init__(self, *args, **kwargs): - self.default_rules.insert(0, 'spoiler') + self.default_rules.insert(0, "spoiler") super(SpoilerInlineLexer, self).__init__(*args, **kwargs) - + def output_spoiler(self, m): return self.renderer.spoiler(m.group(1), m.group(2)) class SpoilerRenderer(mistune.Renderer): def spoiler(self, summary, text): - return '''
+ return """
%s
%s
-
''' % (summary, text) \ No newline at end of file +
""" % ( + summary, + text, + ) diff --git a/judge/jinja2/rating.py b/judge/jinja2/rating.py index b531bb7..3e4fc88 100644 --- a/judge/jinja2/rating.py +++ b/judge/jinja2/rating.py @@ -14,22 +14,22 @@ def _get_rating_value(func, obj): return func(obj.rating) -@registry.function('rating_class') +@registry.function("rating_class") def get_rating_class(obj): - return _get_rating_value(rating_class, obj) or 'rate-none' + return _get_rating_value(rating_class, obj) or "rate-none" -@registry.function(name='rating_name') +@registry.function(name="rating_name") def get_name(obj): - return _get_rating_value(rating_name, obj) or 'Unrated' + return _get_rating_value(rating_name, obj) or "Unrated" -@registry.function(name='rating_progress') +@registry.function(name="rating_progress") def get_progress(obj): return _get_rating_value(rating_progress, obj) or 0.0 @registry.function -@registry.render_with('user/rating.html') +@registry.render_with("user/rating.html") def rating_number(obj): - return {'rating': obj} + return {"rating": obj} diff --git a/judge/jinja2/reference.py b/judge/jinja2/reference.py index 184e109..49a6ef2 100644 --- a/judge/jinja2/reference.py +++ b/judge/jinja2/reference.py @@ -13,17 +13,17 @@ from judge.models import Contest, Problem, Profile from judge.ratings import rating_class, rating_progress from . import registry -rereference = re.compile(r'\[(r?user):(\w+)\]') +rereference = re.compile(r"\[(r?user):(\w+)\]") def get_user(username, data): if not data: - element = Element('span') + element = Element("span") element.text = username return element - element = Element('span', {'class': Profile.get_user_css_class(*data)}) - link = Element('a', {'href': reverse('user_page', args=[username])}) + element = Element("span", {"class": Profile.get_user_css_class(*data)}) + link = Element("a", {"href": reverse("user_page", args=[username])}) link.text = username element.append(link) return element @@ -31,17 +31,21 @@ def get_user(username, data): def get_user_rating(username, data): if not data: - element = Element('span') + element = Element("span") element.text = username return element rating = data[1] - element = Element('a', {'class': 'rate-group', 'href': reverse('user_page', args=[username])}) + element = Element( + "a", {"class": "rate-group", "href": reverse("user_page", args=[username])} + ) if rating: rating_css = rating_class(rating) - rate_box = Element('span', {'class': 'rate-box ' + rating_css}) - rate_box.append(Element('span', {'style': 'height: %3.fem' % rating_progress(rating)})) - user = Element('span', {'class': 'rating ' + rating_css}) + rate_box = Element("span", {"class": "rate-box " + rating_css}) + rate_box.append( + Element("span", {"style": "height: %3.fem" % rating_progress(rating)}) + ) + user = Element("span", {"class": "rating " + rating_css}) user.text = username element.append(rate_box) element.append(user) @@ -51,21 +55,24 @@ def get_user_rating(username, data): def get_user_info(usernames): - return {name: (rank, rating) for name, rank, rating in - Profile.objects.filter(user__username__in=usernames) - .values_list('user__username', 'display_rank', 'rating')} + return { + name: (rank, rating) + for name, rank, rating in Profile.objects.filter( + user__username__in=usernames + ).values_list("user__username", "display_rank", "rating") + } def get_user_from_text(text): user_list = set() for i in rereference.finditer(text): - user_list.add(text[i.start() + 6: i.end() - 1]) + user_list.add(text[i.start() + 6 : i.end() - 1]) return Profile.objects.filter(user__username__in=user_list) reference_map = { - 'user': (get_user, get_user_info), - 'ruser': (get_user_rating, get_user_info), + "user": (get_user, get_user_info), + "ruser": (get_user_rating, get_user_info), } @@ -77,9 +84,9 @@ def process_reference(text): elements = [] for piece in rereference.finditer(text): if prev is None: - tail = text[last:piece.start()] + tail = text[last : piece.start()] else: - prev.append(text[last:piece.start()]) + prev.append(text[last : piece.start()]) prev = list(piece.groups()) elements.append(prev) last = piece.end() @@ -143,52 +150,52 @@ def item_title(item): return item.name elif isinstance(item, Contest): return item.name - return '' + return "" @registry.function -@registry.render_with('user/link.html') +@registry.render_with("user/link.html") def link_user(user): if isinstance(user, Profile): user, profile = user.user, user elif isinstance(user, AbstractUser): profile = user.profile - elif type(user).__name__ == 'ContestRankingProfile': + elif type(user).__name__ == "ContestRankingProfile": user, profile = user.user, user else: - raise ValueError('Expected profile or user, got %s' % (type(user),)) - return {'user': user, 'profile': profile} + raise ValueError("Expected profile or user, got %s" % (type(user),)) + return {"user": user, "profile": profile} @registry.function -@registry.render_with('user/link-list.html') +@registry.render_with("user/link-list.html") def link_users(users): - return {'users': users} + return {"users": users} @registry.function -@registry.render_with('runtime-version-fragment.html') +@registry.render_with("runtime-version-fragment.html") def runtime_versions(versions): - return {'runtime_versions': versions} + return {"runtime_versions": versions} -@registry.filter(name='absolutify') +@registry.filter(name="absolutify") def absolute_links(text, url): tree = lxml_tree.fromstring(text) - for anchor in tree.xpath('.//a'): - href = anchor.get('href') + for anchor in tree.xpath(".//a"): + href = anchor.get("href") if href: - anchor.set('href', urljoin(url, href)) + anchor.set("href", urljoin(url, href)) return tree -@registry.function(name='urljoin') +@registry.function(name="urljoin") def join(first, second, *rest): if not rest: return urljoin(first, second) return urljoin(urljoin(first, second), *rest) -@registry.filter(name='ansi2html') +@registry.filter(name="ansi2html") def ansi2html(s): return mark_safe(Ansi2HTMLConverter(inline=True).convert(s, full=False)) diff --git a/judge/jinja2/registry.py b/judge/jinja2/registry.py index da12166..21d3b85 100644 --- a/judge/jinja2/registry.py +++ b/judge/jinja2/registry.py @@ -5,7 +5,7 @@ tests = {} filters = {} extensions = [] -__all__ = ['render_with', 'function', 'filter', 'test', 'extension'] +__all__ = ["render_with", "function", "filter", "test", "extension"] def _store_function(store, func, name=None): @@ -16,6 +16,7 @@ def _store_function(store, func, name=None): def _register_function(store, name, func): if name is None and func is None: + def decorator(func): _store_function(store, func) return func @@ -26,6 +27,7 @@ def _register_function(store, name, func): _store_function(store, name) return name else: + def decorator(func): _store_function(store, func, name) return func diff --git a/judge/jinja2/render.py b/judge/jinja2/render.py index 778e26a..bea8c7c 100644 --- a/judge/jinja2/render.py +++ b/judge/jinja2/render.py @@ -1,5 +1,9 @@ -from django.template import (Context, Template as DjangoTemplate, TemplateSyntaxError as DjangoTemplateSyntaxError, - VariableDoesNotExist) +from django.template import ( + Context, + Template as DjangoTemplate, + TemplateSyntaxError as DjangoTemplateSyntaxError, + VariableDoesNotExist, +) from . import registry @@ -24,4 +28,4 @@ def render_django(template, **context): try: return compile_template(template).render(Context(context)) except (VariableDoesNotExist, DjangoTemplateSyntaxError): - return 'Error rendering: %r' % template + return "Error rendering: %r" % template diff --git a/judge/jinja2/social.py b/judge/jinja2/social.py index 9f82971..645809d 100644 --- a/judge/jinja2/social.py +++ b/judge/jinja2/social.py @@ -1,13 +1,29 @@ from django.template.loader import get_template from django.utils.safestring import mark_safe -from django_social_share.templatetags.social_share import post_to_facebook_url, post_to_gplus_url, post_to_twitter_url +from django_social_share.templatetags.social_share import ( + post_to_facebook_url, + post_to_gplus_url, + post_to_twitter_url, +) from . import registry SHARES = [ - ('post_to_twitter', 'django_social_share/templatetags/post_to_twitter.html', post_to_twitter_url), - ('post_to_facebook', 'django_social_share/templatetags/post_to_facebook.html', post_to_facebook_url), - ('post_to_gplus', 'django_social_share/templatetags/post_to_gplus.html', post_to_gplus_url), + ( + "post_to_twitter", + "django_social_share/templatetags/post_to_twitter.html", + post_to_twitter_url, + ), + ( + "post_to_facebook", + "django_social_share/templatetags/post_to_facebook.html", + post_to_facebook_url, + ), + ( + "post_to_gplus", + "django_social_share/templatetags/post_to_gplus.html", + post_to_gplus_url, + ), # For future versions: # ('post_to_linkedin', 'django_social_share/templatetags/post_to_linkedin.html', post_to_linkedin_url), # ('post_to_reddit', 'django_social_share/templatetags/post_to_reddit.html', post_to_reddit_url), @@ -17,7 +33,7 @@ SHARES = [ def make_func(name, template, url_func): def func(request, *args): link_text = args[-1] - context = {'request': request, 'link_text': mark_safe(link_text)} + context = {"request": request, "link_text": mark_safe(link_text)} context = url_func(context, *args[:-1]) return mark_safe(get_template(template).render(context)) @@ -31,4 +47,6 @@ for name, template, url_func in SHARES: @registry.function def recaptcha_init(language=None): - return get_template('snowpenguin/recaptcha/recaptcha_init.html').render({'explicit': False, 'language': language}) + return get_template("snowpenguin/recaptcha/recaptcha_init.html").render( + {"explicit": False, "language": language} + ) diff --git a/judge/jinja2/spaceless.py b/judge/jinja2/spaceless.py index 81c5186..0644e89 100644 --- a/judge/jinja2/spaceless.py +++ b/judge/jinja2/spaceless.py @@ -15,15 +15,17 @@ class SpacelessExtension(Extension): https://stackoverflow.com/a/23741298/1090657 """ - tags = {'spaceless'} + tags = {"spaceless"} def parse(self, parser): lineno = next(parser.stream).lineno - body = parser.parse_statements(['name:endspaceless'], drop_needle=True) + body = parser.parse_statements(["name:endspaceless"], drop_needle=True) return nodes.CallBlock( - self.call_method('_strip_spaces', [], [], None, None), - [], [], body, + self.call_method("_strip_spaces", [], [], None, None), + [], + [], + body, ).set_lineno(lineno) def _strip_spaces(self, caller=None): - return Markup(re.sub(r'>\s+<', '><', caller().unescape().strip())) + return Markup(re.sub(r">\s+<", "><", caller().unescape().strip())) diff --git a/judge/jinja2/submission.py b/judge/jinja2/submission.py index 47765b2..bdf5650 100644 --- a/judge/jinja2/submission.py +++ b/judge/jinja2/submission.py @@ -2,7 +2,9 @@ from . import registry @registry.function -def submission_layout(submission, profile_id, user, editable_problem_ids, completed_problem_ids): +def submission_layout( + submission, profile_id, user, editable_problem_ids, completed_problem_ids +): problem_id = submission.problem_id can_view = False @@ -12,13 +14,15 @@ def submission_layout(submission, profile_id, user, editable_problem_ids, comple if profile_id == submission.user_id: can_view = True - if user.has_perm('judge.change_submission'): + if user.has_perm("judge.change_submission"): can_view = True if submission.problem_id in completed_problem_ids: - can_view |= submission.problem.is_public or profile_id in submission.problem.tester_ids + can_view |= ( + submission.problem.is_public or profile_id in submission.problem.tester_ids + ) - if not can_view and hasattr(submission, 'contest'): + if not can_view and hasattr(submission, "contest"): contest = submission.contest.participation.contest if contest.is_editable_by(user): can_view = True diff --git a/judge/jinja2/timedelta.py b/judge/jinja2/timedelta.py index 2069610..95acff6 100644 --- a/judge/jinja2/timedelta.py +++ b/judge/jinja2/timedelta.py @@ -5,14 +5,14 @@ from . import registry @registry.filter -def timedelta(value, display='long'): +def timedelta(value, display="long"): if value is None: return value return nice_repr(value, display) @registry.filter -def timestampdelta(value, display='long'): +def timestampdelta(value, display="long"): value = datetime.timedelta(seconds=value) return timedelta(value, display) @@ -23,8 +23,8 @@ def seconds(timedelta): @registry.filter -@registry.render_with('time-remaining-fragment.html') +@registry.render_with("time-remaining-fragment.html") def as_countdown(time): time_now = datetime.datetime.now(datetime.timezone.utc) initial = abs(time - time_now) - return {'countdown': time, 'initial': initial} + return {"countdown": time, "initial": initial} diff --git a/judge/judgeapi.py b/judge/judgeapi.py index 57627b2..96a2153 100644 --- a/judge/judgeapi.py +++ b/judge/judgeapi.py @@ -8,43 +8,51 @@ from django.conf import settings from judge import event_poster as event -logger = logging.getLogger('judge.judgeapi') -size_pack = struct.Struct('!I') +logger = logging.getLogger("judge.judgeapi") +size_pack = struct.Struct("!I") def _post_update_submission(submission, done=False): if submission.problem.is_public: - event.post('submissions', {'type': 'done-submission' if done else 'update-submission', - 'id': submission.id, - 'contest': submission.contest_key, - 'user': submission.user_id, 'problem': submission.problem_id, - 'status': submission.status, 'language': submission.language.key}) + event.post( + "submissions", + { + "type": "done-submission" if done else "update-submission", + "id": submission.id, + "contest": submission.contest_key, + "user": submission.user_id, + "problem": submission.problem_id, + "status": submission.status, + "language": submission.language.key, + }, + ) def judge_request(packet, reply=True): - sock = socket.create_connection(settings.BRIDGED_DJANGO_CONNECT or - settings.BRIDGED_DJANGO_ADDRESS[0]) + sock = socket.create_connection( + settings.BRIDGED_DJANGO_CONNECT or settings.BRIDGED_DJANGO_ADDRESS[0] + ) - output = json.dumps(packet, separators=(',', ':')) - output = zlib.compress(output.encode('utf-8')) - writer = sock.makefile('wb') + output = json.dumps(packet, separators=(",", ":")) + output = zlib.compress(output.encode("utf-8")) + writer = sock.makefile("wb") writer.write(size_pack.pack(len(output))) writer.write(output) writer.close() if reply: - reader = sock.makefile('rb', -1) + reader = sock.makefile("rb", -1) input = reader.read(size_pack.size) if not input: - raise ValueError('Judge did not respond') + raise ValueError("Judge did not respond") length = size_pack.unpack(input)[0] input = reader.read(length) if not input: - raise ValueError('Judge did not respond') + raise ValueError("Judge did not respond") reader.close() sock.close() - result = json.loads(zlib.decompress(input).decode('utf-8')) + result = json.loads(zlib.decompress(input).decode("utf-8")) return result @@ -56,13 +64,23 @@ def judge_submission(submission, rejudge=False, batch_rejudge=False, judge_id=No REJUDGE_PRIORITY = 2 BATCH_REJUDGE_PRIORITY = 3 - updates = {'time': None, 'memory': None, 'points': None, 'result': None, 'error': None, - 'was_rejudged': rejudge, 'status': 'QU'} + updates = { + "time": None, + "memory": None, + "points": None, + "result": None, + "error": None, + "was_rejudged": rejudge, + "status": "QU", + } try: # This is set proactively; it might get unset in judgecallback's on_grading_begin if the problem doesn't # actually have pretests stored on the judge. - updates['is_pretested'] = all(ContestSubmission.objects.filter(submission=submission) - .values_list('problem__contest__run_pretests_only', 'problem__is_pretested')[0]) + updates["is_pretested"] = all( + ContestSubmission.objects.filter(submission=submission).values_list( + "problem__contest__run_pretests_only", "problem__is_pretested" + )[0] + ) except IndexError: priority = DEFAULT_PRIORITY else: @@ -76,43 +94,65 @@ def judge_submission(submission, rejudge=False, batch_rejudge=False, judge_id=No # as that would prevent people from knowing a submission is being scheduled for rejudging. # It is worth noting that this mechanism does not prevent a new rejudge from being scheduled # while already queued, but that does not lead to data corruption. - if not Submission.objects.filter(id=submission.id).exclude(status__in=('P', 'G')).update(**updates): + if ( + not Submission.objects.filter(id=submission.id) + .exclude(status__in=("P", "G")) + .update(**updates) + ): return False SubmissionTestCase.objects.filter(submission_id=submission.id).delete() try: - response = judge_request({ - 'name': 'submission-request', - 'submission-id': submission.id, - 'problem-id': submission.problem.code, - 'language': submission.language.key, - 'source': submission.source.source, - 'judge-id': judge_id, - 'priority': BATCH_REJUDGE_PRIORITY if batch_rejudge else REJUDGE_PRIORITY if rejudge else priority, - }) + response = judge_request( + { + "name": "submission-request", + "submission-id": submission.id, + "problem-id": submission.problem.code, + "language": submission.language.key, + "source": submission.source.source, + "judge-id": judge_id, + "priority": BATCH_REJUDGE_PRIORITY + if batch_rejudge + else REJUDGE_PRIORITY + if rejudge + else priority, + } + ) except BaseException: - logger.exception('Failed to send request to judge') - Submission.objects.filter(id=submission.id).update(status='IE', result='IE') + logger.exception("Failed to send request to judge") + Submission.objects.filter(id=submission.id).update(status="IE", result="IE") success = False else: - if response['name'] != 'submission-received' or response['submission-id'] != submission.id: - Submission.objects.filter(id=submission.id).update(status='IE', result='IE') + if ( + response["name"] != "submission-received" + or response["submission-id"] != submission.id + ): + Submission.objects.filter(id=submission.id).update(status="IE", result="IE") _post_update_submission(submission) success = True return success def disconnect_judge(judge, force=False): - judge_request({'name': 'disconnect-judge', 'judge-id': judge.name, 'force': force}, reply=False) + judge_request( + {"name": "disconnect-judge", "judge-id": judge.name, "force": force}, + reply=False, + ) def abort_submission(submission): from .models import Submission - response = judge_request({'name': 'terminate-submission', 'submission-id': submission.id}) + + response = judge_request( + {"name": "terminate-submission", "submission-id": submission.id} + ) # This defaults to true, so that in the case the JudgeList fails to remove the submission from the queue, # and returns a bad-request, the submission is not falsely shown as "Aborted" when it will still be judged. - if not response.get('judge-aborted', True): - Submission.objects.filter(id=submission.id).update(status='AB', result='AB') - event.post('sub_%s' % Submission.get_id_secret(submission.id), {'type': 'aborted-submission'}) - _post_update_submission(submission, done=True) \ No newline at end of file + if not response.get("judge-aborted", True): + Submission.objects.filter(id=submission.id).update(status="AB", result="AB") + event.post( + "sub_%s" % Submission.get_id_secret(submission.id), + {"type": "aborted-submission"}, + ) + _post_update_submission(submission, done=True) diff --git a/judge/lxml_tree.py b/judge/lxml_tree.py index 014f7dc..bb71840 100644 --- a/judge/lxml_tree.py +++ b/judge/lxml_tree.py @@ -4,7 +4,7 @@ from django.utils.safestring import SafeData, mark_safe from lxml import html from lxml.etree import ParserError, XMLSyntaxError -logger = logging.getLogger('judge.html') +logger = logging.getLogger("judge.html") class HTMLTreeString(SafeData): @@ -12,9 +12,11 @@ class HTMLTreeString(SafeData): try: self._tree = html.fromstring(str, parser=html.HTMLParser(recover=True)) except (XMLSyntaxError, ParserError) as e: - if str and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'): - logger.exception('Failed to parse HTML string') - self._tree = html.Element('div') + if str and ( + not isinstance(e, ParserError) or e.args[0] != "Document is empty" + ): + logger.exception("Failed to parse HTML string") + self._tree = html.Element("div") def __getattr__(self, attr): try: @@ -23,15 +25,15 @@ class HTMLTreeString(SafeData): return getattr(str(self), attr) def __setattr__(self, key, value): - if key[0] == '_': + if key[0] == "_": super(HTMLTreeString, self).__setattr__(key, value) setattr(self._tree, key, value) def __repr__(self): - return '' % str(self) + return "" % str(self) def __str__(self): - return mark_safe(html.tostring(self._tree, encoding='unicode')) + return mark_safe(html.tostring(self._tree, encoding="unicode")) def __radd__(self, other): return other + str(self) diff --git a/judge/management/commands/addjudge.py b/judge/management/commands/addjudge.py index 699b3ed..f9ad589 100644 --- a/judge/management/commands/addjudge.py +++ b/judge/management/commands/addjudge.py @@ -4,15 +4,14 @@ from judge.models import Judge class Command(BaseCommand): - help = 'create a judge' + help = "create a judge" def add_arguments(self, parser): - parser.add_argument('name', help='the name of the judge') - parser.add_argument('auth_key', help='authentication key for the judge') + parser.add_argument("name", help="the name of the judge") + parser.add_argument("auth_key", help="authentication key for the judge") def handle(self, *args, **options): judge = Judge() - judge.name = options['name'] - judge.auth_key = options['auth_key'] + judge.name = options["name"] + judge.auth_key = options["auth_key"] judge.save() - diff --git a/judge/management/commands/adduser.py b/judge/management/commands/adduser.py index f214f4a..640625c 100644 --- a/judge/management/commands/adduser.py +++ b/judge/management/commands/adduser.py @@ -6,27 +6,39 @@ from judge.models import Language, Profile class Command(BaseCommand): - help = 'creates a user' + help = "creates a user" def add_arguments(self, parser): - parser.add_argument('name', help='username') - parser.add_argument('email', help='email, not necessary to be resolvable') - parser.add_argument('password', help='password for the user') - parser.add_argument('language', nargs='?', default=settings.DEFAULT_USER_LANGUAGE, - help='default language ID for user') + parser.add_argument("name", help="username") + parser.add_argument("email", help="email, not necessary to be resolvable") + parser.add_argument("password", help="password for the user") + parser.add_argument( + "language", + nargs="?", + default=settings.DEFAULT_USER_LANGUAGE, + help="default language ID for user", + ) - parser.add_argument('--superuser', action='store_true', default=False, - help="if specified, creates user with superuser privileges") - parser.add_argument('--staff', action='store_true', default=False, - help="if specified, creates user with staff privileges") + parser.add_argument( + "--superuser", + action="store_true", + default=False, + help="if specified, creates user with superuser privileges", + ) + parser.add_argument( + "--staff", + action="store_true", + default=False, + help="if specified, creates user with staff privileges", + ) def handle(self, *args, **options): - usr = User(username=options['name'], email=options['email'], is_active=True) - usr.set_password(options['password']) - usr.is_superuser = options['superuser'] - usr.is_staff = options['staff'] + usr = User(username=options["name"], email=options["email"], is_active=True) + usr.set_password(options["password"]) + usr.is_superuser = options["superuser"] + usr.is_staff = options["staff"] usr.save() profile = Profile(user=usr) - profile.language = Language.objects.get(key=options['language']) + profile.language = Language.objects.get(key=options["language"]) profile.save() diff --git a/judge/management/commands/camo.py b/judge/management/commands/camo.py index aa65521..774837a 100644 --- a/judge/management/commands/camo.py +++ b/judge/management/commands/camo.py @@ -4,13 +4,13 @@ from judge.utils.camo import client as camo_client class Command(BaseCommand): - help = 'obtains the camo url for the specified url' + help = "obtains the camo url for the specified url" def add_arguments(self, parser): - parser.add_argument('url', help='url to use camo on') + parser.add_argument("url", help="url to use camo on") def handle(self, *args, **options): if camo_client is None: - raise CommandError('Camo not available') + raise CommandError("Camo not available") - print(camo_client.image_url(options['url'])) + print(camo_client.image_url(options["url"])) diff --git a/judge/management/commands/copy_language.py b/judge/management/commands/copy_language.py index 233ccc7..09120a6 100644 --- a/judge/management/commands/copy_language.py +++ b/judge/management/commands/copy_language.py @@ -4,24 +4,30 @@ from judge.models import Language, LanguageLimit class Command(BaseCommand): - help = 'allows the problems that allow to be submitted in ' + help = "allows the problems that allow to be submitted in " def add_arguments(self, parser): - parser.add_argument('source', help='language to copy from') - parser.add_argument('target', help='language to copy to') + parser.add_argument("source", help="language to copy from") + parser.add_argument("target", help="language to copy to") def handle(self, *args, **options): try: - source = Language.objects.get(key=options['source']) + source = Language.objects.get(key=options["source"]) except Language.DoesNotExist: - raise CommandError('Invalid source language: %s' % options['source']) + raise CommandError("Invalid source language: %s" % options["source"]) try: - target = Language.objects.get(key=options['target']) + target = Language.objects.get(key=options["target"]) except Language.DoesNotExist: - raise CommandError('Invalid target language: %s' % options['target']) + raise CommandError("Invalid target language: %s" % options["target"]) target.problem_set.set(source.problem_set.all()) - LanguageLimit.objects.bulk_create(LanguageLimit(problem=ll.problem, language=target, time_limit=ll.time_limit, - memory_limit=ll.memory_limit) - for ll in LanguageLimit.objects.filter(language=source)) + LanguageLimit.objects.bulk_create( + LanguageLimit( + problem=ll.problem, + language=target, + time_limit=ll.time_limit, + memory_limit=ll.memory_limit, + ) + for ll in LanguageLimit.objects.filter(language=source) + ) diff --git a/judge/management/commands/create_problem.py b/judge/management/commands/create_problem.py index b095e08..568103f 100644 --- a/judge/management/commands/create_problem.py +++ b/judge/management/commands/create_problem.py @@ -4,20 +4,20 @@ from judge.models import Problem, ProblemGroup, ProblemType class Command(BaseCommand): - help = 'create an empty problem' + help = "create an empty problem" def add_arguments(self, parser): - parser.add_argument('code', help='problem code') - parser.add_argument('name', help='problem title') - parser.add_argument('body', help='problem description') - parser.add_argument('type', help='problem type') - parser.add_argument('group', help='problem group') + parser.add_argument("code", help="problem code") + parser.add_argument("name", help="problem title") + parser.add_argument("body", help="problem description") + parser.add_argument("type", help="problem type") + parser.add_argument("group", help="problem group") def handle(self, *args, **options): problem = Problem() - problem.code = options['code'] - problem.name = options['name'] - problem.description = options['body'] - problem.group = ProblemGroup.objects.get(name=options['group']) - problem.types = [ProblemType.objects.get(name=options['type'])] + problem.code = options["code"] + problem.name = options["name"] + problem.description = options["body"] + problem.group = ProblemGroup.objects.get(name=options["group"]) + problem.types = [ProblemType.objects.get(name=options["type"])] problem.save() diff --git a/judge/management/commands/generate_data.py b/judge/management/commands/generate_data.py index f7fdbd4..687ccd1 100644 --- a/judge/management/commands/generate_data.py +++ b/judge/management/commands/generate_data.py @@ -7,43 +7,47 @@ from django.conf import settings def gen_submissions(): - headers = ['uid', 'pid'] - with open(os.path.join(settings.ML_DATA_PATH, 'submissions.csv'), 'w') as csvfile: + headers = ["uid", "pid"] + with open(os.path.join(settings.ML_DATA_PATH, "submissions.csv"), "w") as csvfile: f = csv.writer(csvfile) f.writerow(headers) - + last_pid = defaultdict(int) for u in Profile.objects.all(): used = set() - print('Processing user', u.id) - for s in Submission.objects.filter(user=u).order_by('-date'): + print("Processing user", u.id) + for s in Submission.objects.filter(user=u).order_by("-date"): if s.problem.id not in used: used.add(s.problem.id) f.writerow([u.id, s.problem.id]) + def gen_users(): - headers = ['uid', 'username', 'rating', 'points'] - with open(os.path.join(settings.ML_DATA_PATH, 'profiles.csv'), 'w') as csvfile: + headers = ["uid", "username", "rating", "points"] + with open(os.path.join(settings.ML_DATA_PATH, "profiles.csv"), "w") as csvfile: f = csv.writer(csvfile) f.writerow(headers) - + for u in Profile.objects.all(): f.writerow([u.id, u.username, u.rating, u.performance_points]) + def gen_problems(): - headers = ['pid', 'code', 'name', 'points', 'url'] - with open(os.path.join(settings.ML_DATA_PATH, 'problems.csv'), 'w') as csvfile: + headers = ["pid", "code", "name", "points", "url"] + with open(os.path.join(settings.ML_DATA_PATH, "problems.csv"), "w") as csvfile: f = csv.writer(csvfile) f.writerow(headers) - + for p in Problem.objects.all(): - f.writerow([p.id, p.code, p.name, p.points, 'lqdoj.edu.vn/problem/' + p.code]) + f.writerow( + [p.id, p.code, p.name, p.points, "lqdoj.edu.vn/problem/" + p.code] + ) class Command(BaseCommand): - help = 'generate data for ML' + help = "generate data for ML" def handle(self, *args, **options): gen_users() gen_problems() - gen_submissions() \ No newline at end of file + gen_submissions() diff --git a/judge/management/commands/makedmojmessages.py b/judge/management/commands/makedmojmessages.py index 55d3acc..981726f 100644 --- a/judge/management/commands/makedmojmessages.py +++ b/judge/management/commands/makedmojmessages.py @@ -5,33 +5,69 @@ import sys from django.conf import settings from django.core.management import CommandError -from django.core.management.commands.makemessages import Command as MakeMessagesCommand, check_programs +from django.core.management.commands.makemessages import ( + Command as MakeMessagesCommand, + check_programs, +) from judge.models import NavigationBar, ProblemType class Command(MakeMessagesCommand): def add_arguments(self, parser): - parser.add_argument('--locale', '-l', default=[], dest='locale', action='append', - help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). ' - 'Can be used multiple times.') - parser.add_argument('--exclude', '-x', default=[], dest='exclude', action='append', - help='Locales to exclude. Default is none. Can be used multiple times.') - parser.add_argument('--all', '-a', action='store_true', dest='all', - default=False, help='Updates the message files for all existing locales.') - parser.add_argument('--no-wrap', action='store_true', dest='no_wrap', - default=False, help="Don't break long message lines into several lines.") - parser.add_argument('--no-obsolete', action='store_true', dest='no_obsolete', - default=False, help="Remove obsolete message strings.") - parser.add_argument('--keep-pot', action='store_true', dest='keep_pot', - default=False, help="Keep .pot file after making messages. Useful when debugging.") + parser.add_argument( + "--locale", + "-l", + default=[], + dest="locale", + action="append", + help="Creates or updates the message files for the given locale(s) (e.g. pt_BR). " + "Can be used multiple times.", + ) + parser.add_argument( + "--exclude", + "-x", + default=[], + dest="exclude", + action="append", + help="Locales to exclude. Default is none. Can be used multiple times.", + ) + parser.add_argument( + "--all", + "-a", + action="store_true", + dest="all", + default=False, + help="Updates the message files for all existing locales.", + ) + parser.add_argument( + "--no-wrap", + action="store_true", + dest="no_wrap", + default=False, + help="Don't break long message lines into several lines.", + ) + parser.add_argument( + "--no-obsolete", + action="store_true", + dest="no_obsolete", + default=False, + help="Remove obsolete message strings.", + ) + parser.add_argument( + "--keep-pot", + action="store_true", + dest="keep_pot", + default=False, + help="Keep .pot file after making messages. Useful when debugging.", + ) def handle(self, *args, **options): - locale = options.get('locale') - exclude = options.get('exclude') - self.domain = 'dmoj-user' - self.verbosity = options.get('verbosity') - process_all = options.get('all') + locale = options.get("locale") + exclude = options.get("exclude") + self.domain = "dmoj-user" + self.verbosity = options.get("verbosity") + process_all = options.get("all") # Need to ensure that the i18n framework is enabled if settings.configured: @@ -40,43 +76,47 @@ class Command(MakeMessagesCommand): settings.configure(USE_I18N=True) # Avoid messing with mutable class variables - if options.get('no_wrap'): - self.msgmerge_options = self.msgmerge_options[:] + ['--no-wrap'] - self.msguniq_options = self.msguniq_options[:] + ['--no-wrap'] - self.msgattrib_options = self.msgattrib_options[:] + ['--no-wrap'] - self.xgettext_options = self.xgettext_options[:] + ['--no-wrap'] - if options.get('no_location'): - self.msgmerge_options = self.msgmerge_options[:] + ['--no-location'] - self.msguniq_options = self.msguniq_options[:] + ['--no-location'] - self.msgattrib_options = self.msgattrib_options[:] + ['--no-location'] - self.xgettext_options = self.xgettext_options[:] + ['--no-location'] + if options.get("no_wrap"): + self.msgmerge_options = self.msgmerge_options[:] + ["--no-wrap"] + self.msguniq_options = self.msguniq_options[:] + ["--no-wrap"] + self.msgattrib_options = self.msgattrib_options[:] + ["--no-wrap"] + self.xgettext_options = self.xgettext_options[:] + ["--no-wrap"] + if options.get("no_location"): + self.msgmerge_options = self.msgmerge_options[:] + ["--no-location"] + self.msguniq_options = self.msguniq_options[:] + ["--no-location"] + self.msgattrib_options = self.msgattrib_options[:] + ["--no-location"] + self.xgettext_options = self.xgettext_options[:] + ["--no-location"] - self.no_obsolete = options.get('no_obsolete') - self.keep_pot = options.get('keep_pot') + self.no_obsolete = options.get("no_obsolete") + self.keep_pot = options.get("keep_pot") if locale is None and not exclude and not process_all: - raise CommandError("Type '%s help %s' for usage information." % ( - os.path.basename(sys.argv[0]), sys.argv[1])) + raise CommandError( + "Type '%s help %s' for usage information." + % (os.path.basename(sys.argv[0]), sys.argv[1]) + ) self.invoked_for_django = False self.locale_paths = [] self.default_locale_path = None - if os.path.isdir(os.path.join('conf', 'locale')): - self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))] + if os.path.isdir(os.path.join("conf", "locale")): + self.locale_paths = [os.path.abspath(os.path.join("conf", "locale"))] self.default_locale_path = self.locale_paths[0] self.invoked_for_django = True else: self.locale_paths.extend(settings.LOCALE_PATHS) # Allow to run makemessages inside an app dir - if os.path.isdir('locale'): - self.locale_paths.append(os.path.abspath('locale')) + if os.path.isdir("locale"): + self.locale_paths.append(os.path.abspath("locale")) if self.locale_paths: self.default_locale_path = self.locale_paths[0] if not os.path.exists(self.default_locale_path): os.makedirs(self.default_locale_path) # Build locale list - locale_dirs = list(filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path))) + locale_dirs = list( + filter(os.path.isdir, glob.glob("%s/*" % self.default_locale_path)) + ) all_locales = list(map(os.path.basename, locale_dirs)) # Account for excluded locales @@ -87,9 +127,9 @@ class Command(MakeMessagesCommand): locales = set(locales) - set(exclude) if locales: - check_programs('msguniq', 'msgmerge', 'msgattrib') + check_programs("msguniq", "msgmerge", "msgattrib") - check_programs('xgettext') + check_programs("xgettext") try: potfiles = self.build_potfiles() @@ -108,23 +148,33 @@ class Command(MakeMessagesCommand): return [] def _emit_message(self, potfile, string): - potfile.write(''' + potfile.write( + """ msgid "%s" msgstr "" -''' % string.replace('\\', r'\\').replace('\t', '\\t').replace('\n', '\\n').replace('"', '\\"')) +""" + % string.replace("\\", r"\\") + .replace("\t", "\\t") + .replace("\n", "\\n") + .replace('"', '\\"') + ) def process_files(self, file_list): - with io.open(os.path.join(self.default_locale_path, 'dmoj-user.pot'), 'w', encoding='utf-8') as potfile: + with io.open( + os.path.join(self.default_locale_path, "dmoj-user.pot"), + "w", + encoding="utf-8", + ) as potfile: if self.verbosity > 1: - self.stdout.write('processing navigation bar') - for label in NavigationBar.objects.values_list('label', flat=True): + self.stdout.write("processing navigation bar") + for label in NavigationBar.objects.values_list("label", flat=True): if self.verbosity > 2: self.stdout.write('processing navigation item label "%s"\n' % label) self._emit_message(potfile, label) if self.verbosity > 1: - self.stdout.write('processing problem types') - for name in ProblemType.objects.values_list('full_name', flat=True): + self.stdout.write("processing problem types") + for name in ProblemType.objects.values_list("full_name", flat=True): if self.verbosity > 2: self.stdout.write('processing problem type name "%s"\n' % name) self._emit_message(potfile, name) diff --git a/judge/management/commands/render_pdf.py b/judge/management/commands/render_pdf.py index 258081d..f8b2966 100644 --- a/judge/management/commands/render_pdf.py +++ b/judge/management/commands/render_pdf.py @@ -8,52 +8,98 @@ from django.template.loader import get_template from django.utils import translation from judge.models import Problem, ProblemTranslation -from judge.pdf_problems import DefaultPdfMaker, PhantomJSPdfMaker, PuppeteerPDFRender, SeleniumPDFRender, \ - SlimerJSPdfMaker +from judge.pdf_problems import ( + DefaultPdfMaker, + PhantomJSPdfMaker, + PuppeteerPDFRender, + SeleniumPDFRender, + SlimerJSPdfMaker, +) + class Command(BaseCommand): - help = 'renders a PDF file of a problem' + help = "renders a PDF file of a problem" def add_arguments(self, parser): - parser.add_argument('code', help='code of problem to render') - parser.add_argument('directory', nargs='?', help='directory to store temporaries') - parser.add_argument('-l', '--language', default=settings.LANGUAGE_CODE, - help='language to render PDF in') - parser.add_argument('-p', '--phantomjs', action='store_const', const=PhantomJSPdfMaker, - default=DefaultPdfMaker, dest='engine') - parser.add_argument('-s', '--slimerjs', action='store_const', const=SlimerJSPdfMaker, dest='engine') - parser.add_argument('-c', '--chrome', '--puppeteer', action='store_const', - const=PuppeteerPDFRender, dest='engine') - parser.add_argument('-S', '--selenium', action='store_const', const=SeleniumPDFRender, dest='engine') + parser.add_argument("code", help="code of problem to render") + parser.add_argument( + "directory", nargs="?", help="directory to store temporaries" + ) + parser.add_argument( + "-l", + "--language", + default=settings.LANGUAGE_CODE, + help="language to render PDF in", + ) + parser.add_argument( + "-p", + "--phantomjs", + action="store_const", + const=PhantomJSPdfMaker, + default=DefaultPdfMaker, + dest="engine", + ) + parser.add_argument( + "-s", + "--slimerjs", + action="store_const", + const=SlimerJSPdfMaker, + dest="engine", + ) + parser.add_argument( + "-c", + "--chrome", + "--puppeteer", + action="store_const", + const=PuppeteerPDFRender, + dest="engine", + ) + parser.add_argument( + "-S", + "--selenium", + action="store_const", + const=SeleniumPDFRender, + dest="engine", + ) def handle(self, *args, **options): try: - problem = Problem.objects.get(code=options['code']) + problem = Problem.objects.get(code=options["code"]) except Problem.DoesNotExist: - print('Bad problem code') + print("Bad problem code") return try: - trans = problem.translations.get(language=options['language']) + trans = problem.translations.get(language=options["language"]) except ProblemTranslation.DoesNotExist: trans = None - directory = options['directory'] - with options['engine'](directory, clean_up=directory is None) as maker, \ - translation.override(options['language']): + directory = options["directory"] + with options["engine"]( + directory, clean_up=directory is None + ) as maker, translation.override(options["language"]): problem_name = problem.name if trans is None else trans.name - maker.html = get_template('problem/raw.html').render({ - 'problem': problem, - 'problem_name': problem_name, - 'description': problem.description if trans is None else trans.description, - 'url': '', - 'math_engine': maker.math_engine, - }).replace('"//', '"https://').replace("'//", "'https://") + maker.html = ( + get_template("problem/raw.html") + .render( + { + "problem": problem, + "problem_name": problem_name, + "description": problem.description + if trans is None + else trans.description, + "url": "", + "math_engine": maker.math_engine, + } + ) + .replace('"//', '"https://') + .replace("'//", "'https://") + ) maker.title = problem_name - for file in ('style.css', 'pygment-github.css', 'mathjax_config.js'): + for file in ("style.css", "pygment-github.css", "mathjax_config.js"): maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) maker.make(debug=True) if not maker.success: print(maker.log, file=sys.stderr) elif directory is None: - shutil.move(maker.pdffile, problem.code + '.pdf') + shutil.move(maker.pdffile, problem.code + ".pdf") diff --git a/judge/management/commands/runbridged.py b/judge/management/commands/runbridged.py index c6f4536..b968d63 100644 --- a/judge/management/commands/runbridged.py +++ b/judge/management/commands/runbridged.py @@ -5,4 +5,4 @@ from judge.bridge.daemon import judge_daemon class Command(BaseCommand): def handle(self, *args, **options): - judge_daemon() \ No newline at end of file + judge_daemon() diff --git a/judge/management/commands/runmoss.py b/judge/management/commands/runmoss.py index 0558da3..c8d026c 100644 --- a/judge/management/commands/runmoss.py +++ b/judge/management/commands/runmoss.py @@ -6,42 +6,50 @@ from judge.models import Contest, ContestParticipation, Submission class Command(BaseCommand): - help = 'Checks for duplicate code using MOSS' + help = "Checks for duplicate code using MOSS" LANG_MAPPING = { - ('C++', MOSS_LANG_CC), - ('C', MOSS_LANG_C), - ('Java', MOSS_LANG_JAVA), - ('Python', MOSS_LANG_PYTHON), - ('Pascal', MOSS_LANG_PASCAL), + ("C++", MOSS_LANG_CC), + ("C", MOSS_LANG_C), + ("Java", MOSS_LANG_JAVA), + ("Python", MOSS_LANG_PYTHON), + ("Pascal", MOSS_LANG_PASCAL), } def add_arguments(self, parser): - parser.add_argument('contest', help='the id of the contest') + parser.add_argument("contest", help="the id of the contest") def handle(self, *args, **options): moss_api_key = settings.MOSS_API_KEY if moss_api_key is None: - print('No MOSS API Key supplied') + print("No MOSS API Key supplied") return - contest = options['contest'] + contest = options["contest"] - for problem in Contest.objects.get(key=contest).problems.order_by('code'): - print('========== %s / %s ==========' % (problem.code, problem.name)) + for problem in Contest.objects.get(key=contest).problems.order_by("code"): + print("========== %s / %s ==========" % (problem.code, problem.name)) for dmoj_lang, moss_lang in self.LANG_MAPPING: - print("%s: " % dmoj_lang, end=' ') + print("%s: " % dmoj_lang, end=" ") subs = Submission.objects.filter( - contest__participation__virtual__in=(ContestParticipation.LIVE, ContestParticipation.SPECTATE), + contest__participation__virtual__in=( + ContestParticipation.LIVE, + ContestParticipation.SPECTATE, + ), contest__participation__contest__key=contest, - result='AC', problem__id=problem.id, + result="AC", + problem__id=problem.id, language__common_name=dmoj_lang, - ).values_list('user__user__username', 'source__source') + ).values_list("user__user__username", "source__source") if not subs: - print('') + print("") continue - moss_call = MOSS(moss_api_key, language=moss_lang, matching_file_limit=100, - comment='%s - %s' % (contest, problem.code)) + moss_call = MOSS( + moss_api_key, + language=moss_lang, + matching_file_limit=100, + comment="%s - %s" % (contest, problem.code), + ) users = set() @@ -49,6 +57,6 @@ class Command(BaseCommand): if username in users: continue users.add(username) - moss_call.add_file_from_memory(username, source.encode('utf-8')) + moss_call.add_file_from_memory(username, source.encode("utf-8")) - print('(%d): %s' % (subs.count(), moss_call.process())) + print("(%d): %s" % (subs.count(), moss_call.process())) diff --git a/judge/middleware.py b/judge/middleware.py index b35e1a3..6f5a053 100644 --- a/judge/middleware.py +++ b/judge/middleware.py @@ -10,11 +10,13 @@ class ShortCircuitMiddleware: def __call__(self, request): try: - callback, args, kwargs = resolve(request.path_info, getattr(request, 'urlconf', None)) + callback, args, kwargs = resolve( + request.path_info, getattr(request, "urlconf", None) + ) except Resolver404: callback, args, kwargs = None, None, None - if getattr(callback, 'short_circuit_middleware', False): + if getattr(callback, "short_circuit_middleware", False): return callback(request, *args, **kwargs) return self.get_response(request) @@ -26,11 +28,16 @@ class DMOJLoginMiddleware(object): def __call__(self, request): if request.user.is_authenticated: profile = request.profile = request.user.profile - login_2fa_path = reverse('login_2fa') - if (profile.is_totp_enabled and not request.session.get('2fa_passed', False) and - request.path not in (login_2fa_path, reverse('auth_logout')) and - not request.path.startswith(settings.STATIC_URL)): - return HttpResponseRedirect(login_2fa_path + '?next=' + urlquote(request.get_full_path())) + login_2fa_path = reverse("login_2fa") + if ( + profile.is_totp_enabled + and not request.session.get("2fa_passed", False) + and request.path not in (login_2fa_path, reverse("auth_logout")) + and not request.path.startswith(settings.STATIC_URL) + ): + return HttpResponseRedirect( + login_2fa_path + "?next=" + urlquote(request.get_full_path()) + ) else: request.profile = None return self.get_response(request) @@ -57,7 +64,7 @@ class ContestMiddleware(object): profile.update_contest() request.participation = profile.current_contest request.in_contest = request.participation is not None - request.contest_mode = request.session.get('contest_mode', True) + request.contest_mode = request.session.get("contest_mode", True) else: request.in_contest = False request.participation = None diff --git a/judge/migrations/0001_squashed_0084_contest_formats.py b/judge/migrations/0001_squashed_0084_contest_formats.py index 22aa659..ae4e6ae 100644 --- a/judge/migrations/0001_squashed_0084_contest_formats.py +++ b/judge/migrations/0001_squashed_0084_contest_formats.py @@ -14,748 +14,3275 @@ import judge.utils.problem_data class Migration(migrations.Migration): - replaces = [('judge', '0001_initial'), ('judge', '0002_license'), ('judge', '0003_license_key'), ('judge', '0004_language_limit'), ('judge', '0005_nav_path_len'), ('judge', '0006_language_extension'), ('judge', '0007_test_site_perm'), ('judge', '0008_contestproblem_order'), ('judge', '0009_solution_problem'), ('judge', '0010_comment_page_index'), ('judge', '0011_organization_is_open'), ('judge', '0012_organization_perms'), ('judge', '0013_private_contests'), ('judge', '0014_multi_organization'), ('judge', '0015_remove_single_organization'), ('judge', '0016_organizationrequest'), ('judge', '0017_edit_public_problem_perm'), ('judge', '0018_django_1_9'), ('judge', '0019_og_images'), ('judge', '0020_profile_user_script'), ('judge', '0021_output_prefix_override'), ('judge', '0022_judge_last_ping'), ('judge', '0023_contest_tag'), ('judge', '0024_submission_judge'), ('judge', '0025_submission_rejudge_flag'), ('judge', '0026_change_public_visibility_perm'), ('judge', '0027_bridge_revert'), ('judge', '0028_judge_ip'), ('judge', '0029_problem_translation'), ('judge', '0030_remove_contest_profile'), ('judge', '0031_judge_versions'), ('judge', '0032_hide_problem_tags_in_contest'), ('judge', '0033_proper_pretest_support'), ('judge', '0034_submission_is_pretested'), ('judge', '0035_contest_spectate_mode'), ('judge', '0036_contest_participation_unique'), ('judge', '0037_user_count_ac_rate_field'), ('judge', '0038_profile_problem_count'), ('judge', '0039_remove_contest_is_external'), ('judge', '0040_profile_math_engine'), ('judge', '0041_virtual_contest_participation'), ('judge', '0042_remove_spectate_field'), ('judge', '0043_contest_user_count'), ('judge', '0044_organization_slots'), ('judge', '0045_organization_access_code'), ('judge', '0046_blogpost_authors'), ('judge', '0047_site_managed_data'), ('judge', '0048_site_managed_checkers'), ('judge', '0049_contest_summary'), ('judge', '0050_problem_tester_field'), ('judge', '0051_was_rejudged_field'), ('judge', '0052_switch_to_durationfield'), ('judge', '0053_opengraph_problems'), ('judge', '0054_tickets'), ('judge', '0055_add_performance_points'), ('judge', '0056_ticket_is_open'), ('judge', '0057_blue_pretests'), ('judge', '0058_problem_curator_field'), ('judge', '0059_problem_is_manually_managed'), ('judge', '0060_contest_clarifications'), ('judge', '0061_language_template'), ('judge', '0062_add_contest_submission_limit'), ('judge', '0063_new_solutions'), ('judge', '0064_unique_solution'), ('judge', '0065_blogpost_perms'), ('judge', '0066_submission_date_index'), ('judge', '0067_contest_access_code'), ('judge', '0068_hide_scoreboard'), ('judge', '0069_judge_blocking'), ('judge', '0070_organization_slug'), ('judge', '0071_organization_private_problems'), ('judge', '0072_contest_logo_override_image'), ('judge', '0073_comment_lock'), ('judge', '0074_totp'), ('judge', '0075_organization_admin_reverse'), ('judge', '0076_problem_statistics'), ('judge', '0077_remove_organization_key'), ('judge', '0078_add_user_notes'), ('judge', '0079_remove_comment_title'), ('judge', '0080_contest_banned_users'), ('judge', '0081_unlisted_users'), ('judge', '0082_remove_profile_name'), ('judge', '0083_extended_feedback'), ('judge', '0084_contest_formats')] + replaces = [ + ("judge", "0001_initial"), + ("judge", "0002_license"), + ("judge", "0003_license_key"), + ("judge", "0004_language_limit"), + ("judge", "0005_nav_path_len"), + ("judge", "0006_language_extension"), + ("judge", "0007_test_site_perm"), + ("judge", "0008_contestproblem_order"), + ("judge", "0009_solution_problem"), + ("judge", "0010_comment_page_index"), + ("judge", "0011_organization_is_open"), + ("judge", "0012_organization_perms"), + ("judge", "0013_private_contests"), + ("judge", "0014_multi_organization"), + ("judge", "0015_remove_single_organization"), + ("judge", "0016_organizationrequest"), + ("judge", "0017_edit_public_problem_perm"), + ("judge", "0018_django_1_9"), + ("judge", "0019_og_images"), + ("judge", "0020_profile_user_script"), + ("judge", "0021_output_prefix_override"), + ("judge", "0022_judge_last_ping"), + ("judge", "0023_contest_tag"), + ("judge", "0024_submission_judge"), + ("judge", "0025_submission_rejudge_flag"), + ("judge", "0026_change_public_visibility_perm"), + ("judge", "0027_bridge_revert"), + ("judge", "0028_judge_ip"), + ("judge", "0029_problem_translation"), + ("judge", "0030_remove_contest_profile"), + ("judge", "0031_judge_versions"), + ("judge", "0032_hide_problem_tags_in_contest"), + ("judge", "0033_proper_pretest_support"), + ("judge", "0034_submission_is_pretested"), + ("judge", "0035_contest_spectate_mode"), + ("judge", "0036_contest_participation_unique"), + ("judge", "0037_user_count_ac_rate_field"), + ("judge", "0038_profile_problem_count"), + ("judge", "0039_remove_contest_is_external"), + ("judge", "0040_profile_math_engine"), + ("judge", "0041_virtual_contest_participation"), + ("judge", "0042_remove_spectate_field"), + ("judge", "0043_contest_user_count"), + ("judge", "0044_organization_slots"), + ("judge", "0045_organization_access_code"), + ("judge", "0046_blogpost_authors"), + ("judge", "0047_site_managed_data"), + ("judge", "0048_site_managed_checkers"), + ("judge", "0049_contest_summary"), + ("judge", "0050_problem_tester_field"), + ("judge", "0051_was_rejudged_field"), + ("judge", "0052_switch_to_durationfield"), + ("judge", "0053_opengraph_problems"), + ("judge", "0054_tickets"), + ("judge", "0055_add_performance_points"), + ("judge", "0056_ticket_is_open"), + ("judge", "0057_blue_pretests"), + ("judge", "0058_problem_curator_field"), + ("judge", "0059_problem_is_manually_managed"), + ("judge", "0060_contest_clarifications"), + ("judge", "0061_language_template"), + ("judge", "0062_add_contest_submission_limit"), + ("judge", "0063_new_solutions"), + ("judge", "0064_unique_solution"), + ("judge", "0065_blogpost_perms"), + ("judge", "0066_submission_date_index"), + ("judge", "0067_contest_access_code"), + ("judge", "0068_hide_scoreboard"), + ("judge", "0069_judge_blocking"), + ("judge", "0070_organization_slug"), + ("judge", "0071_organization_private_problems"), + ("judge", "0072_contest_logo_override_image"), + ("judge", "0073_comment_lock"), + ("judge", "0074_totp"), + ("judge", "0075_organization_admin_reverse"), + ("judge", "0076_problem_statistics"), + ("judge", "0077_remove_organization_key"), + ("judge", "0078_add_user_notes"), + ("judge", "0079_remove_comment_title"), + ("judge", "0080_contest_banned_users"), + ("judge", "0081_unlisted_users"), + ("judge", "0082_remove_profile_name"), + ("judge", "0083_extended_feedback"), + ("judge", "0084_contest_formats"), + ] initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contenttypes', '0002_remove_content_type_name'), + ("contenttypes", "0002_remove_content_type_name"), ] operations = [ migrations.CreateModel( - name='BlogPost', + name="BlogPost", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100, verbose_name='post title')), - ('slug', models.SlugField(verbose_name='slug')), - ('visible', models.BooleanField(default=False, verbose_name='public visibility')), - ('sticky', models.BooleanField(default=False, verbose_name='sticky')), - ('publish_on', models.DateTimeField(verbose_name='publish after')), - ('content', models.TextField(verbose_name='post content')), - ('summary', models.TextField(blank=True, verbose_name='post summary')), - ('og_image', models.CharField(blank=True, default='', max_length=150, verbose_name='openGraph image')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100, verbose_name="post title")), + ("slug", models.SlugField(verbose_name="slug")), + ( + "visible", + models.BooleanField( + default=False, verbose_name="public visibility" + ), + ), + ("sticky", models.BooleanField(default=False, verbose_name="sticky")), + ("publish_on", models.DateTimeField(verbose_name="publish after")), + ("content", models.TextField(verbose_name="post content")), + ("summary", models.TextField(blank=True, verbose_name="post summary")), + ( + "og_image", + models.CharField( + blank=True, + default="", + max_length=150, + verbose_name="openGraph image", + ), + ), ], options={ - 'verbose_name_plural': 'blog posts', - 'permissions': (('edit_all_post', 'Edit all posts'),), - 'verbose_name': 'blog post', + "verbose_name_plural": "blog posts", + "permissions": (("edit_all_post", "Edit all posts"),), + "verbose_name": "blog post", }, ), migrations.CreateModel( - name='Comment', + name="Comment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')), - ('page', models.CharField(db_index=True, max_length=30, validators=[django.core.validators.RegexValidator('^[pcs]:[a-z0-9]+$|^b:\\d+$', 'Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$')], verbose_name='associated page')), - ('score', models.IntegerField(default=0, verbose_name='votes')), - ('body', models.TextField(max_length=8192, verbose_name='body of comment')), - ('hidden', models.BooleanField(default=0, verbose_name='hide the comment')), - ('lft', models.PositiveIntegerField(db_index=True, editable=False)), - ('rght', models.PositiveIntegerField(db_index=True, editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(db_index=True, editable=False)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "time", + models.DateTimeField(auto_now_add=True, verbose_name="posted time"), + ), + ( + "page", + models.CharField( + db_index=True, + max_length=30, + validators=[ + django.core.validators.RegexValidator( + "^[pcs]:[a-z0-9]+$|^b:\\d+$", + "Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$", + ) + ], + verbose_name="associated page", + ), + ), + ("score", models.IntegerField(default=0, verbose_name="votes")), + ( + "body", + models.TextField(max_length=8192, verbose_name="body of comment"), + ), + ( + "hidden", + models.BooleanField(default=0, verbose_name="hide the comment"), + ), + ("lft", models.PositiveIntegerField(db_index=True, editable=False)), + ("rght", models.PositiveIntegerField(db_index=True, editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ("level", models.PositiveIntegerField(db_index=True, editable=False)), ], options={ - 'verbose_name_plural': 'comments', - 'verbose_name': 'comment', + "verbose_name_plural": "comments", + "verbose_name": "comment", }, ), migrations.CreateModel( - name='CommentLock', + name="CommentLock", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('page', models.CharField(db_index=True, max_length=30, validators=[django.core.validators.RegexValidator('^[pcs]:[a-z0-9]+$|^b:\\d+$', 'Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$')], verbose_name='associated page')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "page", + models.CharField( + db_index=True, + max_length=30, + validators=[ + django.core.validators.RegexValidator( + "^[pcs]:[a-z0-9]+$|^b:\\d+$", + "Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$", + ) + ], + verbose_name="associated page", + ), + ), ], options={ - 'permissions': (('override_comment_lock', 'Override comment lock'),), + "permissions": (("override_comment_lock", "Override comment lock"),), }, ), migrations.CreateModel( - name='CommentVote', + name="CommentVote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('score', models.IntegerField()), - ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='judge.Comment')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("score", models.IntegerField()), + ( + "comment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="votes", + to="judge.Comment", + ), + ), ], options={ - 'verbose_name_plural': 'comment votes', - 'verbose_name': 'comment vote', + "verbose_name_plural": "comment votes", + "verbose_name": "comment vote", }, ), migrations.CreateModel( - name='Contest', + name="Contest", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z0-9]+$', 'Contest id must be ^[a-z0-9]+$')], verbose_name='contest id')), - ('name', models.CharField(db_index=True, max_length=100, verbose_name='contest name')), - ('description', models.TextField(blank=True, verbose_name='description')), - ('start_time', models.DateTimeField(db_index=True, verbose_name='start time')), - ('end_time', models.DateTimeField(db_index=True, verbose_name='end time')), - ('time_limit', models.DurationField(blank=True, null=True, verbose_name='time limit')), - ('is_public', models.BooleanField(default=False, help_text='Should be set even for organization-private contests, where it determines whether the contest is visible to members of the specified organizations.', verbose_name='publicly visible')), - ('is_rated', models.BooleanField(default=False, help_text='Whether this contest can be rated.', verbose_name='contest rated')), - ('hide_scoreboard', models.BooleanField(default=False, help_text='Whether the scoreboard should remain hidden for the duration of the contest.', verbose_name='hide scoreboard')), - ('use_clarifications', models.BooleanField(default=True, help_text='Use clarification system instead of comments.', verbose_name='no comments')), - ('rate_all', models.BooleanField(default=False, help_text='Rate all users who joined.', verbose_name='rate all')), - ('is_private', models.BooleanField(default=False, verbose_name='private to organizations')), - ('hide_problem_tags', models.BooleanField(default=False, help_text='Whether problem tags should be hidden by default.', verbose_name='hide problem tags')), - ('run_pretests_only', models.BooleanField(default=False, help_text='Whether judges should grade pretests only, versus all testcases. Commonly set during a contest, then unset prior to rejudging user submissions when the contest ends.', verbose_name='run pretests only')), - ('og_image', models.CharField(blank=True, default='', max_length=150, verbose_name='OpenGraph image')), - ('logo_override_image', models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users inside the contest.', max_length=150, verbose_name='Logo override image')), - ('user_count', models.IntegerField(default=0, verbose_name='the amount of live participants')), - ('summary', models.TextField(blank=True, help_text='Plain-text, shown in meta description tag, e.g. for social media.', verbose_name='contest summary')), - ('access_code', models.CharField(blank=True, default='', help_text='An optional code to prompt contestants before they are allowed to join the contest. Leave it blank to disable.', max_length=255, verbose_name='access code')), - ('format_name', models.CharField(choices=[('default', 'Default')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format')), - ('format_config', jsonfield.fields.JSONField(blank=True, help_text='A JSON object to serve as the configuration for the chosen contest format module. Leave empty to use None. Exact format depends on the contest format selected.', null=True, verbose_name='contest format configuration')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "key", + models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[a-z0-9]+$", "Contest id must be ^[a-z0-9]+$" + ) + ], + verbose_name="contest id", + ), + ), + ( + "name", + models.CharField( + db_index=True, max_length=100, verbose_name="contest name" + ), + ), + ( + "description", + models.TextField(blank=True, verbose_name="description"), + ), + ( + "start_time", + models.DateTimeField(db_index=True, verbose_name="start time"), + ), + ( + "end_time", + models.DateTimeField(db_index=True, verbose_name="end time"), + ), + ( + "time_limit", + models.DurationField( + blank=True, null=True, verbose_name="time limit" + ), + ), + ( + "is_public", + models.BooleanField( + default=False, + help_text="Should be set even for organization-private contests, where it determines whether the contest is visible to members of the specified organizations.", + verbose_name="publicly visible", + ), + ), + ( + "is_rated", + models.BooleanField( + default=False, + help_text="Whether this contest can be rated.", + verbose_name="contest rated", + ), + ), + ( + "hide_scoreboard", + models.BooleanField( + default=False, + help_text="Whether the scoreboard should remain hidden for the duration of the contest.", + verbose_name="hide scoreboard", + ), + ), + ( + "use_clarifications", + models.BooleanField( + default=True, + help_text="Use clarification system instead of comments.", + verbose_name="no comments", + ), + ), + ( + "rate_all", + models.BooleanField( + default=False, + help_text="Rate all users who joined.", + verbose_name="rate all", + ), + ), + ( + "is_private", + models.BooleanField( + default=False, verbose_name="private to organizations" + ), + ), + ( + "hide_problem_tags", + models.BooleanField( + default=False, + help_text="Whether problem tags should be hidden by default.", + verbose_name="hide problem tags", + ), + ), + ( + "run_pretests_only", + models.BooleanField( + default=False, + help_text="Whether judges should grade pretests only, versus all testcases. Commonly set during a contest, then unset prior to rejudging user submissions when the contest ends.", + verbose_name="run pretests only", + ), + ), + ( + "og_image", + models.CharField( + blank=True, + default="", + max_length=150, + verbose_name="OpenGraph image", + ), + ), + ( + "logo_override_image", + models.CharField( + blank=True, + default="", + help_text="This image will replace the default site logo for users inside the contest.", + max_length=150, + verbose_name="Logo override image", + ), + ), + ( + "user_count", + models.IntegerField( + default=0, verbose_name="the amount of live participants" + ), + ), + ( + "summary", + models.TextField( + blank=True, + help_text="Plain-text, shown in meta description tag, e.g. for social media.", + verbose_name="contest summary", + ), + ), + ( + "access_code", + models.CharField( + blank=True, + default="", + help_text="An optional code to prompt contestants before they are allowed to join the contest. Leave it blank to disable.", + max_length=255, + verbose_name="access code", + ), + ), + ( + "format_name", + models.CharField( + choices=[("default", "Default")], + default="default", + help_text="The contest format module to use.", + max_length=32, + verbose_name="contest format", + ), + ), + ( + "format_config", + jsonfield.fields.JSONField( + blank=True, + help_text="A JSON object to serve as the configuration for the chosen contest format module. Leave empty to use None. Exact format depends on the contest format selected.", + null=True, + verbose_name="contest format configuration", + ), + ), ], options={ - 'verbose_name_plural': 'contests', - 'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes')), - 'verbose_name': 'contest', + "verbose_name_plural": "contests", + "permissions": ( + ("see_private_contest", "See private contests"), + ("edit_own_contest", "Edit own contests"), + ("edit_all_contest", "Edit all contests"), + ("contest_rating", "Rate contests"), + ("contest_access_code", "Contest access codes"), + ), + "verbose_name": "contest", }, ), migrations.CreateModel( - name='ContestParticipation', + name="ContestParticipation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('real_start', models.DateTimeField(db_column='start', default=django.utils.timezone.now, verbose_name='start time')), - ('score', models.IntegerField(db_index=True, default=0, verbose_name='score')), - ('cumtime', models.PositiveIntegerField(default=0, verbose_name='cumulative time')), - ('virtual', models.IntegerField(default=0, help_text='0 means non-virtual, otherwise the n-th virtual participation', verbose_name='virtual participation id')), - ('format_data', jsonfield.fields.JSONField(blank=True, null=True, verbose_name='contest format specific data')), - ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='users', to='judge.Contest', verbose_name='associated contest')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "real_start", + models.DateTimeField( + db_column="start", + default=django.utils.timezone.now, + verbose_name="start time", + ), + ), + ( + "score", + models.IntegerField(db_index=True, default=0, verbose_name="score"), + ), + ( + "cumtime", + models.PositiveIntegerField( + default=0, verbose_name="cumulative time" + ), + ), + ( + "virtual", + models.IntegerField( + default=0, + help_text="0 means non-virtual, otherwise the n-th virtual participation", + verbose_name="virtual participation id", + ), + ), + ( + "format_data", + jsonfield.fields.JSONField( + blank=True, + null=True, + verbose_name="contest format specific data", + ), + ), + ( + "contest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="users", + to="judge.Contest", + verbose_name="associated contest", + ), + ), ], options={ - 'verbose_name_plural': 'contest participations', - 'verbose_name': 'contest participation', + "verbose_name_plural": "contest participations", + "verbose_name": "contest participation", }, ), migrations.CreateModel( - name='ContestProblem', + name="ContestProblem", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('points', models.IntegerField(verbose_name='points')), - ('partial', models.BooleanField(default=True, verbose_name='partial')), - ('is_pretested', models.BooleanField(default=False, verbose_name='is pretested')), - ('order', models.PositiveIntegerField(db_index=True, verbose_name='order')), - ('output_prefix_override', models.IntegerField(blank=True, null=True, verbose_name='output prefix length override')), - ('max_submissions', models.IntegerField(default=0, help_text='Maximum number of submissions for this problem, or 0 for no limit.', validators=[django.core.validators.MinValueValidator(0, "Why include a problem you can't submit to?")])), - ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contest_problems', to='judge.Contest', verbose_name='contest')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("points", models.IntegerField(verbose_name="points")), + ("partial", models.BooleanField(default=True, verbose_name="partial")), + ( + "is_pretested", + models.BooleanField(default=False, verbose_name="is pretested"), + ), + ( + "order", + models.PositiveIntegerField(db_index=True, verbose_name="order"), + ), + ( + "output_prefix_override", + models.IntegerField( + blank=True, + null=True, + verbose_name="output prefix length override", + ), + ), + ( + "max_submissions", + models.IntegerField( + default=0, + help_text="Maximum number of submissions for this problem, or 0 for no limit.", + validators=[ + django.core.validators.MinValueValidator( + 0, "Why include a problem you can't submit to?" + ) + ], + ), + ), + ( + "contest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contest_problems", + to="judge.Contest", + verbose_name="contest", + ), + ), ], options={ - 'verbose_name_plural': 'contest problems', - 'verbose_name': 'contest problem', + "verbose_name_plural": "contest problems", + "verbose_name": "contest problem", }, ), migrations.CreateModel( - name='ContestSubmission', + name="ContestSubmission", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('points', models.FloatField(default=0.0, verbose_name='points')), - ('is_pretest', models.BooleanField(default=False, help_text='Whether this submission was ran only on pretests.', verbose_name='is pretested')), - ('participation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', related_query_name='submission', to='judge.ContestParticipation', verbose_name='participation')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', related_query_name='submission', to='judge.ContestProblem', verbose_name='problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("points", models.FloatField(default=0.0, verbose_name="points")), + ( + "is_pretest", + models.BooleanField( + default=False, + help_text="Whether this submission was ran only on pretests.", + verbose_name="is pretested", + ), + ), + ( + "participation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submissions", + related_query_name="submission", + to="judge.ContestParticipation", + verbose_name="participation", + ), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submissions", + related_query_name="submission", + to="judge.ContestProblem", + verbose_name="problem", + ), + ), ], options={ - 'verbose_name_plural': 'contest submissions', - 'verbose_name': 'contest submission', + "verbose_name_plural": "contest submissions", + "verbose_name": "contest submission", }, ), migrations.CreateModel( - name='ContestTag', + name="ContestTag", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z-]+$', message='Lowercase letters and hyphens only.')], verbose_name='tag name')), - ('color', models.CharField(max_length=7, validators=[django.core.validators.RegexValidator('^#(?:[A-Fa-f0-9]{3}){1,2}$', 'Invalid colour.')], verbose_name='tag colour')), - ('description', models.TextField(blank=True, verbose_name='tag description')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[a-z-]+$", + message="Lowercase letters and hyphens only.", + ) + ], + verbose_name="tag name", + ), + ), + ( + "color", + models.CharField( + max_length=7, + validators=[ + django.core.validators.RegexValidator( + "^#(?:[A-Fa-f0-9]{3}){1,2}$", "Invalid colour." + ) + ], + verbose_name="tag colour", + ), + ), + ( + "description", + models.TextField(blank=True, verbose_name="tag description"), + ), ], options={ - 'verbose_name_plural': 'contest tags', - 'verbose_name': 'contest tag', + "verbose_name_plural": "contest tags", + "verbose_name": "contest tag", }, ), migrations.CreateModel( - name='Judge', + name="Judge", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='Server name, hostname-style', max_length=50, unique=True)), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='time of creation')), - ('auth_key', models.CharField(help_text='A key to authenticated this judge', max_length=100, verbose_name='authentication key')), - ('is_blocked', models.BooleanField(default=False, help_text='Whether this judge should be blocked from connecting, even if its key is correct.', verbose_name='block judge')), - ('online', models.BooleanField(default=False, verbose_name='judge online status')), - ('start_time', models.DateTimeField(null=True, verbose_name='judge start time')), - ('ping', models.FloatField(null=True, verbose_name='response time')), - ('load', models.FloatField(help_text='Load for the last minute, divided by processors to be fair.', null=True, verbose_name='system load')), - ('description', models.TextField(blank=True, verbose_name='description')), - ('last_ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='Last connected IP')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + help_text="Server name, hostname-style", + max_length=50, + unique=True, + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, verbose_name="time of creation" + ), + ), + ( + "auth_key", + models.CharField( + help_text="A key to authenticated this judge", + max_length=100, + verbose_name="authentication key", + ), + ), + ( + "is_blocked", + models.BooleanField( + default=False, + help_text="Whether this judge should be blocked from connecting, even if its key is correct.", + verbose_name="block judge", + ), + ), + ( + "online", + models.BooleanField( + default=False, verbose_name="judge online status" + ), + ), + ( + "start_time", + models.DateTimeField(null=True, verbose_name="judge start time"), + ), + ("ping", models.FloatField(null=True, verbose_name="response time")), + ( + "load", + models.FloatField( + help_text="Load for the last minute, divided by processors to be fair.", + null=True, + verbose_name="system load", + ), + ), + ( + "description", + models.TextField(blank=True, verbose_name="description"), + ), + ( + "last_ip", + models.GenericIPAddressField( + blank=True, null=True, verbose_name="Last connected IP" + ), + ), ], options={ - 'ordering': ['name'], - 'verbose_name_plural': 'judges', - 'verbose_name': 'judge', + "ordering": ["name"], + "verbose_name_plural": "judges", + "verbose_name": "judge", }, ), migrations.CreateModel( - name='Language', + name="Language", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(help_text='The identifier for this language; the same as its executor id for judges.', max_length=6, unique=True, verbose_name='short identifier')), - ('name', models.CharField(help_text='Longer name for the language, e.g. "Python 2" or "C++11".', max_length=20, verbose_name='long name')), - ('short_name', models.CharField(blank=True, help_text='More readable, but short, name to display publicly; e.g. "PY2" or "C++11". If left blank, it will default to the short identifier.', max_length=10, null=True, verbose_name='short name')), - ('common_name', models.CharField(help_text='Common name for the language. For example, the common name for C++03, C++11, and C++14 would be "C++"', max_length=10, verbose_name='common name')), - ('ace', models.CharField(help_text='Language ID for Ace.js editor highlighting, appended to "mode-" to determine the Ace JavaScript file to use, e.g., "python".', max_length=20, verbose_name='ace mode name')), - ('pygments', models.CharField(help_text='Language ID for Pygments highlighting in source windows.', max_length=20, verbose_name='pygments name')), - ('template', models.TextField(blank=True, help_text='Code template to display in submission editor.', verbose_name='code template')), - ('info', models.CharField(blank=True, help_text="Do not set this unless you know what you're doing! It will override the usually more specific, judge-provided runtime info!", max_length=50, verbose_name='runtime info override')), - ('description', models.TextField(blank=True, help_text='Use field this to inform users of quirks with your environment, additional restrictions, etc.', verbose_name='language description')), - ('extension', models.CharField(help_text='The extension of source files, e.g., "py" or "cpp".', max_length=10, verbose_name='extension')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "key", + models.CharField( + help_text="The identifier for this language; the same as its executor id for judges.", + max_length=6, + unique=True, + verbose_name="short identifier", + ), + ), + ( + "name", + models.CharField( + help_text='Longer name for the language, e.g. "Python 2" or "C++11".', + max_length=20, + verbose_name="long name", + ), + ), + ( + "short_name", + models.CharField( + blank=True, + help_text='More readable, but short, name to display publicly; e.g. "PY2" or "C++11". If left blank, it will default to the short identifier.', + max_length=10, + null=True, + verbose_name="short name", + ), + ), + ( + "common_name", + models.CharField( + help_text='Common name for the language. For example, the common name for C++03, C++11, and C++14 would be "C++"', + max_length=10, + verbose_name="common name", + ), + ), + ( + "ace", + models.CharField( + help_text='Language ID for Ace.js editor highlighting, appended to "mode-" to determine the Ace JavaScript file to use, e.g., "python".', + max_length=20, + verbose_name="ace mode name", + ), + ), + ( + "pygments", + models.CharField( + help_text="Language ID for Pygments highlighting in source windows.", + max_length=20, + verbose_name="pygments name", + ), + ), + ( + "template", + models.TextField( + blank=True, + help_text="Code template to display in submission editor.", + verbose_name="code template", + ), + ), + ( + "info", + models.CharField( + blank=True, + help_text="Do not set this unless you know what you're doing! It will override the usually more specific, judge-provided runtime info!", + max_length=50, + verbose_name="runtime info override", + ), + ), + ( + "description", + models.TextField( + blank=True, + help_text="Use field this to inform users of quirks with your environment, additional restrictions, etc.", + verbose_name="language description", + ), + ), + ( + "extension", + models.CharField( + help_text='The extension of source files, e.g., "py" or "cpp".', + max_length=10, + verbose_name="extension", + ), + ), ], options={ - 'ordering': ['key'], - 'verbose_name_plural': 'languages', - 'verbose_name': 'language', + "ordering": ["key"], + "verbose_name_plural": "languages", + "verbose_name": "language", }, ), migrations.CreateModel( - name='LanguageLimit', + name="LanguageLimit", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time_limit', models.FloatField(verbose_name='time limit')), - ('memory_limit', models.IntegerField(verbose_name='memory limit')), - ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='language')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("time_limit", models.FloatField(verbose_name="time limit")), + ("memory_limit", models.IntegerField(verbose_name="memory limit")), + ( + "language", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Language", + verbose_name="language", + ), + ), ], options={ - 'verbose_name_plural': 'language-specific resource limits', - 'verbose_name': 'language-specific resource limit', + "verbose_name_plural": "language-specific resource limits", + "verbose_name": "language-specific resource limit", }, ), migrations.CreateModel( - name='License', + name="License", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[-\\w.]+$', 'License key must be ^[-\\w.]+$')], verbose_name='key')), - ('link', models.CharField(max_length=256, verbose_name='link')), - ('name', models.CharField(max_length=256, verbose_name='full name')), - ('display', models.CharField(blank=True, help_text='Displayed on pages under this license', max_length=256, verbose_name='short name')), - ('icon', models.CharField(blank=True, help_text='URL to the icon', max_length=256, verbose_name='icon')), - ('text', models.TextField(verbose_name='license text')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "key", + models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[-\\w.]+$", "License key must be ^[-\\w.]+$" + ) + ], + verbose_name="key", + ), + ), + ("link", models.CharField(max_length=256, verbose_name="link")), + ("name", models.CharField(max_length=256, verbose_name="full name")), + ( + "display", + models.CharField( + blank=True, + help_text="Displayed on pages under this license", + max_length=256, + verbose_name="short name", + ), + ), + ( + "icon", + models.CharField( + blank=True, + help_text="URL to the icon", + max_length=256, + verbose_name="icon", + ), + ), + ("text", models.TextField(verbose_name="license text")), ], options={ - 'verbose_name_plural': 'licenses', - 'verbose_name': 'license', + "verbose_name_plural": "licenses", + "verbose_name": "license", }, ), migrations.CreateModel( - name='MiscConfig', + name="MiscConfig", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(db_index=True, max_length=30)), - ('value', models.TextField(blank=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("key", models.CharField(db_index=True, max_length=30)), + ("value", models.TextField(blank=True)), ], options={ - 'verbose_name_plural': 'miscellaneous configuration', - 'verbose_name': 'configuration item', + "verbose_name_plural": "miscellaneous configuration", + "verbose_name": "configuration item", }, ), migrations.CreateModel( - name='NavigationBar', + name="NavigationBar", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order', models.PositiveIntegerField(db_index=True, verbose_name='order')), - ('key', models.CharField(max_length=10, unique=True, verbose_name='identifier')), - ('label', models.CharField(max_length=20, verbose_name='label')), - ('path', models.CharField(max_length=255, verbose_name='link path')), - ('regex', models.TextField(validators=[judge.models.interface.validate_regex], verbose_name='highlight regex')), - ('lft', models.PositiveIntegerField(db_index=True, editable=False)), - ('rght', models.PositiveIntegerField(db_index=True, editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(db_index=True, editable=False)), - ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='judge.NavigationBar', verbose_name='parent item')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "order", + models.PositiveIntegerField(db_index=True, verbose_name="order"), + ), + ( + "key", + models.CharField( + max_length=10, unique=True, verbose_name="identifier" + ), + ), + ("label", models.CharField(max_length=20, verbose_name="label")), + ("path", models.CharField(max_length=255, verbose_name="link path")), + ( + "regex", + models.TextField( + validators=[judge.models.interface.validate_regex], + verbose_name="highlight regex", + ), + ), + ("lft", models.PositiveIntegerField(db_index=True, editable=False)), + ("rght", models.PositiveIntegerField(db_index=True, editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ("level", models.PositiveIntegerField(db_index=True, editable=False)), + ( + "parent", + mptt.fields.TreeForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="judge.NavigationBar", + verbose_name="parent item", + ), + ), ], options={ - 'verbose_name_plural': 'navigation bar', - 'verbose_name': 'navigation item', + "verbose_name_plural": "navigation bar", + "verbose_name": "navigation item", }, ), migrations.CreateModel( - name='Organization', + name="Organization", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='organization title')), - ('slug', models.SlugField(help_text='Organization name shown in URL', max_length=128, verbose_name='organization slug')), - ('short_name', models.CharField(help_text='Displayed beside user name during contests', max_length=20, verbose_name='short name')), - ('about', models.TextField(verbose_name='organization description')), - ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), - ('is_open', models.BooleanField(default=True, help_text='Allow joining organization', verbose_name='is open organization?')), - ('slots', models.IntegerField(blank=True, help_text='Maximum amount of users in this organization, only applicable to private organizations', null=True, verbose_name='maximum size')), - ('access_code', models.CharField(blank=True, help_text='Student access code', max_length=7, null=True, verbose_name='access code')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField(max_length=128, verbose_name="organization title"), + ), + ( + "slug", + models.SlugField( + help_text="Organization name shown in URL", + max_length=128, + verbose_name="organization slug", + ), + ), + ( + "short_name", + models.CharField( + help_text="Displayed beside user name during contests", + max_length=20, + verbose_name="short name", + ), + ), + ("about", models.TextField(verbose_name="organization description")), + ( + "creation_date", + models.DateTimeField( + auto_now_add=True, verbose_name="creation date" + ), + ), + ( + "is_open", + models.BooleanField( + default=True, + help_text="Allow joining organization", + verbose_name="is open organization?", + ), + ), + ( + "slots", + models.IntegerField( + blank=True, + help_text="Maximum amount of users in this organization, only applicable to private organizations", + null=True, + verbose_name="maximum size", + ), + ), + ( + "access_code", + models.CharField( + blank=True, + help_text="Student access code", + max_length=7, + null=True, + verbose_name="access code", + ), + ), ], options={ - 'ordering': ['name'], - 'verbose_name_plural': 'organizations', - 'permissions': (('organization_admin', 'Administer organizations'), ('edit_all_organization', 'Edit all organizations')), - 'verbose_name': 'organization', + "ordering": ["name"], + "verbose_name_plural": "organizations", + "permissions": ( + ("organization_admin", "Administer organizations"), + ("edit_all_organization", "Edit all organizations"), + ), + "verbose_name": "organization", }, ), migrations.CreateModel( - name='OrganizationRequest', + name="OrganizationRequest", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='request time')), - ('state', models.CharField(choices=[('P', 'Pending'), ('A', 'Approved'), ('R', 'Rejected')], max_length=1, verbose_name='state')), - ('reason', models.TextField(verbose_name='reason')), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests', to='judge.Organization', verbose_name='organization')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "time", + models.DateTimeField( + auto_now_add=True, verbose_name="request time" + ), + ), + ( + "state", + models.CharField( + choices=[ + ("P", "Pending"), + ("A", "Approved"), + ("R", "Rejected"), + ], + max_length=1, + verbose_name="state", + ), + ), + ("reason", models.TextField(verbose_name="reason")), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="requests", + to="judge.Organization", + verbose_name="organization", + ), + ), ], options={ - 'verbose_name_plural': 'organization join requests', - 'verbose_name': 'organization join request', + "verbose_name_plural": "organization join requests", + "verbose_name": "organization join request", }, ), migrations.CreateModel( - name='PrivateMessage', + name="PrivateMessage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=50, verbose_name='message title')), - ('content', models.TextField(verbose_name='message body')), - ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='message timestamp')), - ('read', models.BooleanField(default=False, verbose_name='read')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField(max_length=50, verbose_name="message title"), + ), + ("content", models.TextField(verbose_name="message body")), + ( + "timestamp", + models.DateTimeField( + auto_now_add=True, verbose_name="message timestamp" + ), + ), + ("read", models.BooleanField(default=False, verbose_name="read")), ], ), migrations.CreateModel( - name='PrivateMessageThread', + name="PrivateMessageThread", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('messages', models.ManyToManyField(to='judge.PrivateMessage', verbose_name='messages in the thread')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "messages", + models.ManyToManyField( + to="judge.PrivateMessage", verbose_name="messages in the thread" + ), + ), ], ), migrations.CreateModel( - name='Problem', + name="Problem", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z0-9]+$', 'Problem code must be ^[a-z0-9]+$')], verbose_name='problem code')), - ('name', models.CharField(db_index=True, max_length=100, verbose_name='problem name')), - ('description', models.TextField(verbose_name='problem body')), - ('time_limit', models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', verbose_name='time limit')), - ('memory_limit', models.IntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', verbose_name='memory limit')), - ('short_circuit', models.BooleanField(default=False)), - ('points', models.FloatField(verbose_name='points')), - ('partial', models.BooleanField(default=False, verbose_name='allows partial points')), - ('is_public', models.BooleanField(db_index=True, default=False, verbose_name='publicly visible')), - ('is_manually_managed', models.BooleanField(db_index=True, default=False, help_text='Whether judges should be allowed to manage data or not', verbose_name='manually managed')), - ('date', models.DateTimeField(blank=True, db_index=True, help_text="Doesn't have magic ability to auto-publish due to backward compatibility", null=True, verbose_name='date of publishing')), - ('og_image', models.CharField(blank=True, max_length=150, verbose_name='OpenGraph image')), - ('summary', models.TextField(blank=True, help_text='Plain-text, shown in meta description tag, e.g. for social media.', verbose_name='problem summary')), - ('user_count', models.IntegerField(default=0, help_text='The number of users who solved the problem.', verbose_name='number of users')), - ('ac_rate', models.FloatField(default=0, verbose_name='solve rate')), - ('is_organization_private', models.BooleanField(default=False, verbose_name='private to organizations')), - ('allowed_languages', models.ManyToManyField(to='judge.Language', verbose_name='allowed languages')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[a-z0-9]+$", "Problem code must be ^[a-z0-9]+$" + ) + ], + verbose_name="problem code", + ), + ), + ( + "name", + models.CharField( + db_index=True, max_length=100, verbose_name="problem name" + ), + ), + ("description", models.TextField(verbose_name="problem body")), + ( + "time_limit", + models.FloatField( + help_text="The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.", + verbose_name="time limit", + ), + ), + ( + "memory_limit", + models.IntegerField( + help_text="The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).", + verbose_name="memory limit", + ), + ), + ("short_circuit", models.BooleanField(default=False)), + ("points", models.FloatField(verbose_name="points")), + ( + "partial", + models.BooleanField( + default=False, verbose_name="allows partial points" + ), + ), + ( + "is_public", + models.BooleanField( + db_index=True, default=False, verbose_name="publicly visible" + ), + ), + ( + "is_manually_managed", + models.BooleanField( + db_index=True, + default=False, + help_text="Whether judges should be allowed to manage data or not", + verbose_name="manually managed", + ), + ), + ( + "date", + models.DateTimeField( + blank=True, + db_index=True, + help_text="Doesn't have magic ability to auto-publish due to backward compatibility", + null=True, + verbose_name="date of publishing", + ), + ), + ( + "og_image", + models.CharField( + blank=True, max_length=150, verbose_name="OpenGraph image" + ), + ), + ( + "summary", + models.TextField( + blank=True, + help_text="Plain-text, shown in meta description tag, e.g. for social media.", + verbose_name="problem summary", + ), + ), + ( + "user_count", + models.IntegerField( + default=0, + help_text="The number of users who solved the problem.", + verbose_name="number of users", + ), + ), + ("ac_rate", models.FloatField(default=0, verbose_name="solve rate")), + ( + "is_organization_private", + models.BooleanField( + default=False, verbose_name="private to organizations" + ), + ), + ( + "allowed_languages", + models.ManyToManyField( + to="judge.Language", verbose_name="allowed languages" + ), + ), ], options={ - 'verbose_name_plural': 'problems', - 'permissions': (('see_private_problem', 'See hidden problems'), ('edit_own_problem', 'Edit own problems'), ('edit_all_problem', 'Edit all problems'), ('edit_public_problem', 'Edit all public problems'), ('clone_problem', 'Clone problem'), ('change_public_visibility', 'Change is_public field'), ('change_manually_managed', 'Change is_manually_managed field'), ('see_organization_problem', 'See organization-private problems')), - 'verbose_name': 'problem', + "verbose_name_plural": "problems", + "permissions": ( + ("see_private_problem", "See hidden problems"), + ("edit_own_problem", "Edit own problems"), + ("edit_all_problem", "Edit all problems"), + ("edit_public_problem", "Edit all public problems"), + ("clone_problem", "Clone problem"), + ("change_public_visibility", "Change is_public field"), + ("change_manually_managed", "Change is_manually_managed field"), + ("see_organization_problem", "See organization-private problems"), + ), + "verbose_name": "problem", }, ), migrations.CreateModel( - name='ProblemClarification', + name="ProblemClarification", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.TextField(verbose_name='clarification body')), - ('date', models.DateTimeField(auto_now_add=True, verbose_name='clarification timestamp')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Problem', verbose_name='clarified problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.TextField(verbose_name="clarification body")), + ( + "date", + models.DateTimeField( + auto_now_add=True, verbose_name="clarification timestamp" + ), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Problem", + verbose_name="clarified problem", + ), + ), ], ), migrations.CreateModel( - name='ProblemData', + name="ProblemData", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('zipfile', models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, verbose_name='data zip file')), - ('generator', models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, verbose_name='generator file')), - ('output_prefix', models.IntegerField(blank=True, null=True, verbose_name='output prefix length')), - ('output_limit', models.IntegerField(blank=True, null=True, verbose_name='output limit length')), - ('feedback', models.TextField(blank=True, verbose_name='init.yml generation feedback')), - ('checker', models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line')], max_length=10, verbose_name='checker')), - ('checker_args', models.TextField(blank=True, help_text='checker arguments as a JSON object', verbose_name='checker arguments')), - ('problem', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='data_files', to='judge.Problem', verbose_name='problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "zipfile", + models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + verbose_name="data zip file", + ), + ), + ( + "generator", + models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + verbose_name="generator file", + ), + ), + ( + "output_prefix", + models.IntegerField( + blank=True, null=True, verbose_name="output prefix length" + ), + ), + ( + "output_limit", + models.IntegerField( + blank=True, null=True, verbose_name="output limit length" + ), + ), + ( + "feedback", + models.TextField( + blank=True, verbose_name="init.yml generation feedback" + ), + ), + ( + "checker", + models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ], + max_length=10, + verbose_name="checker", + ), + ), + ( + "checker_args", + models.TextField( + blank=True, + help_text="checker arguments as a JSON object", + verbose_name="checker arguments", + ), + ), + ( + "problem", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="data_files", + to="judge.Problem", + verbose_name="problem", + ), + ), ], ), migrations.CreateModel( - name='ProblemGroup', + name="ProblemGroup", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20, unique=True, verbose_name='problem group ID')), - ('full_name', models.CharField(max_length=100, verbose_name='problem group name')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=20, unique=True, verbose_name="problem group ID" + ), + ), + ( + "full_name", + models.CharField(max_length=100, verbose_name="problem group name"), + ), ], options={ - 'ordering': ['full_name'], - 'verbose_name_plural': 'problem groups', - 'verbose_name': 'problem group', + "ordering": ["full_name"], + "verbose_name_plural": "problem groups", + "verbose_name": "problem group", }, ), migrations.CreateModel( - name='ProblemTestCase', + name="ProblemTestCase", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order', models.IntegerField(verbose_name='case position')), - ('type', models.CharField(choices=[('C', 'Normal case'), ('S', 'Batch start'), ('E', 'Batch end')], default='C', max_length=1, verbose_name='case type')), - ('input_file', models.CharField(blank=True, max_length=100, verbose_name='input file name')), - ('output_file', models.CharField(blank=True, max_length=100, verbose_name='output file name')), - ('generator_args', models.TextField(blank=True, verbose_name='generator arguments')), - ('points', models.IntegerField(blank=True, null=True, verbose_name='point value')), - ('is_pretest', models.BooleanField(verbose_name='case is pretest?')), - ('output_prefix', models.IntegerField(blank=True, null=True, verbose_name='output prefix length')), - ('output_limit', models.IntegerField(blank=True, null=True, verbose_name='output limit length')), - ('checker', models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line')], max_length=10, verbose_name='checker')), - ('checker_args', models.TextField(blank=True, help_text='checker arguments as a JSON object', verbose_name='checker arguments')), - ('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cases', to='judge.Problem', verbose_name='problem data set')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order", models.IntegerField(verbose_name="case position")), + ( + "type", + models.CharField( + choices=[ + ("C", "Normal case"), + ("S", "Batch start"), + ("E", "Batch end"), + ], + default="C", + max_length=1, + verbose_name="case type", + ), + ), + ( + "input_file", + models.CharField( + blank=True, max_length=100, verbose_name="input file name" + ), + ), + ( + "output_file", + models.CharField( + blank=True, max_length=100, verbose_name="output file name" + ), + ), + ( + "generator_args", + models.TextField(blank=True, verbose_name="generator arguments"), + ), + ( + "points", + models.IntegerField( + blank=True, null=True, verbose_name="point value" + ), + ), + ("is_pretest", models.BooleanField(verbose_name="case is pretest?")), + ( + "output_prefix", + models.IntegerField( + blank=True, null=True, verbose_name="output prefix length" + ), + ), + ( + "output_limit", + models.IntegerField( + blank=True, null=True, verbose_name="output limit length" + ), + ), + ( + "checker", + models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ], + max_length=10, + verbose_name="checker", + ), + ), + ( + "checker_args", + models.TextField( + blank=True, + help_text="checker arguments as a JSON object", + verbose_name="checker arguments", + ), + ), + ( + "dataset", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="cases", + to="judge.Problem", + verbose_name="problem data set", + ), + ), ], ), migrations.CreateModel( - name='ProblemTranslation', + name="ProblemTranslation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('language', models.CharField(choices=[('de', 'German'), ('en', 'English'), ('es', 'Spanish'), ('fr', 'French'), ('hr', 'Croatian'), ('hu', 'Hungarian'), ('ko', 'Korean'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sr-latn', 'Serbian (Latin)'), ('tr', 'Turkish'), ('vi', 'Vietnamese'), ('zh-hans', 'Simplified Chinese')], max_length=7, verbose_name='language')), - ('name', models.CharField(db_index=True, max_length=100, verbose_name='translated name')), - ('description', models.TextField(verbose_name='translated description')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='judge.Problem', verbose_name='problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "language", + models.CharField( + choices=[ + ("de", "German"), + ("en", "English"), + ("es", "Spanish"), + ("fr", "French"), + ("hr", "Croatian"), + ("hu", "Hungarian"), + ("ko", "Korean"), + ("ro", "Romanian"), + ("ru", "Russian"), + ("sr-latn", "Serbian (Latin)"), + ("tr", "Turkish"), + ("vi", "Vietnamese"), + ("zh-hans", "Simplified Chinese"), + ], + max_length=7, + verbose_name="language", + ), + ), + ( + "name", + models.CharField( + db_index=True, max_length=100, verbose_name="translated name" + ), + ), + ( + "description", + models.TextField(verbose_name="translated description"), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="translations", + to="judge.Problem", + verbose_name="problem", + ), + ), ], options={ - 'verbose_name_plural': 'problem translations', - 'verbose_name': 'problem translation', + "verbose_name_plural": "problem translations", + "verbose_name": "problem translation", }, ), migrations.CreateModel( - name='ProblemType', + name="ProblemType", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20, unique=True, verbose_name='problem category ID')), - ('full_name', models.CharField(max_length=100, verbose_name='problem category name')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=20, unique=True, verbose_name="problem category ID" + ), + ), + ( + "full_name", + models.CharField( + max_length=100, verbose_name="problem category name" + ), + ), ], options={ - 'ordering': ['full_name'], - 'verbose_name_plural': 'problem types', - 'verbose_name': 'problem type', + "ordering": ["full_name"], + "verbose_name_plural": "problem types", + "verbose_name": "problem type", }, ), migrations.CreateModel( - name='Profile', + name="Profile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('about', models.TextField(blank=True, null=True, verbose_name='self-description')), - ('timezone', models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='America/Toronto', max_length=50, verbose_name='location')), - ('points', models.FloatField(db_index=True, default=0)), - ('performance_points', models.FloatField(db_index=True, default=0)), - ('problem_count', models.IntegerField(db_index=True, default=0)), - ('ace_theme', models.CharField(choices=[('ambiance', 'Ambiance'), ('chaos', 'Chaos'), ('chrome', 'Chrome'), ('clouds', 'Clouds'), ('clouds_midnight', 'Clouds Midnight'), ('cobalt', 'Cobalt'), ('crimson_editor', 'Crimson Editor'), ('dawn', 'Dawn'), ('dreamweaver', 'Dreamweaver'), ('eclipse', 'Eclipse'), ('github', 'Github'), ('idle_fingers', 'Idle Fingers'), ('katzenmilch', 'Katzenmilch'), ('kr_theme', 'KR Theme'), ('kuroir', 'Kuroir'), ('merbivore', 'Merbivore'), ('merbivore_soft', 'Merbivore Soft'), ('mono_industrial', 'Mono Industrial'), ('monokai', 'Monokai'), ('pastel_on_dark', 'Pastel on Dark'), ('solarized_dark', 'Solarized Dark'), ('solarized_light', 'Solarized Light'), ('terminal', 'Terminal'), ('textmate', 'Textmate'), ('tomorrow', 'Tomorrow'), ('tomorrow_night', 'Tomorrow Night'), ('tomorrow_night_blue', 'Tomorrow Night Blue'), ('tomorrow_night_bright', 'Tomorrow Night Bright'), ('tomorrow_night_eighties', 'Tomorrow Night Eighties'), ('twilight', 'Twilight'), ('vibrant_ink', 'Vibrant Ink'), ('xcode', 'XCode')], default='github', max_length=30)), - ('last_access', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last access time')), - ('ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='last IP')), - ('display_rank', models.CharField(choices=[('user', 'Normal User'), ('setter', 'Problem Setter'), ('admin', 'Admin')], default='user', max_length=10, verbose_name='display rank')), - ('mute', models.BooleanField(default=False, help_text='Some users are at their best when silent.', verbose_name='comment mute')), - ('is_unlisted', models.BooleanField(default=False, help_text='User will not be ranked.', verbose_name='unlisted user')), - ('rating', models.IntegerField(default=None, null=True)), - ('user_script', models.TextField(blank=True, default='', help_text='User-defined JavaScript for site customization.', max_length=65536, verbose_name='user script')), - ('math_engine', models.CharField(choices=[('tex', 'Leave as LaTeX'), ('svg', 'SVG with PNG fallback'), ('mml', 'MathML only'), ('jax', 'MathJax with SVG/PNG fallback'), ('auto', 'Detect best quality')], default='auto', help_text='the rendering engine used to render math', max_length=4, verbose_name='math engine')), - ('is_totp_enabled', models.BooleanField(default=False, help_text='check to enable TOTP-based two factor authentication', verbose_name='2FA enabled')), - ('totp_key', judge.models.profile.EncryptedNullCharField(blank=True, help_text='32 character base32-encoded key for TOTP', max_length=32, null=True, validators=[django.core.validators.RegexValidator('^$|^[A-Z2-7]{32}$', 'TOTP key must be empty or base32')], verbose_name='TOTP key')), - ('notes', models.TextField(blank=True, help_text='Notes for administrators regarding this user.', null=True, verbose_name='internal notes')), - ('current_contest', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='judge.ContestParticipation', verbose_name='current contest')), - ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='preferred language')), - ('organizations', sortedm2m.fields.SortedManyToManyField(blank=True, help_text=None, related_name='members', related_query_name='member', to='judge.Organization', verbose_name='organization')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user associated')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "about", + models.TextField( + blank=True, null=True, verbose_name="self-description" + ), + ), + ( + "timezone", + models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ( + "America/Argentina/Catamarca", + "Argentina/Catamarca", + ), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ( + "America/Argentina/La_Rioja", + "Argentina/La_Rioja", + ), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ( + "America/Argentina/San_Juan", + "Argentina/San_Juan", + ), + ( + "America/Argentina/San_Luis", + "Argentina/San_Luis", + ), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ( + "America/Indiana/Indianapolis", + "Indiana/Indianapolis", + ), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ( + "America/Indiana/Petersburg", + "Indiana/Petersburg", + ), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ( + "America/Kentucky/Louisville", + "Kentucky/Louisville", + ), + ( + "America/Kentucky/Monticello", + "Kentucky/Monticello", + ), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ( + "America/North_Dakota/Beulah", + "North_Dakota/Beulah", + ), + ( + "America/North_Dakota/Center", + "North_Dakota/Center", + ), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="America/Toronto", + max_length=50, + verbose_name="location", + ), + ), + ("points", models.FloatField(db_index=True, default=0)), + ("performance_points", models.FloatField(db_index=True, default=0)), + ("problem_count", models.IntegerField(db_index=True, default=0)), + ( + "ace_theme", + models.CharField( + choices=[ + ("ambiance", "Ambiance"), + ("chaos", "Chaos"), + ("chrome", "Chrome"), + ("clouds", "Clouds"), + ("clouds_midnight", "Clouds Midnight"), + ("cobalt", "Cobalt"), + ("crimson_editor", "Crimson Editor"), + ("dawn", "Dawn"), + ("dreamweaver", "Dreamweaver"), + ("eclipse", "Eclipse"), + ("github", "Github"), + ("idle_fingers", "Idle Fingers"), + ("katzenmilch", "Katzenmilch"), + ("kr_theme", "KR Theme"), + ("kuroir", "Kuroir"), + ("merbivore", "Merbivore"), + ("merbivore_soft", "Merbivore Soft"), + ("mono_industrial", "Mono Industrial"), + ("monokai", "Monokai"), + ("pastel_on_dark", "Pastel on Dark"), + ("solarized_dark", "Solarized Dark"), + ("solarized_light", "Solarized Light"), + ("terminal", "Terminal"), + ("textmate", "Textmate"), + ("tomorrow", "Tomorrow"), + ("tomorrow_night", "Tomorrow Night"), + ("tomorrow_night_blue", "Tomorrow Night Blue"), + ("tomorrow_night_bright", "Tomorrow Night Bright"), + ("tomorrow_night_eighties", "Tomorrow Night Eighties"), + ("twilight", "Twilight"), + ("vibrant_ink", "Vibrant Ink"), + ("xcode", "XCode"), + ], + default="github", + max_length=30, + ), + ), + ( + "last_access", + models.DateTimeField( + default=django.utils.timezone.now, + verbose_name="last access time", + ), + ), + ( + "ip", + models.GenericIPAddressField( + blank=True, null=True, verbose_name="last IP" + ), + ), + ( + "display_rank", + models.CharField( + choices=[ + ("user", "Normal User"), + ("setter", "Problem Setter"), + ("admin", "Admin"), + ], + default="user", + max_length=10, + verbose_name="display rank", + ), + ), + ( + "mute", + models.BooleanField( + default=False, + help_text="Some users are at their best when silent.", + verbose_name="comment mute", + ), + ), + ( + "is_unlisted", + models.BooleanField( + default=False, + help_text="User will not be ranked.", + verbose_name="unlisted user", + ), + ), + ("rating", models.IntegerField(default=None, null=True)), + ( + "user_script", + models.TextField( + blank=True, + default="", + help_text="User-defined JavaScript for site customization.", + max_length=65536, + verbose_name="user script", + ), + ), + ( + "math_engine", + models.CharField( + choices=[ + ("tex", "Leave as LaTeX"), + ("svg", "SVG with PNG fallback"), + ("mml", "MathML only"), + ("jax", "MathJax with SVG/PNG fallback"), + ("auto", "Detect best quality"), + ], + default="auto", + help_text="the rendering engine used to render math", + max_length=4, + verbose_name="math engine", + ), + ), + ( + "is_totp_enabled", + models.BooleanField( + default=False, + help_text="check to enable TOTP-based two factor authentication", + verbose_name="2FA enabled", + ), + ), + ( + "totp_key", + judge.models.profile.EncryptedNullCharField( + blank=True, + help_text="32 character base32-encoded key for TOTP", + max_length=32, + null=True, + validators=[ + django.core.validators.RegexValidator( + "^$|^[A-Z2-7]{32}$", "TOTP key must be empty or base32" + ) + ], + verbose_name="TOTP key", + ), + ), + ( + "notes", + models.TextField( + blank=True, + help_text="Notes for administrators regarding this user.", + null=True, + verbose_name="internal notes", + ), + ), + ( + "current_contest", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="judge.ContestParticipation", + verbose_name="current contest", + ), + ), + ( + "language", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Language", + verbose_name="preferred language", + ), + ), + ( + "organizations", + sortedm2m.fields.SortedManyToManyField( + blank=True, + help_text=None, + related_name="members", + related_query_name="member", + to="judge.Organization", + verbose_name="organization", + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="user associated", + ), + ), ], options={ - 'verbose_name_plural': 'user profiles', - 'permissions': (('test_site', 'Shows in-progress development stuff'), ('totp', 'Edit TOTP settings')), - 'verbose_name': 'user profile', + "verbose_name_plural": "user profiles", + "permissions": ( + ("test_site", "Shows in-progress development stuff"), + ("totp", "Edit TOTP settings"), + ), + "verbose_name": "user profile", }, ), migrations.CreateModel( - name='Rating', + name="Rating", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('rank', models.IntegerField(verbose_name='rank')), - ('rating', models.IntegerField(verbose_name='rating')), - ('volatility', models.IntegerField(verbose_name='volatility')), - ('last_rated', models.DateTimeField(db_index=True, verbose_name='last rated')), - ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='judge.Contest', verbose_name='contest')), - ('participation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='rating', to='judge.ContestParticipation', verbose_name='participation')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='judge.Profile', verbose_name='user')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("rank", models.IntegerField(verbose_name="rank")), + ("rating", models.IntegerField(verbose_name="rating")), + ("volatility", models.IntegerField(verbose_name="volatility")), + ( + "last_rated", + models.DateTimeField(db_index=True, verbose_name="last rated"), + ), + ( + "contest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ratings", + to="judge.Contest", + verbose_name="contest", + ), + ), + ( + "participation", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="rating", + to="judge.ContestParticipation", + verbose_name="participation", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ratings", + to="judge.Profile", + verbose_name="user", + ), + ), ], options={ - 'verbose_name_plural': 'contest ratings', - 'verbose_name': 'contest rating', + "verbose_name_plural": "contest ratings", + "verbose_name": "contest rating", }, ), migrations.CreateModel( - name='RuntimeVersion', + name="RuntimeVersion", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=64, verbose_name='runtime name')), - ('version', models.CharField(blank=True, max_length=64, verbose_name='runtime version')), - ('priority', models.IntegerField(default=0, verbose_name='order in which to display this runtime')), - ('judge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Judge', verbose_name='judge on which this runtime exists')), - ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='language to which this runtime belongs')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=64, verbose_name="runtime name")), + ( + "version", + models.CharField( + blank=True, max_length=64, verbose_name="runtime version" + ), + ), + ( + "priority", + models.IntegerField( + default=0, verbose_name="order in which to display this runtime" + ), + ), + ( + "judge", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Judge", + verbose_name="judge on which this runtime exists", + ), + ), + ( + "language", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Language", + verbose_name="language to which this runtime belongs", + ), + ), ], ), migrations.CreateModel( - name='Solution', + name="Solution", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_public', models.BooleanField(default=False, verbose_name='public visibility')), - ('publish_on', models.DateTimeField(verbose_name='publish date')), - ('content', models.TextField(verbose_name='editorial content')), - ('authors', models.ManyToManyField(blank=True, to='judge.Profile', verbose_name='authors')), - ('problem', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='solution', to='judge.Problem', verbose_name='associated problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "is_public", + models.BooleanField( + default=False, verbose_name="public visibility" + ), + ), + ("publish_on", models.DateTimeField(verbose_name="publish date")), + ("content", models.TextField(verbose_name="editorial content")), + ( + "authors", + models.ManyToManyField( + blank=True, to="judge.Profile", verbose_name="authors" + ), + ), + ( + "problem", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="solution", + to="judge.Problem", + verbose_name="associated problem", + ), + ), ], options={ - 'verbose_name_plural': 'solutions', - 'permissions': (('see_private_solution', 'See hidden solutions'),), - 'verbose_name': 'solution', + "verbose_name_plural": "solutions", + "permissions": (("see_private_solution", "See hidden solutions"),), + "verbose_name": "solution", }, ), migrations.CreateModel( - name='Submission', + name="Submission", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='submission time')), - ('time', models.FloatField(db_index=True, null=True, verbose_name='execution time')), - ('memory', models.FloatField(null=True, verbose_name='memory usage')), - ('points', models.FloatField(db_index=True, null=True, verbose_name='points granted')), - ('source', models.TextField(max_length=65536, verbose_name='source code')), - ('status', models.CharField(choices=[('QU', 'Queued'), ('P', 'Processing'), ('G', 'Grading'), ('D', 'Completed'), ('IE', 'Internal Error'), ('CE', 'Compile Error'), ('AB', 'Aborted')], db_index=True, default='QU', max_length=2, verbose_name='status')), - ('result', models.CharField(blank=True, choices=[('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('OLE', 'Output Limit Exceeded'), ('IR', 'Invalid Return'), ('RTE', 'Runtime Error'), ('CE', 'Compile Error'), ('IE', 'Internal Error'), ('SC', 'Short circuit'), ('AB', 'Aborted')], db_index=True, default=None, max_length=3, null=True, verbose_name='result')), - ('error', models.TextField(blank=True, null=True, verbose_name='compile errors')), - ('current_testcase', models.IntegerField(default=0)), - ('batch', models.BooleanField(default=False, verbose_name='batched cases')), - ('case_points', models.FloatField(default=0, verbose_name='test case points')), - ('case_total', models.FloatField(default=0, verbose_name='test case total points')), - ('was_rejudged', models.BooleanField(default=False, verbose_name='was rejudged by admin')), - ('is_pretested', models.BooleanField(default=False, verbose_name='was ran on pretests only')), - ('judged_on', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='judge.Judge', verbose_name='judged on')), - ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='submission language')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Problem')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "date", + models.DateTimeField( + auto_now_add=True, db_index=True, verbose_name="submission time" + ), + ), + ( + "time", + models.FloatField( + db_index=True, null=True, verbose_name="execution time" + ), + ), + ("memory", models.FloatField(null=True, verbose_name="memory usage")), + ( + "points", + models.FloatField( + db_index=True, null=True, verbose_name="points granted" + ), + ), + ( + "source", + models.TextField(max_length=65536, verbose_name="source code"), + ), + ( + "status", + models.CharField( + choices=[ + ("QU", "Queued"), + ("P", "Processing"), + ("G", "Grading"), + ("D", "Completed"), + ("IE", "Internal Error"), + ("CE", "Compile Error"), + ("AB", "Aborted"), + ], + db_index=True, + default="QU", + max_length=2, + verbose_name="status", + ), + ), + ( + "result", + models.CharField( + blank=True, + choices=[ + ("AC", "Accepted"), + ("WA", "Wrong Answer"), + ("TLE", "Time Limit Exceeded"), + ("MLE", "Memory Limit Exceeded"), + ("OLE", "Output Limit Exceeded"), + ("IR", "Invalid Return"), + ("RTE", "Runtime Error"), + ("CE", "Compile Error"), + ("IE", "Internal Error"), + ("SC", "Short circuit"), + ("AB", "Aborted"), + ], + db_index=True, + default=None, + max_length=3, + null=True, + verbose_name="result", + ), + ), + ( + "error", + models.TextField( + blank=True, null=True, verbose_name="compile errors" + ), + ), + ("current_testcase", models.IntegerField(default=0)), + ( + "batch", + models.BooleanField(default=False, verbose_name="batched cases"), + ), + ( + "case_points", + models.FloatField(default=0, verbose_name="test case points"), + ), + ( + "case_total", + models.FloatField(default=0, verbose_name="test case total points"), + ), + ( + "was_rejudged", + models.BooleanField( + default=False, verbose_name="was rejudged by admin" + ), + ), + ( + "is_pretested", + models.BooleanField( + default=False, verbose_name="was ran on pretests only" + ), + ), + ( + "judged_on", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="judge.Judge", + verbose_name="judged on", + ), + ), + ( + "language", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Language", + verbose_name="submission language", + ), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="judge.Problem" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="judge.Profile" + ), + ), ], options={ - 'verbose_name_plural': 'submissions', - 'permissions': (('abort_any_submission', 'Abort any submission'), ('rejudge_submission', 'Rejudge the submission'), ('rejudge_submission_lot', 'Rejudge a lot of submissions'), ('spam_submission', 'Submit without limit'), ('view_all_submission', 'View all submission'), ('resubmit_other', "Resubmit others' submission")), - 'verbose_name': 'submission', + "verbose_name_plural": "submissions", + "permissions": ( + ("abort_any_submission", "Abort any submission"), + ("rejudge_submission", "Rejudge the submission"), + ("rejudge_submission_lot", "Rejudge a lot of submissions"), + ("spam_submission", "Submit without limit"), + ("view_all_submission", "View all submission"), + ("resubmit_other", "Resubmit others' submission"), + ), + "verbose_name": "submission", }, ), migrations.CreateModel( - name='SubmissionTestCase', + name="SubmissionTestCase", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('case', models.IntegerField(verbose_name='test case ID')), - ('status', models.CharField(choices=[('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('OLE', 'Output Limit Exceeded'), ('IR', 'Invalid Return'), ('RTE', 'Runtime Error'), ('CE', 'Compile Error'), ('IE', 'Internal Error'), ('SC', 'Short circuit'), ('AB', 'Aborted')], max_length=3, verbose_name='status flag')), - ('time', models.FloatField(null=True, verbose_name='execution time')), - ('memory', models.FloatField(null=True, verbose_name='memory usage')), - ('points', models.FloatField(null=True, verbose_name='points granted')), - ('total', models.FloatField(null=True, verbose_name='points possible')), - ('batch', models.IntegerField(null=True, verbose_name='batch number')), - ('feedback', models.CharField(blank=True, max_length=50, verbose_name='judging feedback')), - ('extended_feedback', models.TextField(blank=True, verbose_name='extended judging feedback')), - ('output', models.TextField(blank=True, verbose_name='program output')), - ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='test_cases', to='judge.Submission', verbose_name='associated submission')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("case", models.IntegerField(verbose_name="test case ID")), + ( + "status", + models.CharField( + choices=[ + ("AC", "Accepted"), + ("WA", "Wrong Answer"), + ("TLE", "Time Limit Exceeded"), + ("MLE", "Memory Limit Exceeded"), + ("OLE", "Output Limit Exceeded"), + ("IR", "Invalid Return"), + ("RTE", "Runtime Error"), + ("CE", "Compile Error"), + ("IE", "Internal Error"), + ("SC", "Short circuit"), + ("AB", "Aborted"), + ], + max_length=3, + verbose_name="status flag", + ), + ), + ("time", models.FloatField(null=True, verbose_name="execution time")), + ("memory", models.FloatField(null=True, verbose_name="memory usage")), + ("points", models.FloatField(null=True, verbose_name="points granted")), + ("total", models.FloatField(null=True, verbose_name="points possible")), + ("batch", models.IntegerField(null=True, verbose_name="batch number")), + ( + "feedback", + models.CharField( + blank=True, max_length=50, verbose_name="judging feedback" + ), + ), + ( + "extended_feedback", + models.TextField( + blank=True, verbose_name="extended judging feedback" + ), + ), + ("output", models.TextField(blank=True, verbose_name="program output")), + ( + "submission", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="test_cases", + to="judge.Submission", + verbose_name="associated submission", + ), + ), ], options={ - 'verbose_name_plural': 'submission test cases', - 'verbose_name': 'submission test case', + "verbose_name_plural": "submission test cases", + "verbose_name": "submission test case", }, ), migrations.CreateModel( - name='Ticket', + name="Ticket", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100, verbose_name='ticket title')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), - ('notes', models.TextField(blank=True, help_text='Staff notes for this issue to aid in processing.', verbose_name='quick notes')), - ('object_id', models.PositiveIntegerField(verbose_name='linked item ID')), - ('is_open', models.BooleanField(default=True, verbose_name='is ticket open?')), - ('assignees', models.ManyToManyField(related_name='assigned_tickets', to='judge.Profile', verbose_name='assignees')), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='linked item type')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='judge.Profile', verbose_name='ticket creator')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField(max_length=100, verbose_name="ticket title"), + ), + ( + "time", + models.DateTimeField( + auto_now_add=True, verbose_name="creation time" + ), + ), + ( + "notes", + models.TextField( + blank=True, + help_text="Staff notes for this issue to aid in processing.", + verbose_name="quick notes", + ), + ), + ( + "object_id", + models.PositiveIntegerField(verbose_name="linked item ID"), + ), + ( + "is_open", + models.BooleanField(default=True, verbose_name="is ticket open?"), + ), + ( + "assignees", + models.ManyToManyField( + related_name="assigned_tickets", + to="judge.Profile", + verbose_name="assignees", + ), + ), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.ContentType", + verbose_name="linked item type", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", + to="judge.Profile", + verbose_name="ticket creator", + ), + ), ], ), migrations.CreateModel( - name='TicketMessage', + name="TicketMessage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('body', models.TextField(verbose_name='message body')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='message time')), - ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', related_query_name='message', to='judge.Ticket', verbose_name='ticket')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to='judge.Profile', verbose_name='poster')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("body", models.TextField(verbose_name="message body")), + ( + "time", + models.DateTimeField( + auto_now_add=True, verbose_name="message time" + ), + ), + ( + "ticket", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="messages", + related_query_name="message", + to="judge.Ticket", + verbose_name="ticket", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ticket_messages", + to="judge.Profile", + verbose_name="poster", + ), + ), ], ), migrations.AddField( - model_name='problem', - name='authors', - field=models.ManyToManyField(blank=True, related_name='authored_problems', to='judge.Profile', verbose_name='creators'), + model_name="problem", + name="authors", + field=models.ManyToManyField( + blank=True, + related_name="authored_problems", + to="judge.Profile", + verbose_name="creators", + ), ), migrations.AddField( - model_name='problem', - name='banned_users', - field=models.ManyToManyField(blank=True, help_text='Bans the selected users from submitting to this problem.', to='judge.Profile', verbose_name='personae non gratae'), + model_name="problem", + name="banned_users", + field=models.ManyToManyField( + blank=True, + help_text="Bans the selected users from submitting to this problem.", + to="judge.Profile", + verbose_name="personae non gratae", + ), ), migrations.AddField( - model_name='problem', - name='curators', - field=models.ManyToManyField(blank=True, help_text='These users will be able to edit a problem, but not be publicly shown as an author.', related_name='curated_problems', to='judge.Profile', verbose_name='curators'), + model_name="problem", + name="curators", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to edit a problem, but not be publicly shown as an author.", + related_name="curated_problems", + to="judge.Profile", + verbose_name="curators", + ), ), migrations.AddField( - model_name='problem', - name='group', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.ProblemGroup', verbose_name='problem group'), + model_name="problem", + name="group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.ProblemGroup", + verbose_name="problem group", + ), ), migrations.AddField( - model_name='problem', - name='license', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='judge.License'), + model_name="problem", + name="license", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="judge.License", + ), ), migrations.AddField( - model_name='problem', - name='organizations', - field=models.ManyToManyField(blank=True, help_text='If private, only these organizations may see the problem.', to='judge.Organization', verbose_name='organizations'), + model_name="problem", + name="organizations", + field=models.ManyToManyField( + blank=True, + help_text="If private, only these organizations may see the problem.", + to="judge.Organization", + verbose_name="organizations", + ), ), migrations.AddField( - model_name='problem', - name='testers', - field=models.ManyToManyField(blank=True, help_text='These users will be able to view a private problem, but not edit it.', related_name='tested_problems', to='judge.Profile', verbose_name='testers'), + model_name="problem", + name="testers", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to view a private problem, but not edit it.", + related_name="tested_problems", + to="judge.Profile", + verbose_name="testers", + ), ), migrations.AddField( - model_name='problem', - name='types', - field=models.ManyToManyField(to='judge.ProblemType', verbose_name='problem types'), + model_name="problem", + name="types", + field=models.ManyToManyField( + to="judge.ProblemType", verbose_name="problem types" + ), ), migrations.AddField( - model_name='privatemessage', - name='sender', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to='judge.Profile', verbose_name='sender'), + model_name="privatemessage", + name="sender", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sent_messages", + to="judge.Profile", + verbose_name="sender", + ), ), migrations.AddField( - model_name='privatemessage', - name='target', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to='judge.Profile', verbose_name='target'), + model_name="privatemessage", + name="target", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="received_messages", + to="judge.Profile", + verbose_name="target", + ), ), migrations.AddField( - model_name='organizationrequest', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests', to='judge.Profile', verbose_name='user'), + model_name="organizationrequest", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="requests", + to="judge.Profile", + verbose_name="user", + ), ), migrations.AddField( - model_name='organization', - name='admins', - field=models.ManyToManyField(help_text='Those who can edit this organization', related_name='admin_of', to='judge.Profile', verbose_name='administrators'), + model_name="organization", + name="admins", + field=models.ManyToManyField( + help_text="Those who can edit this organization", + related_name="admin_of", + to="judge.Profile", + verbose_name="administrators", + ), ), migrations.AddField( - model_name='organization', - name='registrant', - field=models.ForeignKey(help_text='User who registered this organization', on_delete=django.db.models.deletion.CASCADE, related_name='registrant+', to='judge.Profile', verbose_name='registrant'), + model_name="organization", + name="registrant", + field=models.ForeignKey( + help_text="User who registered this organization", + on_delete=django.db.models.deletion.CASCADE, + related_name="registrant+", + to="judge.Profile", + verbose_name="registrant", + ), ), migrations.AddField( - model_name='languagelimit', - name='problem', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='language_limits', to='judge.Problem', verbose_name='problem'), + model_name="languagelimit", + name="problem", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="language_limits", + to="judge.Problem", + verbose_name="problem", + ), ), migrations.AddField( - model_name='judge', - name='problems', - field=models.ManyToManyField(related_name='judges', to='judge.Problem', verbose_name='problems'), + model_name="judge", + name="problems", + field=models.ManyToManyField( + related_name="judges", to="judge.Problem", verbose_name="problems" + ), ), migrations.AddField( - model_name='judge', - name='runtimes', - field=models.ManyToManyField(related_name='judges', to='judge.Language', verbose_name='judges'), + model_name="judge", + name="runtimes", + field=models.ManyToManyField( + related_name="judges", to="judge.Language", verbose_name="judges" + ), ), migrations.AddField( - model_name='contestsubmission', - name='submission', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='contest', to='judge.Submission', verbose_name='submission'), + model_name="contestsubmission", + name="submission", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="contest", + to="judge.Submission", + verbose_name="submission", + ), ), migrations.AddField( - model_name='contestproblem', - name='problem', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contests', to='judge.Problem', verbose_name='problem'), + model_name="contestproblem", + name="problem", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contests", + to="judge.Problem", + verbose_name="problem", + ), ), migrations.AddField( - model_name='contestparticipation', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contest_history', to='judge.Profile', verbose_name='user'), + model_name="contestparticipation", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contest_history", + to="judge.Profile", + verbose_name="user", + ), ), migrations.AddField( - model_name='contest', - name='banned_users', - field=models.ManyToManyField(blank=True, help_text='Bans the selected users from joining this contest.', to='judge.Profile', verbose_name='personae non gratae'), + model_name="contest", + name="banned_users", + field=models.ManyToManyField( + blank=True, + help_text="Bans the selected users from joining this contest.", + to="judge.Profile", + verbose_name="personae non gratae", + ), ), migrations.AddField( - model_name='contest', - name='organizations', - field=models.ManyToManyField(blank=True, help_text='If private, only these organizations may see the contest', to='judge.Organization', verbose_name='organizations'), + model_name="contest", + name="organizations", + field=models.ManyToManyField( + blank=True, + help_text="If private, only these organizations may see the contest", + to="judge.Organization", + verbose_name="organizations", + ), ), migrations.AddField( - model_name='contest', - name='organizers', - field=models.ManyToManyField(help_text='These people will be able to edit the contest.', related_name='_contest_organizers_+', to='judge.Profile'), + model_name="contest", + name="organizers", + field=models.ManyToManyField( + help_text="These people will be able to edit the contest.", + related_name="_contest_organizers_+", + to="judge.Profile", + ), ), migrations.AddField( - model_name='contest', - name='problems', - field=models.ManyToManyField(through='judge.ContestProblem', to='judge.Problem', verbose_name='problems'), + model_name="contest", + name="problems", + field=models.ManyToManyField( + through="judge.ContestProblem", + to="judge.Problem", + verbose_name="problems", + ), ), migrations.AddField( - model_name='contest', - name='rate_exclude', - field=models.ManyToManyField(blank=True, related_name='_contest_rate_exclude_+', to='judge.Profile', verbose_name='exclude from ratings'), + model_name="contest", + name="rate_exclude", + field=models.ManyToManyField( + blank=True, + related_name="_contest_rate_exclude_+", + to="judge.Profile", + verbose_name="exclude from ratings", + ), ), migrations.AddField( - model_name='contest', - name='tags', - field=models.ManyToManyField(blank=True, related_name='contests', to='judge.ContestTag', verbose_name='contest tags'), + model_name="contest", + name="tags", + field=models.ManyToManyField( + blank=True, + related_name="contests", + to="judge.ContestTag", + verbose_name="contest tags", + ), ), migrations.AddField( - model_name='commentvote', - name='voter', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='voted_comments', to='judge.Profile'), + model_name="commentvote", + name="voter", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="voted_comments", + to="judge.Profile", + ), ), migrations.AddField( - model_name='comment', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='commenter'), + model_name="comment", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Profile", + verbose_name="commenter", + ), ), migrations.AddField( - model_name='comment', - name='parent', - field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='judge.Comment', verbose_name='parent'), + model_name="comment", + name="parent", + field=mptt.fields.TreeForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="replies", + to="judge.Comment", + verbose_name="parent", + ), ), migrations.AddField( - model_name='blogpost', - name='authors', - field=models.ManyToManyField(blank=True, to='judge.Profile', verbose_name='authors'), + model_name="blogpost", + name="authors", + field=models.ManyToManyField( + blank=True, to="judge.Profile", verbose_name="authors" + ), ), migrations.AlterUniqueTogether( - name='rating', - unique_together=set([('user', 'contest')]), + name="rating", + unique_together=set([("user", "contest")]), ), migrations.AlterUniqueTogether( - name='problemtranslation', - unique_together=set([('problem', 'language')]), + name="problemtranslation", + unique_together=set([("problem", "language")]), ), migrations.AlterUniqueTogether( - name='languagelimit', - unique_together=set([('problem', 'language')]), + name="languagelimit", + unique_together=set([("problem", "language")]), ), migrations.AlterUniqueTogether( - name='contestproblem', - unique_together=set([('problem', 'contest')]), + name="contestproblem", + unique_together=set([("problem", "contest")]), ), migrations.AlterUniqueTogether( - name='contestparticipation', - unique_together=set([('contest', 'user', 'virtual')]), + name="contestparticipation", + unique_together=set([("contest", "user", "virtual")]), ), migrations.AlterUniqueTogether( - name='commentvote', - unique_together=set([('voter', 'comment')]), + name="commentvote", + unique_together=set([("voter", "comment")]), ), ] diff --git a/judge/migrations/0085_submission_source.py b/judge/migrations/0085_submission_source.py index d094efd..3f0dd18 100644 --- a/judge/migrations/0085_submission_source.py +++ b/judge/migrations/0085_submission_source.py @@ -7,33 +7,61 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0084_contest_formats'), + ("judge", "0084_contest_formats"), ] operations = [ migrations.CreateModel( - name='SubmissionSource', + name="SubmissionSource", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('source', models.TextField(max_length=65536, verbose_name='source code')), - ('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='link', to='judge.Submission', verbose_name='associated submission')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "source", + models.TextField(max_length=65536, verbose_name="source code"), + ), + ( + "submission", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="link", + to="judge.Submission", + verbose_name="associated submission", + ), + ), ], ), migrations.RunSQL( - ['''INSERT INTO judge_submissionsource (source, submission_id) - SELECT source, id AS 'submission_id' FROM judge_submission;'''], - ['''UPDATE judge_submission sub + [ + """INSERT INTO judge_submissionsource (source, submission_id) + SELECT source, id AS 'submission_id' FROM judge_submission;""" + ], + [ + """UPDATE judge_submission sub INNER JOIN judge_submissionsource src ON sub.id = src.submission_id - SET sub.source = src.source;'''], + SET sub.source = src.source;""" + ], elidable=True, ), migrations.RemoveField( - model_name='submission', - name='source', + model_name="submission", + name="source", ), migrations.AlterField( - model_name='submissionsource', - name='submission', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='source', to='judge.Submission', verbose_name='associated submission'), + model_name="submissionsource", + name="submission", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="source", + to="judge.Submission", + verbose_name="associated submission", + ), ), ] diff --git a/judge/migrations/0086_rating_ceiling.py b/judge/migrations/0086_rating_ceiling.py index a544a21..d529ac1 100644 --- a/judge/migrations/0086_rating_ceiling.py +++ b/judge/migrations/0086_rating_ceiling.py @@ -6,18 +6,28 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0085_submission_source'), + ("judge", "0085_submission_source"), ] operations = [ migrations.AddField( - model_name='contest', - name='rating_ceiling', - field=models.IntegerField(blank=True, help_text='Rating ceiling for contest', null=True, verbose_name='rating ceiling'), + model_name="contest", + name="rating_ceiling", + field=models.IntegerField( + blank=True, + help_text="Rating ceiling for contest", + null=True, + verbose_name="rating ceiling", + ), ), migrations.AddField( - model_name='contest', - name='rating_floor', - field=models.IntegerField(blank=True, help_text='Rating floor for contest', null=True, verbose_name='rating floor'), + model_name="contest", + name="rating_floor", + field=models.IntegerField( + blank=True, + help_text="Rating floor for contest", + null=True, + verbose_name="rating floor", + ), ), ] diff --git a/judge/migrations/0087_problem_resource_limits.py b/judge/migrations/0087_problem_resource_limits.py index e6da153..c9fd18f 100644 --- a/judge/migrations/0087_problem_resource_limits.py +++ b/judge/migrations/0087_problem_resource_limits.py @@ -7,18 +7,28 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0086_rating_ceiling'), + ("judge", "0086_rating_ceiling"), ] operations = [ migrations.AlterField( - model_name='problem', - name='memory_limit', - field=models.PositiveIntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', verbose_name='memory limit'), + model_name="problem", + name="memory_limit", + field=models.PositiveIntegerField( + help_text="The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).", + verbose_name="memory limit", + ), ), migrations.AlterField( - model_name='problem', - name='time_limit', - field=models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2000)], verbose_name='time limit'), + model_name="problem", + name="time_limit", + field=models.FloatField( + help_text="The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(2000), + ], + verbose_name="time limit", + ), ), ] diff --git a/judge/migrations/0088_private_contests.py b/judge/migrations/0088_private_contests.py index b3505b5..8a5b886 100644 --- a/judge/migrations/0088_private_contests.py +++ b/judge/migrations/0088_private_contests.py @@ -6,32 +6,53 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0087_problem_resource_limits'), + ("judge", "0087_problem_resource_limits"), ] operations = [ migrations.AlterModelOptions( - name='contest', - options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + name="contest", + options={ + "permissions": ( + ("see_private_contest", "See private contests"), + ("edit_own_contest", "Edit own contests"), + ("edit_all_contest", "Edit all contests"), + ("contest_rating", "Rate contests"), + ("contest_access_code", "Contest access codes"), + ("create_private_contest", "Create private contests"), + ), + "verbose_name": "contest", + "verbose_name_plural": "contests", + }, ), migrations.RenameField( - model_name='contest', - old_name='is_public', - new_name='is_visible', + model_name="contest", + old_name="is_public", + new_name="is_visible", ), migrations.AddField( - model_name='contest', - name='is_organization_private', - field=models.BooleanField(default=False, verbose_name='private to organizations'), + model_name="contest", + name="is_organization_private", + field=models.BooleanField( + default=False, verbose_name="private to organizations" + ), ), migrations.AddField( - model_name='contest', - name='private_contestants', - field=models.ManyToManyField(blank=True, help_text='If private, only these users may see the contest', related_name='_contest_private_contestants_+', to='judge.Profile', verbose_name='private contestants'), + model_name="contest", + name="private_contestants", + field=models.ManyToManyField( + blank=True, + help_text="If private, only these users may see the contest", + related_name="_contest_private_contestants_+", + to="judge.Profile", + verbose_name="private contestants", + ), ), migrations.AlterField( - model_name='contest', - name='is_private', - field=models.BooleanField(default=False, verbose_name='private to specific users'), + model_name="contest", + name="is_private", + field=models.BooleanField( + default=False, verbose_name="private to specific users" + ), ), ] diff --git a/judge/migrations/0089_submission_to_contest.py b/judge/migrations/0089_submission_to_contest.py index 5d464cb..f1cb283 100644 --- a/judge/migrations/0089_submission_to_contest.py +++ b/judge/migrations/0089_submission_to_contest.py @@ -7,21 +7,31 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0088_private_contests'), + ("judge", "0088_private_contests"), ] operations = [ migrations.AddField( - model_name='submission', - name='contest_object', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='judge.Contest', verbose_name='contest'), + model_name="submission", + name="contest_object", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="judge.Contest", + verbose_name="contest", + ), ), - migrations.RunSQL(''' + migrations.RunSQL( + """ UPDATE `judge_submission` INNER JOIN `judge_contestsubmission` ON (`judge_submission`.`id` = `judge_contestsubmission`.`submission_id`) INNER JOIN `judge_contestparticipation` ON (`judge_contestsubmission`.`participation_id` = `judge_contestparticipation`.`id`) SET `judge_submission`.`contest_object_id` = `judge_contestparticipation`.`contest_id` - ''', migrations.RunSQL.noop), + """, + migrations.RunSQL.noop, + ), ] diff --git a/judge/migrations/0090_fix_contest_visibility.py b/judge/migrations/0090_fix_contest_visibility.py index 10480fb..8e89949 100644 --- a/judge/migrations/0090_fix_contest_visibility.py +++ b/judge/migrations/0090_fix_contest_visibility.py @@ -4,16 +4,19 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0089_submission_to_contest'), + ("judge", "0089_submission_to_contest"), ] operations = [ - migrations.RunSQL(''' + migrations.RunSQL( + """ UPDATE `judge_contest` SET `judge_contest`.`is_private` = 0, `judge_contest`.`is_organization_private` = 1 WHERE `judge_contest`.`is_private` = 1 - ''', ''' + """, + """ UPDATE `judge_contest` SET `judge_contest`.`is_private` = `judge_contest`.`is_organization_private` - '''), + """, + ), ] diff --git a/judge/migrations/0091_compiler_message_ansi2html.py b/judge/migrations/0091_compiler_message_ansi2html.py index 7240f95..32db431 100644 --- a/judge/migrations/0091_compiler_message_ansi2html.py +++ b/judge/migrations/0091_compiler_message_ansi2html.py @@ -5,17 +5,17 @@ from lxml.html.clean import clean_html def strip_error_html(apps, schema_editor): - Submission = apps.get_model('judge', 'Submission') + Submission = apps.get_model("judge", "Submission") for sub in Submission.objects.filter(error__isnull=False).iterator(): if sub.error: sub.error = clean_html(lh.fromstring(sub.error)).text_content() - sub.save(update_fields=['error']) + sub.save(update_fields=["error"]) class Migration(migrations.Migration): dependencies = [ - ('judge', '0090_fix_contest_visibility'), + ("judge", "0090_fix_contest_visibility"), ] operations = [ diff --git a/judge/migrations/0092_contest_clone.py b/judge/migrations/0092_contest_clone.py index 0235513..0982ee8 100644 --- a/judge/migrations/0092_contest_clone.py +++ b/judge/migrations/0092_contest_clone.py @@ -6,12 +6,24 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0091_compiler_message_ansi2html'), + ("judge", "0091_compiler_message_ansi2html"), ] operations = [ migrations.AlterModelOptions( - name='contest', - options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + name="contest", + options={ + "permissions": ( + ("see_private_contest", "See private contests"), + ("edit_own_contest", "Edit own contests"), + ("edit_all_contest", "Edit all contests"), + ("clone_contest", "Clone contest"), + ("contest_rating", "Rate contests"), + ("contest_access_code", "Contest access codes"), + ("create_private_contest", "Create private contests"), + ), + "verbose_name": "contest", + "verbose_name_plural": "contests", + }, ), ] diff --git a/judge/migrations/0093_contest_moss.py b/judge/migrations/0093_contest_moss.py index 10e2799..0dec938 100644 --- a/judge/migrations/0093_contest_moss.py +++ b/judge/migrations/0093_contest_moss.py @@ -7,39 +7,70 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0092_contest_clone'), + ("judge", "0092_contest_clone"), ] operations = [ migrations.CreateModel( - name='ContestMoss', + name="ContestMoss", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('language', models.CharField(max_length=10)), - ('submission_count', models.PositiveIntegerField(default=0)), - ('url', models.URLField(blank=True, null=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("language", models.CharField(max_length=10)), + ("submission_count", models.PositiveIntegerField(default=0)), + ("url", models.URLField(blank=True, null=True)), ], options={ - 'verbose_name': 'contest moss result', - 'verbose_name_plural': 'contest moss results', + "verbose_name": "contest moss result", + "verbose_name_plural": "contest moss results", }, ), migrations.AlterModelOptions( - name='contest', - options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + name="contest", + options={ + "permissions": ( + ("see_private_contest", "See private contests"), + ("edit_own_contest", "Edit own contests"), + ("edit_all_contest", "Edit all contests"), + ("clone_contest", "Clone contest"), + ("moss_contest", "MOSS contest"), + ("contest_rating", "Rate contests"), + ("contest_access_code", "Contest access codes"), + ("create_private_contest", "Create private contests"), + ), + "verbose_name": "contest", + "verbose_name_plural": "contests", + }, ), migrations.AddField( - model_name='contestmoss', - name='contest', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Contest', verbose_name='contest'), + model_name="contestmoss", + name="contest", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moss", + to="judge.Contest", + verbose_name="contest", + ), ), migrations.AddField( - model_name='contestmoss', - name='problem', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Problem', verbose_name='problem'), + model_name="contestmoss", + name="problem", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moss", + to="judge.Problem", + verbose_name="problem", + ), ), migrations.AlterUniqueTogether( - name='contestmoss', - unique_together={('contest', 'problem', 'language')}, + name="contestmoss", + unique_together={("contest", "problem", "language")}, ), ] diff --git a/judge/migrations/0094_submissiontestcase_unique_together.py b/judge/migrations/0094_submissiontestcase_unique_together.py index 8341329..c56cded 100644 --- a/judge/migrations/0094_submissiontestcase_unique_together.py +++ b/judge/migrations/0094_submissiontestcase_unique_together.py @@ -3,12 +3,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0093_contest_moss'), + ("judge", "0093_contest_moss"), ] operations = [ migrations.AlterUniqueTogether( - name='submissiontestcase', - unique_together={('submission', 'case')}, + name="submissiontestcase", + unique_together={("submission", "case")}, ), ] diff --git a/judge/migrations/0095_organization_logo_override.py b/judge/migrations/0095_organization_logo_override.py index b1d86eb..6db6499 100644 --- a/judge/migrations/0095_organization_logo_override.py +++ b/judge/migrations/0095_organization_logo_override.py @@ -6,13 +6,19 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0094_submissiontestcase_unique_together'), + ("judge", "0094_submissiontestcase_unique_together"), ] operations = [ migrations.AddField( - model_name='organization', - name='logo_override_image', - field=models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users viewing the organization.', max_length=150, verbose_name='Logo override image'), + model_name="organization", + name="logo_override_image", + field=models.CharField( + blank=True, + default="", + help_text="This image will replace the default site logo for users viewing the organization.", + max_length=150, + verbose_name="Logo override image", + ), ), ] diff --git a/judge/migrations/0096_profile_language_set_default.py b/judge/migrations/0096_profile_language_set_default.py index a0a9453..1468f78 100644 --- a/judge/migrations/0096_profile_language_set_default.py +++ b/judge/migrations/0096_profile_language_set_default.py @@ -7,21 +7,26 @@ import judge.models.runtime def create_python3(apps, schema_editor): - Language = apps.get_model('judge', 'Language') - Language.objects.get_or_create(key='PY3', defaults={'name': 'Python 3'})[0] + Language = apps.get_model("judge", "Language") + Language.objects.get_or_create(key="PY3", defaults={"name": "Python 3"})[0] class Migration(migrations.Migration): dependencies = [ - ('judge', '0095_organization_logo_override'), + ("judge", "0095_organization_logo_override"), ] operations = [ migrations.RunPython(create_python3, reverse_code=migrations.RunPython.noop), migrations.AlterField( - model_name='profile', - name='language', - field=models.ForeignKey(default=judge.models.runtime.Language.get_default_language_pk, on_delete=django.db.models.deletion.SET_DEFAULT, to='judge.Language', verbose_name='preferred language'), + model_name="profile", + name="language", + field=models.ForeignKey( + default=judge.models.runtime.Language.get_default_language_pk, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="judge.Language", + verbose_name="preferred language", + ), ), ] diff --git a/judge/migrations/0097_participation_is_disqualified.py b/judge/migrations/0097_participation_is_disqualified.py index 537f83f..3d3f367 100644 --- a/judge/migrations/0097_participation_is_disqualified.py +++ b/judge/migrations/0097_participation_is_disqualified.py @@ -6,18 +6,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0096_profile_language_set_default'), + ("judge", "0096_profile_language_set_default"), ] operations = [ migrations.AddField( - model_name='contestparticipation', - name='is_disqualified', - field=models.BooleanField(default=False, help_text='Whether this participation is disqualified.', verbose_name='is disqualified'), + model_name="contestparticipation", + name="is_disqualified", + field=models.BooleanField( + default=False, + help_text="Whether this participation is disqualified.", + verbose_name="is disqualified", + ), ), migrations.AlterField( - model_name='contestparticipation', - name='virtual', - field=models.IntegerField(default=0, help_text='0 means non-virtual, otherwise the n-th virtual participation.', verbose_name='virtual participation id'), + model_name="contestparticipation", + name="virtual", + field=models.IntegerField( + default=0, + help_text="0 means non-virtual, otherwise the n-th virtual participation.", + verbose_name="virtual participation id", + ), ), ] diff --git a/judge/migrations/0098_auto_20200123_2136.py b/judge/migrations/0098_auto_20200123_2136.py index 333c67c..3093cd3 100644 --- a/judge/migrations/0098_auto_20200123_2136.py +++ b/judge/migrations/0098_auto_20200123_2136.py @@ -8,148 +8,961 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0097_participation_is_disqualified'), + ("judge", "0097_participation_is_disqualified"), ] operations = [ migrations.AlterField( - model_name='comment', - name='level', + model_name="comment", + name="level", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='comment', - name='lft', + model_name="comment", + name="lft", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='comment', - name='rght', + model_name="comment", + name="rght", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='contest', - name='format_name', - field=models.CharField(choices=[('atcoder', 'AtCoder'), ('default', 'Default'), ('ecoo', 'ECOO'), ('ioi', 'IOI')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format'), + model_name="contest", + name="format_name", + field=models.CharField( + choices=[ + ("atcoder", "AtCoder"), + ("default", "Default"), + ("ecoo", "ECOO"), + ("ioi", "IOI"), + ], + default="default", + help_text="The contest format module to use.", + max_length=32, + verbose_name="contest format", + ), ), migrations.AlterField( - model_name='judge', - name='auth_key', - field=models.CharField(help_text='A key to authenticate this judge', max_length=100, verbose_name='authentication key'), + model_name="judge", + name="auth_key", + field=models.CharField( + help_text="A key to authenticate this judge", + max_length=100, + verbose_name="authentication key", + ), ), migrations.AlterField( - model_name='language', - name='description', - field=models.TextField(blank=True, help_text='Use this field to inform users of quirks with your environment, additional restrictions, etc.', verbose_name='language description'), + model_name="language", + name="description", + field=models.TextField( + blank=True, + help_text="Use this field to inform users of quirks with your environment, additional restrictions, etc.", + verbose_name="language description", + ), ), migrations.AlterField( - model_name='languagelimit', - name='memory_limit', - field=models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1048576)], verbose_name='memory limit'), + model_name="languagelimit", + name="memory_limit", + field=models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1048576), + ], + verbose_name="memory limit", + ), ), migrations.AlterField( - model_name='languagelimit', - name='time_limit', - field=models.FloatField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(60)], verbose_name='time limit'), + model_name="languagelimit", + name="time_limit", + field=models.FloatField( + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(60), + ], + verbose_name="time limit", + ), ), migrations.AlterField( - model_name='navigationbar', - name='level', + model_name="navigationbar", + name="level", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='navigationbar', - name='lft', + model_name="navigationbar", + name="lft", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='navigationbar', - name='rght', + model_name="navigationbar", + name="rght", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='problem', - name='allowed_languages', - field=models.ManyToManyField(help_text='List of allowed submission languages.', to='judge.Language', verbose_name='allowed languages'), + model_name="problem", + name="allowed_languages", + field=models.ManyToManyField( + help_text="List of allowed submission languages.", + to="judge.Language", + verbose_name="allowed languages", + ), ), migrations.AlterField( - model_name='problem', - name='authors', - field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the problem, and be listed as authors.', related_name='authored_problems', to='judge.Profile', verbose_name='creators'), + model_name="problem", + name="authors", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to edit the problem, and be listed as authors.", + related_name="authored_problems", + to="judge.Profile", + verbose_name="creators", + ), ), migrations.AlterField( - model_name='problem', - name='code', - field=models.CharField(help_text='A short, unique code for the problem, used in the url after /problem/', max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z0-9]+$', 'Problem code must be ^[a-z0-9]+$')], verbose_name='problem code'), + model_name="problem", + name="code", + field=models.CharField( + help_text="A short, unique code for the problem, used in the url after /problem/", + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[a-z0-9]+$", "Problem code must be ^[a-z0-9]+$" + ) + ], + verbose_name="problem code", + ), ), migrations.AlterField( - model_name='problem', - name='curators', - field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the problem, but not be listed as authors.', related_name='curated_problems', to='judge.Profile', verbose_name='curators'), + model_name="problem", + name="curators", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to edit the problem, but not be listed as authors.", + related_name="curated_problems", + to="judge.Profile", + verbose_name="curators", + ), ), migrations.AlterField( - model_name='problem', - name='group', - field=models.ForeignKey(help_text='The group of problem, shown under Category in the problem list.', on_delete=django.db.models.deletion.CASCADE, to='judge.ProblemGroup', verbose_name='problem group'), + model_name="problem", + name="group", + field=models.ForeignKey( + help_text="The group of problem, shown under Category in the problem list.", + on_delete=django.db.models.deletion.CASCADE, + to="judge.ProblemGroup", + verbose_name="problem group", + ), ), migrations.AlterField( - model_name='problem', - name='is_manually_managed', - field=models.BooleanField(db_index=True, default=False, help_text='Whether judges should be allowed to manage data or not.', verbose_name='manually managed'), + model_name="problem", + name="is_manually_managed", + field=models.BooleanField( + db_index=True, + default=False, + help_text="Whether judges should be allowed to manage data or not.", + verbose_name="manually managed", + ), ), migrations.AlterField( - model_name='problem', - name='license', - field=models.ForeignKey(blank=True, help_text='The license under which this problem is published.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='judge.License'), + model_name="problem", + name="license", + field=models.ForeignKey( + blank=True, + help_text="The license under which this problem is published.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="judge.License", + ), ), migrations.AlterField( - model_name='problem', - name='memory_limit', - field=models.PositiveIntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1048576)], verbose_name='memory limit'), + model_name="problem", + name="memory_limit", + field=models.PositiveIntegerField( + help_text="The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1048576), + ], + verbose_name="memory limit", + ), ), migrations.AlterField( - model_name='problem', - name='name', - field=models.CharField(db_index=True, help_text='The full name of the problem, as shown in the problem list.', max_length=100, verbose_name='problem name'), + model_name="problem", + name="name", + field=models.CharField( + db_index=True, + help_text="The full name of the problem, as shown in the problem list.", + max_length=100, + verbose_name="problem name", + ), ), migrations.AlterField( - model_name='problem', - name='points', - field=models.FloatField(help_text="Points awarded for problem completion. Points are displayed with a 'p' suffix if partial.", validators=[django.core.validators.MinValueValidator(0)], verbose_name='points'), + model_name="problem", + name="points", + field=models.FloatField( + help_text="Points awarded for problem completion. Points are displayed with a 'p' suffix if partial.", + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="points", + ), ), migrations.AlterField( - model_name='problem', - name='testers', - field=models.ManyToManyField(blank=True, help_text='These users will be able to view the private problem, but not edit it.', related_name='tested_problems', to='judge.Profile', verbose_name='testers'), + model_name="problem", + name="testers", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to view the private problem, but not edit it.", + related_name="tested_problems", + to="judge.Profile", + verbose_name="testers", + ), ), migrations.AlterField( - model_name='problem', - name='time_limit', - field=models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(60)], verbose_name='time limit'), + model_name="problem", + name="time_limit", + field=models.FloatField( + help_text="The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(60), + ], + verbose_name="time limit", + ), ), migrations.AlterField( - model_name='problem', - name='types', - field=models.ManyToManyField(help_text="The type of problem, as shown on the problem's page.", to='judge.ProblemType', verbose_name='problem types'), + model_name="problem", + name="types", + field=models.ManyToManyField( + help_text="The type of problem, as shown on the problem's page.", + to="judge.ProblemType", + verbose_name="problem types", + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('checker.py', 'Custom checker')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("checker.py", "Custom checker"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('checker.py', 'Custom checker')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("checker.py", "Custom checker"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtranslation', - name='language', - field=models.CharField(choices=[('de', 'German'), ('en', 'English'), ('es', 'Spanish'), ('fr', 'French'), ('hr', 'Croatian'), ('hu', 'Hungarian'), ('ja', 'Japanese'), ('ko', 'Korean'), ('pt', 'Brazilian Portuguese'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sr-latn', 'Serbian (Latin)'), ('tr', 'Turkish'), ('vi', 'Vietnamese'), ('zh-hans', 'Simplified Chinese'), ('zh-hant', 'Traditional Chinese')], max_length=7, verbose_name='language'), + model_name="problemtranslation", + name="language", + field=models.CharField( + choices=[ + ("de", "German"), + ("en", "English"), + ("es", "Spanish"), + ("fr", "French"), + ("hr", "Croatian"), + ("hu", "Hungarian"), + ("ja", "Japanese"), + ("ko", "Korean"), + ("pt", "Brazilian Portuguese"), + ("ro", "Romanian"), + ("ru", "Russian"), + ("sr-latn", "Serbian (Latin)"), + ("tr", "Turkish"), + ("vi", "Vietnamese"), + ("zh-hans", "Simplified Chinese"), + ("zh-hant", "Traditional Chinese"), + ], + max_length=7, + verbose_name="language", + ), ), migrations.AlterField( - model_name='profile', - name='timezone', - field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='America/Mexico_City', max_length=50, verbose_name='location'), + model_name="profile", + name="timezone", + field=models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ("America/Argentina/San_Juan", "Argentina/San_Juan"), + ("America/Argentina/San_Luis", "Argentina/San_Luis"), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ("America/Indiana/Indianapolis", "Indiana/Indianapolis"), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ("America/Indiana/Petersburg", "Indiana/Petersburg"), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ("America/Kentucky/Louisville", "Kentucky/Louisville"), + ("America/Kentucky/Monticello", "Kentucky/Monticello"), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ("America/North_Dakota/Beulah", "North_Dakota/Beulah"), + ("America/North_Dakota/Center", "North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="America/Mexico_City", + max_length=50, + verbose_name="location", + ), ), ] diff --git a/judge/migrations/0099_custom_checker.py b/judge/migrations/0099_custom_checker.py index b2552b7..24ab46b 100644 --- a/judge/migrations/0099_custom_checker.py +++ b/judge/migrations/0099_custom_checker.py @@ -8,23 +8,59 @@ import judge.utils.problem_data class Migration(migrations.Migration): dependencies = [ - ('judge', '0098_auto_20200123_2136'), + ("judge", "0098_auto_20200123_2136"), ] operations = [ migrations.AddField( - model_name='problemdata', - name='custom_checker', - field=models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, verbose_name='custom checker file'), + model_name="problemdata", + name="custom_checker", + field=models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + verbose_name="custom checker file", + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ], + max_length=10, + verbose_name="checker", + ), ), ] diff --git a/judge/migrations/0100_auto_20200127_0059.py b/judge/migrations/0100_auto_20200127_0059.py index e070f7a..5d1cd91 100644 --- a/judge/migrations/0100_auto_20200127_0059.py +++ b/judge/migrations/0100_auto_20200127_0059.py @@ -9,13 +9,24 @@ import judge.utils.problem_data class Migration(migrations.Migration): dependencies = [ - ('judge', '0099_custom_checker'), + ("judge", "0099_custom_checker"), ] operations = [ migrations.AlterField( - model_name='problemdata', - name='custom_checker', - field=models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['py'])], verbose_name='custom checker file'), + model_name="problemdata", + name="custom_checker", + field=models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + validators=[ + django.core.validators.FileExtensionValidator( + allowed_extensions=["py"] + ) + ], + verbose_name="custom checker file", + ), ), ] diff --git a/judge/migrations/0101_custom_validator.py b/judge/migrations/0101_custom_validator.py index 842006c..8575ce1 100644 --- a/judge/migrations/0101_custom_validator.py +++ b/judge/migrations/0101_custom_validator.py @@ -9,28 +9,727 @@ import judge.utils.problem_data class Migration(migrations.Migration): dependencies = [ - ('judge', '0100_auto_20200127_0059'), + ("judge", "0100_auto_20200127_0059"), ] operations = [ migrations.AddField( - model_name='problemdata', - name='custom_valid', - field=models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['cpp'])], verbose_name='custom validator file'), + model_name="problemdata", + name="custom_valid", + field=models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + validators=[ + django.core.validators.FileExtensionValidator( + allowed_extensions=["cpp"] + ) + ], + verbose_name="custom validator file", + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker'), ('custom_valid', 'Custom Validator')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ("custom_valid", "Custom Validator"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker'), ('custom_valid', 'Custom Validator')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ("custom_valid", "Custom Validator"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='profile', - name='timezone', - field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='Asia/Ho_Chi_Minh', max_length=50, verbose_name='location'), + model_name="profile", + name="timezone", + field=models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ("America/Argentina/San_Juan", "Argentina/San_Juan"), + ("America/Argentina/San_Luis", "Argentina/San_Luis"), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ("America/Indiana/Indianapolis", "Indiana/Indianapolis"), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ("America/Indiana/Petersburg", "Indiana/Petersburg"), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ("America/Kentucky/Louisville", "Kentucky/Louisville"), + ("America/Kentucky/Monticello", "Kentucky/Monticello"), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ("America/North_Dakota/Beulah", "North_Dakota/Beulah"), + ("America/North_Dakota/Center", "North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="Asia/Ho_Chi_Minh", + max_length=50, + verbose_name="location", + ), ), ] diff --git a/judge/migrations/0102_fix_custom_validator.py b/judge/migrations/0102_fix_custom_validator.py index a0dccce..e6bcd4a 100644 --- a/judge/migrations/0102_fix_custom_validator.py +++ b/judge/migrations/0102_fix_custom_validator.py @@ -6,18 +6,50 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0101_custom_validator'), + ("judge", "0101_custom_validator"), ] operations = [ migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker'), ('customval', 'Custom Validator')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ("customval", "Custom Validator"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker'), ('customval', 'Custom Validator')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ("customval", "Custom Validator"), + ], + max_length=10, + verbose_name="checker", + ), ), ] diff --git a/judge/migrations/0103_fix_custom_validator.py b/judge/migrations/0103_fix_custom_validator.py index 2720bb8..d354303 100644 --- a/judge/migrations/0103_fix_custom_validator.py +++ b/judge/migrations/0103_fix_custom_validator.py @@ -6,13 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0102_fix_custom_validator'), + ("judge", "0102_fix_custom_validator"), ] operations = [ migrations.RenameField( - model_name='problemdata', - old_name='custom_valid', - new_name='custom_validator', + model_name="problemdata", + old_name="custom_valid", + new_name="custom_validator", ), ] diff --git a/judge/migrations/0104_auto_20200410_1313.py b/judge/migrations/0104_auto_20200410_1313.py index bbefe21..ccba53d 100644 --- a/judge/migrations/0104_auto_20200410_1313.py +++ b/judge/migrations/0104_auto_20200410_1313.py @@ -6,23 +6,57 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0103_fix_custom_validator'), + ("judge", "0103_fix_custom_validator"), ] operations = [ migrations.AlterField( - model_name='contestproblem', - name='output_prefix_override', - field=models.IntegerField(blank=True, default=0, null=True, verbose_name='visible testcases'), + model_name="contestproblem", + name="output_prefix_override", + field=models.IntegerField( + blank=True, default=0, null=True, verbose_name="visible testcases" + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ], + max_length=10, + verbose_name="checker", + ), ), ] diff --git a/judge/migrations/0105_auto_20200523_0756.py b/judge/migrations/0105_auto_20200523_0756.py index 79ccb4e..553c24f 100644 --- a/judge/migrations/0105_auto_20200523_0756.py +++ b/judge/migrations/0105_auto_20200523_0756.py @@ -6,18 +6,681 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0104_auto_20200410_1313'), + ("judge", "0104_auto_20200410_1313"), ] operations = [ migrations.AlterField( - model_name='contestproblem', - name='output_prefix_override', - field=models.IntegerField(blank=True, default=0, help_text='0 to not show testcases, 1 to show', null=True, verbose_name='visible testcases'), + model_name="contestproblem", + name="output_prefix_override", + field=models.IntegerField( + blank=True, + default=0, + help_text="0 to not show testcases, 1 to show", + null=True, + verbose_name="visible testcases", + ), ), migrations.AlterField( - model_name='profile', - name='timezone', - field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Nuuk', 'Nuuk'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='Asia/Ho_Chi_Minh', max_length=50, verbose_name='location'), + model_name="profile", + name="timezone", + field=models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ("America/Argentina/San_Juan", "Argentina/San_Juan"), + ("America/Argentina/San_Luis", "Argentina/San_Luis"), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ("America/Indiana/Indianapolis", "Indiana/Indianapolis"), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ("America/Indiana/Petersburg", "Indiana/Petersburg"), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ("America/Kentucky/Louisville", "Kentucky/Louisville"), + ("America/Kentucky/Monticello", "Kentucky/Monticello"), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ("America/North_Dakota/Beulah", "North_Dakota/Beulah"), + ("America/North_Dakota/Center", "North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Nuuk", "Nuuk"), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="Asia/Ho_Chi_Minh", + max_length=50, + verbose_name="location", + ), ), ] diff --git a/judge/migrations/0106_friend.py b/judge/migrations/0106_friend.py index e820242..1235013 100644 --- a/judge/migrations/0106_friend.py +++ b/judge/migrations/0106_friend.py @@ -7,16 +7,31 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0105_auto_20200523_0756'), + ("judge", "0105_auto_20200523_0756"), ] operations = [ migrations.CreateModel( - name='Friend', + name="Friend", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('current_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_users', to='judge.Profile')), - ('users', models.ManyToManyField(to='judge.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "current_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="following_users", + to="judge.Profile", + ), + ), + ("users", models.ManyToManyField(to="judge.Profile")), ], ), ] diff --git a/judge/migrations/0107_notification.py b/judge/migrations/0107_notification.py index d8857d6..9539711 100644 --- a/judge/migrations/0107_notification.py +++ b/judge/migrations/0107_notification.py @@ -7,19 +7,45 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0106_friend'), + ("judge", "0106_friend"), ] operations = [ migrations.CreateModel( - name='Notification', + name="Notification", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')), - ('read', models.BooleanField(default=False, verbose_name='read')), - ('category', models.CharField(max_length=10, verbose_name='category')), - ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Comment', verbose_name='comment')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='judge.Profile', verbose_name='owner')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "time", + models.DateTimeField(auto_now_add=True, verbose_name="posted time"), + ), + ("read", models.BooleanField(default=False, verbose_name="read")), + ("category", models.CharField(max_length=10, verbose_name="category")), + ( + "comment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Comment", + verbose_name="comment", + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notifications", + to="judge.Profile", + verbose_name="owner", + ), + ), ], ), ] diff --git a/judge/migrations/0108_submission_judged_date.py b/judge/migrations/0108_submission_judged_date.py index 5794ace..e58ca6b 100644 --- a/judge/migrations/0108_submission_judged_date.py +++ b/judge/migrations/0108_submission_judged_date.py @@ -6,13 +6,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0107_notification'), + ("judge", "0107_notification"), ] operations = [ migrations.AddField( - model_name='submission', - name='judged_date', - field=models.DateTimeField(default=None, null=True, verbose_name='submission judge time'), + model_name="submission", + name="judged_date", + field=models.DateTimeField( + default=None, null=True, verbose_name="submission judge time" + ), ), ] diff --git a/judge/migrations/0109_auto_20201017_1151.py b/judge/migrations/0109_auto_20201017_1151.py index 9bce543..3e94df0 100644 --- a/judge/migrations/0109_auto_20201017_1151.py +++ b/judge/migrations/0109_auto_20201017_1151.py @@ -7,18 +7,27 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0108_submission_judged_date'), + ("judge", "0108_submission_judged_date"), ] operations = [ migrations.AddField( - model_name='notification', - name='html_link', - field=models.TextField(default='', max_length=1000, verbose_name='html link to comments, used for non-comments'), + model_name="notification", + name="html_link", + field=models.TextField( + default="", + max_length=1000, + verbose_name="html link to comments, used for non-comments", + ), ), migrations.AlterField( - model_name='notification', - name='comment', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='judge.Comment', verbose_name='comment'), + model_name="notification", + name="comment", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="judge.Comment", + verbose_name="comment", + ), ), ] diff --git a/judge/migrations/0110_notification_author.py b/judge/migrations/0110_notification_author.py index e0dec3e..367c96a 100644 --- a/judge/migrations/0110_notification_author.py +++ b/judge/migrations/0110_notification_author.py @@ -7,13 +7,18 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0109_auto_20201017_1151'), + ("judge", "0109_auto_20201017_1151"), ] operations = [ migrations.AddField( - model_name='notification', - name='author', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='who trigger, used for non-comment'), + model_name="notification", + name="author", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="judge.Profile", + verbose_name="who trigger, used for non-comment", + ), ), ] diff --git a/judge/migrations/0111_contest_decimal_points.py b/judge/migrations/0111_contest_decimal_points.py index 4dc3968..b83e3b5 100644 --- a/judge/migrations/0111_contest_decimal_points.py +++ b/judge/migrations/0111_contest_decimal_points.py @@ -7,18 +7,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0110_notification_author'), + ("judge", "0110_notification_author"), ] operations = [ migrations.AddField( - model_name='contest', - name='points_precision', - field=models.IntegerField(default=2, help_text='Number of digits to round points to.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)], verbose_name='precision points'), + model_name="contest", + name="points_precision", + field=models.IntegerField( + default=2, + help_text="Number of digits to round points to.", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(10), + ], + verbose_name="precision points", + ), ), migrations.AlterField( - model_name='contestparticipation', - name='score', - field=models.FloatField(db_index=True, default=0, verbose_name='score'), + model_name="contestparticipation", + name="score", + field=models.FloatField(db_index=True, default=0, verbose_name="score"), ), ] diff --git a/judge/migrations/0112_contest_view_contest_scoreboard.py b/judge/migrations/0112_contest_view_contest_scoreboard.py index 9a0dab2..00e2da4 100644 --- a/judge/migrations/0112_contest_view_contest_scoreboard.py +++ b/judge/migrations/0112_contest_view_contest_scoreboard.py @@ -6,13 +6,19 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0111_contest_decimal_points'), + ("judge", "0111_contest_decimal_points"), ] operations = [ migrations.AddField( - model_name='contest', - name='view_contest_scoreboard', - field=models.ManyToManyField(blank=True, help_text='These users will be able to view the scoreboard.', related_name='view_contest_scoreboard', to='judge.Profile', verbose_name='view contest scoreboard'), + model_name="contest", + name="view_contest_scoreboard", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to view the scoreboard.", + related_name="view_contest_scoreboard", + to="judge.Profile", + verbose_name="view contest scoreboard", + ), ), ] diff --git a/judge/migrations/0113_auto_20201228_0911.py b/judge/migrations/0113_auto_20201228_0911.py index f08350a..36d7dda 100644 --- a/judge/migrations/0113_auto_20201228_0911.py +++ b/judge/migrations/0113_auto_20201228_0911.py @@ -6,12 +6,26 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0112_contest_view_contest_scoreboard'), + ("judge", "0112_contest_view_contest_scoreboard"), ] operations = [ migrations.AlterModelOptions( - name='contest', - options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests'), ('change_contest_visibility', 'Change contest visibility')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + name="contest", + options={ + "permissions": ( + ("see_private_contest", "See private contests"), + ("edit_own_contest", "Edit own contests"), + ("edit_all_contest", "Edit all contests"), + ("clone_contest", "Clone contest"), + ("moss_contest", "MOSS contest"), + ("contest_rating", "Rate contests"), + ("contest_access_code", "Contest access codes"), + ("create_private_contest", "Create private contests"), + ("change_contest_visibility", "Change contest visibility"), + ), + "verbose_name": "contest", + "verbose_name_plural": "contests", + }, ), ] diff --git a/judge/migrations/0114_auto_20201228_1041.py b/judge/migrations/0114_auto_20201228_1041.py index 3103b3c..609c5d4 100644 --- a/judge/migrations/0114_auto_20201228_1041.py +++ b/judge/migrations/0114_auto_20201228_1041.py @@ -6,18 +6,25 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0113_auto_20201228_0911'), + ("judge", "0113_auto_20201228_0911"), ] operations = [ migrations.AddField( - model_name='blogpost', - name='is_organization_private', - field=models.BooleanField(default=False, verbose_name='private to organizations'), + model_name="blogpost", + name="is_organization_private", + field=models.BooleanField( + default=False, verbose_name="private to organizations" + ), ), migrations.AddField( - model_name='blogpost', - name='organizations', - field=models.ManyToManyField(blank=True, help_text='If private, only these organizations may see the blog post.', to='judge.Organization', verbose_name='organizations'), + model_name="blogpost", + name="organizations", + field=models.ManyToManyField( + blank=True, + help_text="If private, only these organizations may see the blog post.", + to="judge.Organization", + verbose_name="organizations", + ), ), ] diff --git a/judge/migrations/0115_auto_20210525_0222.py b/judge/migrations/0115_auto_20210525_0222.py index 91312de..52490c2 100644 --- a/judge/migrations/0115_auto_20210525_0222.py +++ b/judge/migrations/0115_auto_20210525_0222.py @@ -2,62 +2,109 @@ from django.db import migrations, models + def hide_scoreboard_eq_true(apps, schema_editor): - Contest = apps.get_model('judge', 'Contest') - Contest.objects.filter(hide_scoreboard=True).update(scoreboard_visibility='C') + Contest = apps.get_model("judge", "Contest") + Contest.objects.filter(hide_scoreboard=True).update(scoreboard_visibility="C") def scoreboard_visibility_eq_contest(apps, schema_editor): - Contest = apps.get_model('judge', 'Contest') - Contest.objects.filter(scoreboard_visibility__in=('C', 'P')).update(hide_scoreboard=True) + Contest = apps.get_model("judge", "Contest") + Contest.objects.filter(scoreboard_visibility__in=("C", "P")).update( + hide_scoreboard=True + ) + class Migration(migrations.Migration): dependencies = [ - ('judge', '0114_auto_20201228_1041'), + ("judge", "0114_auto_20201228_1041"), ] operations = [ migrations.AlterModelOptions( - name='contest', - options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests'), ('change_contest_visibility', 'Change contest visibility'), ('contest_problem_label', 'Edit contest problem label script')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + name="contest", + options={ + "permissions": ( + ("see_private_contest", "See private contests"), + ("edit_own_contest", "Edit own contests"), + ("edit_all_contest", "Edit all contests"), + ("clone_contest", "Clone contest"), + ("moss_contest", "MOSS contest"), + ("contest_rating", "Rate contests"), + ("contest_access_code", "Contest access codes"), + ("create_private_contest", "Create private contests"), + ("change_contest_visibility", "Change contest visibility"), + ("contest_problem_label", "Edit contest problem label script"), + ), + "verbose_name": "contest", + "verbose_name_plural": "contests", + }, ), migrations.RemoveField( - model_name='contest', - name='hide_scoreboard', + model_name="contest", + name="hide_scoreboard", ), migrations.RemoveField( - model_name='contest', - name='organizers', + model_name="contest", + name="organizers", ), migrations.AddField( - model_name='contest', - name='authors', - field=models.ManyToManyField(help_text='These users will be able to edit the contest.', related_name='_contest_authors_+', to='judge.Profile'), + model_name="contest", + name="authors", + field=models.ManyToManyField( + help_text="These users will be able to edit the contest.", + related_name="_contest_authors_+", + to="judge.Profile", + ), ), migrations.AddField( - model_name='contest', - name='curators', - field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the contest, but will not be listed as authors.', related_name='_contest_curators_+', to='judge.Profile'), + model_name="contest", + name="curators", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to edit the contest, but will not be listed as authors.", + related_name="_contest_curators_+", + to="judge.Profile", + ), ), migrations.AddField( - model_name='contest', - name='problem_label_script', - field=models.TextField(blank=True, help_text='A custom Lua function to generate problem labels. Requires a single function with an integer parameter, the zero-indexed contest problem index, and returns a string, the label.', verbose_name='contest problem label script'), + model_name="contest", + name="problem_label_script", + field=models.TextField( + blank=True, + help_text="A custom Lua function to generate problem labels. Requires a single function with an integer parameter, the zero-indexed contest problem index, and returns a string, the label.", + verbose_name="contest problem label script", + ), ), migrations.AddField( - model_name='contest', - name='scoreboard_visibility', - field=models.CharField(choices=[('V', 'Visible'), ('C', 'Hidden for duration of contest'), ('P', 'Hidden for duration of participation')], default='V', help_text='Scoreboard visibility through the duration of the contest', max_length=1, verbose_name='scoreboard visibility'), + model_name="contest", + name="scoreboard_visibility", + field=models.CharField( + choices=[ + ("V", "Visible"), + ("C", "Hidden for duration of contest"), + ("P", "Hidden for duration of participation"), + ], + default="V", + help_text="Scoreboard visibility through the duration of the contest", + max_length=1, + verbose_name="scoreboard visibility", + ), ), migrations.AddField( - model_name='contest', - name='testers', - field=models.ManyToManyField(blank=True, help_text='These users will be able to view the contest, but not edit it.', related_name='_contest_testers_+', to='judge.Profile'), + model_name="contest", + name="testers", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to view the contest, but not edit it.", + related_name="_contest_testers_+", + to="judge.Profile", + ), ), migrations.AddField( - model_name='contestparticipation', - name='tiebreaker', - field=models.FloatField(default=0.0, verbose_name='tie-breaking field'), + model_name="contestparticipation", + name="tiebreaker", + field=models.FloatField(default=0.0, verbose_name="tie-breaking field"), ), ] diff --git a/judge/migrations/0116_auto_20211011_0645.py b/judge/migrations/0116_auto_20211011_0645.py index c067a59..79eaa8d 100644 --- a/judge/migrations/0116_auto_20211011_0645.py +++ b/judge/migrations/0116_auto_20211011_0645.py @@ -6,13 +6,25 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0115_auto_20210525_0222'), + ("judge", "0115_auto_20210525_0222"), ] operations = [ migrations.AlterField( - model_name='contest', - name='format_name', - field=models.CharField(choices=[('atcoder', 'AtCoder'), ('default', 'Default'), ('ecoo', 'ECOO'), ('icpc', 'ICPC'), ('ioi', 'IOI')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format'), + model_name="contest", + name="format_name", + field=models.CharField( + choices=[ + ("atcoder", "AtCoder"), + ("default", "Default"), + ("ecoo", "ECOO"), + ("icpc", "ICPC"), + ("ioi", "IOI"), + ], + default="default", + help_text="The contest format module to use.", + max_length=32, + verbose_name="contest format", + ), ), ] diff --git a/judge/migrations/0117_auto_20211209_0612.py b/judge/migrations/0117_auto_20211209_0612.py index dd3879a..88c170f 100644 --- a/judge/migrations/0117_auto_20211209_0612.py +++ b/judge/migrations/0117_auto_20211209_0612.py @@ -6,13 +6,671 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0116_auto_20211011_0645'), + ("judge", "0116_auto_20211011_0645"), ] operations = [ migrations.AlterField( - model_name='profile', - name='timezone', - field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Nuuk', 'Nuuk'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kanton', 'Kanton'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='Asia/Ho_Chi_Minh', max_length=50, verbose_name='location'), + model_name="profile", + name="timezone", + field=models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ("America/Argentina/San_Juan", "Argentina/San_Juan"), + ("America/Argentina/San_Luis", "Argentina/San_Luis"), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ("America/Indiana/Indianapolis", "Indiana/Indianapolis"), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ("America/Indiana/Petersburg", "Indiana/Petersburg"), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ("America/Kentucky/Louisville", "Kentucky/Louisville"), + ("America/Kentucky/Monticello", "Kentucky/Monticello"), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ("America/North_Dakota/Beulah", "North_Dakota/Beulah"), + ("America/North_Dakota/Center", "North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Nuuk", "Nuuk"), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kanton", "Kanton"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="Asia/Ho_Chi_Minh", + max_length=50, + verbose_name="location", + ), ), ] diff --git a/judge/migrations/0118_rating.py b/judge/migrations/0118_rating.py index 8ad6ad2..fb62906 100644 --- a/judge/migrations/0118_rating.py +++ b/judge/migrations/0118_rating.py @@ -7,7 +7,7 @@ from django.db.models.functions import Coalesce from django.utils import timezone -def tie_ranker(iterable, key=attrgetter('points')): +def tie_ranker(iterable, key=attrgetter("points")): rank = 0 delta = 1 last = None @@ -53,7 +53,9 @@ def WP(RA, RB, VA, VB): return (math.erf((RB - RA) / math.sqrt(2 * (VA * VA + VB * VB))) + 1) / 2.0 -def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is_disqualified): +def recalculate_ratings( + old_rating, old_volatility, actual_rank, times_rated, is_disqualified +): # actual_rank: 1 is first place, N is last place # if there are ties, use the average of places (if places 2, 3, 4, 5 tie, use 3.5 for all of them) @@ -74,7 +76,9 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is for i in range(N): ERank = 0.5 for j in range(N): - ERank += WP(old_rating[i], old_rating[j], old_volatility[i], old_volatility[j]) + ERank += WP( + old_rating[i], old_rating[j], old_volatility[i], old_volatility[j] + ) EPerf = -normal_CDF_inverse((ERank - 0.5) / N) APerf = -normal_CDF_inverse((actual_rank[i] - 0.5) / N) @@ -98,8 +102,10 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is if times_rated[i] == 0: new_volatility[i] = 385 else: - new_volatility[i] = math.sqrt(((new_rating[i] - old_rating[i]) ** 2) / Weight + - (old_volatility[i] ** 2) / (Weight + 1)) + new_volatility[i] = math.sqrt( + ((new_rating[i] - old_rating[i]) ** 2) / Weight + + (old_volatility[i] ** 2) / (Weight + 1) + ) if is_disqualified[i]: # DQed users can manipulate TopCoder ratings to get higher volatility in order to increase their rating @@ -112,23 +118,49 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is # inflate a little if we have to so people who placed first don't lose rating best_rank = min(actual_rank) for i in range(N): - if abs(actual_rank[i] - best_rank) <= 1e-3 and new_rating[i] < old_rating[i] + 1: + if ( + abs(actual_rank[i] - best_rank) <= 1e-3 + and new_rating[i] < old_rating[i] + 1 + ): new_rating[i] = old_rating[i] + 1 - return list(map(int, map(round, new_rating))), list(map(int, map(round, new_volatility))) + return list(map(int, map(round, new_rating))), list( + map(int, map(round, new_volatility)) + ) def tc_rate_contest(contest, Rating, Profile): - rating_subquery = Rating.objects.filter(user=OuterRef('user')) - rating_sorted = rating_subquery.order_by('-contest__end_time') - users = contest.users.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker') \ - .annotate(submissions=Count('submission'), - last_rating=Coalesce(Subquery(rating_sorted.values('rating')[:1]), 1200), - volatility=Coalesce(Subquery(rating_sorted.values('volatility')[:1]), 535), - times=Coalesce(Subquery(rating_subquery.order_by().values('user_id') - .annotate(count=Count('id')).values('count')), 0)) \ - .exclude(user_id__in=contest.rate_exclude.all()) \ - .filter(virtual=0).values('id', 'user_id', 'score', 'cumtime', 'tiebreaker', 'is_disqualified', - 'last_rating', 'volatility', 'times') + rating_subquery = Rating.objects.filter(user=OuterRef("user")) + rating_sorted = rating_subquery.order_by("-contest__end_time") + users = ( + contest.users.order_by("is_disqualified", "-score", "cumtime", "tiebreaker") + .annotate( + submissions=Count("submission"), + last_rating=Coalesce(Subquery(rating_sorted.values("rating")[:1]), 1200), + volatility=Coalesce(Subquery(rating_sorted.values("volatility")[:1]), 535), + times=Coalesce( + Subquery( + rating_subquery.order_by() + .values("user_id") + .annotate(count=Count("id")) + .values("count") + ), + 0, + ), + ) + .exclude(user_id__in=contest.rate_exclude.all()) + .filter(virtual=0) + .values( + "id", + "user_id", + "score", + "cumtime", + "tiebreaker", + "is_disqualified", + "last_rating", + "volatility", + "times", + ) + ) if not contest.rate_all: users = users.filter(submissions__gt=0) if contest.rating_floor is not None: @@ -137,46 +169,68 @@ def tc_rate_contest(contest, Rating, Profile): users = users.exclude(last_rating__gt=contest.rating_ceiling) users = list(users) - participation_ids = list(map(itemgetter('id'), users)) - user_ids = list(map(itemgetter('user_id'), users)) - is_disqualified = list(map(itemgetter('is_disqualified'), users)) - ranking = list(tie_ranker(users, key=itemgetter('score', 'cumtime', 'tiebreaker'))) - old_rating = list(map(itemgetter('last_rating'), users)) - old_volatility = list(map(itemgetter('volatility'), users)) - times_ranked = list(map(itemgetter('times'), users)) - rating, volatility = recalculate_ratings(old_rating, old_volatility, ranking, times_ranked, is_disqualified) + participation_ids = list(map(itemgetter("id"), users)) + user_ids = list(map(itemgetter("user_id"), users)) + is_disqualified = list(map(itemgetter("is_disqualified"), users)) + ranking = list(tie_ranker(users, key=itemgetter("score", "cumtime", "tiebreaker"))) + old_rating = list(map(itemgetter("last_rating"), users)) + old_volatility = list(map(itemgetter("volatility"), users)) + times_ranked = list(map(itemgetter("times"), users)) + rating, volatility = recalculate_ratings( + old_rating, old_volatility, ranking, times_ranked, is_disqualified + ) now = timezone.now() - ratings = [Rating(user_id=i, contest=contest, rating=r, volatility=v, last_rated=now, participation_id=p, rank=z) - for i, p, r, v, z in zip(user_ids, participation_ids, rating, volatility, ranking)] + ratings = [ + Rating( + user_id=i, + contest=contest, + rating=r, + volatility=v, + last_rated=now, + participation_id=p, + rank=z, + ) + for i, p, r, v, z in zip( + user_ids, participation_ids, rating, volatility, ranking + ) + ] Rating.objects.bulk_create(ratings) - Profile.objects.filter(contest_history__contest=contest, contest_history__virtual=0).update( - rating=Subquery(Rating.objects.filter(user=OuterRef('id')) - .order_by('-contest__end_time').values('rating')[:1])) + Profile.objects.filter( + contest_history__contest=contest, contest_history__virtual=0 + ).update( + rating=Subquery( + Rating.objects.filter(user=OuterRef("id")) + .order_by("-contest__end_time") + .values("rating")[:1] + ) + ) # inspired by rate_all_view def rate_tc(apps, schema_editor): - Contest = apps.get_model('judge', 'Contest') - Rating = apps.get_model('judge', 'Rating') - Profile = apps.get_model('judge', 'Profile') + Contest = apps.get_model("judge", "Contest") + Rating = apps.get_model("judge", "Rating") + Profile = apps.get_model("judge", "Profile") with schema_editor.connection.cursor() as cursor: - cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table) + cursor.execute("TRUNCATE TABLE `%s`" % Rating._meta.db_table) Profile.objects.update(rating=None) - for contest in Contest.objects.filter(is_rated=True, end_time__lte=timezone.now()).order_by('end_time'): + for contest in Contest.objects.filter( + is_rated=True, end_time__lte=timezone.now() + ).order_by("end_time"): tc_rate_contest(contest, Rating, Profile) # inspired by rate_all_view def rate_elo_mmr(apps, schema_editor): - Rating = apps.get_model('judge', 'Rating') - Profile = apps.get_model('judge', 'Profile') + Rating = apps.get_model("judge", "Rating") + Profile = apps.get_model("judge", "Profile") with schema_editor.connection.cursor() as cursor: - cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table) + cursor.execute("TRUNCATE TABLE `%s`" % Rating._meta.db_table) Profile.objects.update(rating=None) # Don't populate Rating @@ -184,25 +238,25 @@ def rate_elo_mmr(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('judge', '0117_auto_20211209_0612'), + ("judge", "0117_auto_20211209_0612"), ] operations = [ migrations.RunPython(migrations.RunPython.noop, rate_tc, atomic=True), migrations.AddField( - model_name='rating', - name='mean', - field=models.FloatField(verbose_name='raw rating'), + model_name="rating", + name="mean", + field=models.FloatField(verbose_name="raw rating"), ), migrations.AddField( - model_name='rating', - name='performance', - field=models.FloatField(verbose_name='contest performance'), + model_name="rating", + name="performance", + field=models.FloatField(verbose_name="contest performance"), ), migrations.RemoveField( - model_name='rating', - name='volatility', - field=models.IntegerField(verbose_name='volatility'), + model_name="rating", + name="volatility", + field=models.IntegerField(verbose_name="volatility"), ), migrations.RunPython(rate_elo_mmr, migrations.RunPython.noop, atomic=True), - ] \ No newline at end of file + ] diff --git a/judge/migrations/0119_auto_20220306_0512.py b/judge/migrations/0119_auto_20220306_0512.py index c85f792..abcf1fb 100644 --- a/judge/migrations/0119_auto_20220306_0512.py +++ b/judge/migrations/0119_auto_20220306_0512.py @@ -6,13 +6,17 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0118_rating'), + ("judge", "0118_rating"), ] operations = [ migrations.AlterField( - model_name='contest', - name='hide_problem_tags', - field=models.BooleanField(default=True, help_text='Whether problem tags should be hidden by default.', verbose_name='hide problem tags'), + model_name="contest", + name="hide_problem_tags", + field=models.BooleanField( + default=True, + help_text="Whether problem tags should be hidden by default.", + verbose_name="hide problem tags", + ), ), ] diff --git a/judge/migrations/0120_auto_20220306_1124.py b/judge/migrations/0120_auto_20220306_1124.py index 849461c..5e02189 100644 --- a/judge/migrations/0120_auto_20220306_1124.py +++ b/judge/migrations/0120_auto_20220306_1124.py @@ -8,27 +8,68 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0119_auto_20220306_0512'), + ("judge", "0119_auto_20220306_0512"), ] operations = [ migrations.AddField( - model_name='profile', - name='is_banned_problem_voting', - field=models.BooleanField(default=False, help_text="User will not be able to vote on problems' point values.", verbose_name='banned from voting'), + model_name="profile", + name="is_banned_problem_voting", + field=models.BooleanField( + default=False, + help_text="User will not be able to vote on problems' point values.", + verbose_name="banned from voting", + ), ), migrations.CreateModel( - name='ProblemPointsVote', + name="ProblemPointsVote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('points', models.IntegerField(help_text='The amount of points you think this problem deserves.', validators=[django.core.validators.MinValueValidator(100), django.core.validators.MaxValueValidator(600)], verbose_name='proposed point value')), - ('vote_time', models.DateTimeField(auto_now_add=True, verbose_name='The time this vote was cast')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problem_points_votes', to='judge.Problem')), - ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problem_points_votes', to='judge.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "points", + models.IntegerField( + help_text="The amount of points you think this problem deserves.", + validators=[ + django.core.validators.MinValueValidator(100), + django.core.validators.MaxValueValidator(600), + ], + verbose_name="proposed point value", + ), + ), + ( + "vote_time", + models.DateTimeField( + auto_now_add=True, verbose_name="The time this vote was cast" + ), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="problem_points_votes", + to="judge.Problem", + ), + ), + ( + "voter", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="problem_points_votes", + to="judge.Profile", + ), + ), ], options={ - 'verbose_name': 'vote', - 'verbose_name_plural': 'votes', + "verbose_name": "vote", + "verbose_name_plural": "votes", }, ), ] diff --git a/judge/migrations/0121_auto_20220415_0135.py b/judge/migrations/0121_auto_20220415_0135.py index bc70618..77d3f6a 100644 --- a/judge/migrations/0121_auto_20220415_0135.py +++ b/judge/migrations/0121_auto_20220415_0135.py @@ -9,23 +9,68 @@ import judge.utils.problem_data class Migration(migrations.Migration): dependencies = [ - ('judge', '0120_auto_20220306_1124'), + ("judge", "0120_auto_20220306_1124"), ] operations = [ migrations.AddField( - model_name='problemdata', - name='interactive_judge', - field=models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['cpp'])], verbose_name='interactive judge'), + model_name="problemdata", + name="interactive_judge", + field=models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + validators=[ + django.core.validators.FileExtensionValidator( + allowed_extensions=["cpp"] + ) + ], + verbose_name="interactive judge", + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)'), ('interact', 'Interactive')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ("interact", "Interactive"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)'), ('interact', 'Interactive')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ("interact", "Interactive"), + ], + max_length=10, + verbose_name="checker", + ), ), ] diff --git a/judge/migrations/0122_auto_20220425_1202.py b/judge/migrations/0122_auto_20220425_1202.py index ee48db3..427cd98 100644 --- a/judge/migrations/0122_auto_20220425_1202.py +++ b/judge/migrations/0122_auto_20220425_1202.py @@ -6,13 +6,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0121_auto_20220415_0135'), + ("judge", "0121_auto_20220415_0135"), ] operations = [ migrations.AlterField( - model_name='contest', - name='time_limit', - field=models.DurationField(blank=True, help_text='Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00', null=True, verbose_name='time limit'), + model_name="contest", + name="time_limit", + field=models.DurationField( + blank=True, + help_text="Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00", + null=True, + verbose_name="time limit", + ), ), ] diff --git a/judge/migrations/0123_auto_20220502_2356.py b/judge/migrations/0123_auto_20220502_2356.py index f681a51..54ed0c7 100644 --- a/judge/migrations/0123_auto_20220502_2356.py +++ b/judge/migrations/0123_auto_20220502_2356.py @@ -7,30 +7,85 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0122_auto_20220425_1202'), + ("judge", "0122_auto_20220425_1202"), ] operations = [ migrations.AlterModelOptions( - name='problem', - options={'permissions': (('see_private_problem', 'See hidden problems'), ('edit_own_problem', 'Edit own problems'), ('edit_all_problem', 'Edit all problems'), ('edit_public_problem', 'Edit all public problems'), ('clone_problem', 'Clone problem'), ('change_public_visibility', 'Change is_public field'), ('change_manually_managed', 'Change is_manually_managed field'), ('see_organization_problem', 'See organization-private problems'), ('suggest_problem_changes', 'Suggest changes to problem')), 'verbose_name': 'problem', 'verbose_name_plural': 'problems'}, + name="problem", + options={ + "permissions": ( + ("see_private_problem", "See hidden problems"), + ("edit_own_problem", "Edit own problems"), + ("edit_all_problem", "Edit all problems"), + ("edit_public_problem", "Edit all public problems"), + ("clone_problem", "Clone problem"), + ("change_public_visibility", "Change is_public field"), + ("change_manually_managed", "Change is_manually_managed field"), + ("see_organization_problem", "See organization-private problems"), + ("suggest_problem_changes", "Suggest changes to problem"), + ), + "verbose_name": "problem", + "verbose_name_plural": "problems", + }, ), migrations.CreateModel( - name='VolunteerProblemVote', + name="VolunteerProblemVote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True)), - ('knowledge_points', models.PositiveIntegerField(help_text='Points awarded by knowledge difficulty', verbose_name='knowledge points')), - ('thinking_points', models.PositiveIntegerField(help_text='Points awarded by thinking difficulty', verbose_name='thinking points')), - ('feedback', models.TextField(blank=True, verbose_name='feedback')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_user_votes', to='judge.Problem')), - ('types', models.ManyToManyField(help_text="The type of problem, as shown on the problem's page.", to='judge.ProblemType', verbose_name='problem types')), - ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_problem_votes', to='judge.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("time", models.DateTimeField(auto_now_add=True)), + ( + "knowledge_points", + models.PositiveIntegerField( + help_text="Points awarded by knowledge difficulty", + verbose_name="knowledge points", + ), + ), + ( + "thinking_points", + models.PositiveIntegerField( + help_text="Points awarded by thinking difficulty", + verbose_name="thinking points", + ), + ), + ("feedback", models.TextField(blank=True, verbose_name="feedback")), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="volunteer_user_votes", + to="judge.Problem", + ), + ), + ( + "types", + models.ManyToManyField( + help_text="The type of problem, as shown on the problem's page.", + to="judge.ProblemType", + verbose_name="problem types", + ), + ), + ( + "voter", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="volunteer_problem_votes", + to="judge.Profile", + ), + ), ], options={ - 'verbose_name': 'volunteer vote', - 'verbose_name_plural': 'volunteer votes', - 'unique_together': {('voter', 'problem')}, + "verbose_name": "volunteer vote", + "verbose_name_plural": "volunteer votes", + "unique_together": {("voter", "problem")}, }, ), ] diff --git a/judge/ml/collab_filter.py b/judge/ml/collab_filter.py index 2a8171f..3719377 100644 --- a/judge/ml/collab_filter.py +++ b/judge/ml/collab_filter.py @@ -5,14 +5,16 @@ from dmoj.decorators import timeit class CollabFilter: - DOT = 'dot' - COSINE = 'cosine' + DOT = "dot" + COSINE = "cosine" # name = 'collab_filter' or 'collab_filter_time' @timeit def __init__(self, name, **kwargs): - embeddings = np.load(os.path.join(settings.ML_OUTPUT_PATH, name + '/embeddings.npz'), - allow_pickle=True) + embeddings = np.load( + os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"), + allow_pickle=True, + ) arr0, arr1 = embeddings.files self.user_embeddings = embeddings[arr0] self.problem_embeddings = embeddings[arr1] @@ -42,9 +44,10 @@ class CollabFilter: if uid >= len(self.user_embeddings): uid = 0 scores = self.compute_scores( - self.user_embeddings[uid], self.problem_embeddings, measure) - - res = [] # [(score, problem)] + self.user_embeddings[uid], self.problem_embeddings, measure + ) + + res = [] # [(score, problem)] for problem in problems: pid = problem.id if pid < len(scores): @@ -53,17 +56,17 @@ class CollabFilter: res.sort(reverse=True, key=lambda x: x[0]) return res[:limit] - # return a list of pid def problems_neighbors(self, problem, problemset, measure=DOT, limit=None): pid = problem.id if pid >= len(self.problem_embeddings): return None scores = self.compute_scores( - self.problem_embeddings[pid], self.problem_embeddings, measure) + self.problem_embeddings[pid], self.problem_embeddings, measure + ) res = [] for p in problemset: if p.id < len(scores): res.append((scores[p.id], p)) res.sort(reverse=True, key=lambda x: x[0]) - return res[:limit] \ No newline at end of file + return res[:limit] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index 88367c3..8eaf102 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -1,32 +1,67 @@ from reversion import revisions -from judge.models.choices import ACE_THEMES, EFFECTIVE_MATH_ENGINES, MATH_ENGINES_CHOICES, TIMEZONE +from judge.models.choices import ( + ACE_THEMES, + EFFECTIVE_MATH_ENGINES, + MATH_ENGINES_CHOICES, + TIMEZONE, +) from judge.models.comment import Comment, CommentLock, CommentVote, Notification -from judge.models.contest import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestSubmission, \ - ContestTag, Rating +from judge.models.contest import ( + Contest, + ContestMoss, + ContestParticipation, + ContestProblem, + ContestSubmission, + ContestTag, + Rating, +) from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex from judge.models.message import PrivateMessage, PrivateMessageThread -from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \ - ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet, ProblemPointsVote -from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \ - problem_directory_file +from judge.models.problem import ( + LanguageLimit, + License, + Problem, + ProblemClarification, + ProblemGroup, + ProblemTranslation, + ProblemType, + Solution, + TranslatedProblemForeignKeyQuerySet, + TranslatedProblemQuerySet, + ProblemPointsVote, +) +from judge.models.problem_data import ( + CHECKERS, + ProblemData, + ProblemTestCase, + problem_data_storage, + problem_directory_file, +) from judge.models.profile import Organization, OrganizationRequest, Profile, Friend from judge.models.runtime import Judge, Language, RuntimeVersion -from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase +from judge.models.submission import ( + SUBMISSION_RESULT, + Submission, + SubmissionSource, + SubmissionTestCase, +) from judge.models.ticket import Ticket, TicketMessage from judge.models.volunteer import VolunteerProblemVote -revisions.register(Profile, exclude=['points', 'last_access', 'ip', 'rating']) -revisions.register(Problem, follow=['language_limits']) +revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) +revisions.register(Problem, follow=["language_limits"]) revisions.register(LanguageLimit) -revisions.register(Contest, follow=['contest_problems']) +revisions.register(Contest, follow=["contest_problems"]) revisions.register(ContestProblem) revisions.register(Organization) revisions.register(BlogPost) revisions.register(Solution) -revisions.register(Judge, fields=['name', 'created', 'auth_key', 'description']) +revisions.register(Judge, fields=["name", "created", "auth_key", "description"]) revisions.register(Language) -revisions.register(Comment, fields=['author', 'time', 'page', 'score', 'body', 'hidden', 'parent']) +revisions.register( + Comment, fields=["author", "time", "page", "score", "body", "hidden", "parent"] +) revisions.register(ProblemTranslation) revisions.register(ProblemPointsVote) revisions.register(ContestMoss) diff --git a/judge/models/choices.py b/judge/models/choices.py index 16c57de..b6fab5c 100644 --- a/judge/models/choices.py +++ b/judge/models/choices.py @@ -8,11 +8,11 @@ from django.utils.translation import gettext_lazy as _ def make_timezones(): data = defaultdict(list) for tz in pytz.all_timezones: - if '/' in tz: - area, loc = tz.split('/', 1) + if "/" in tz: + area, loc = tz.split("/", 1) else: - area, loc = 'Other', tz - if not loc.startswith('GMT'): + area, loc = "Other", tz + if not loc.startswith("GMT"): data[area].append((tz, loc)) return sorted(data.items(), key=itemgetter(0)) @@ -21,46 +21,46 @@ TIMEZONE = make_timezones() del make_timezones ACE_THEMES = ( - ('ambiance', 'Ambiance'), - ('chaos', 'Chaos'), - ('chrome', 'Chrome'), - ('clouds', 'Clouds'), - ('clouds_midnight', 'Clouds Midnight'), - ('cobalt', 'Cobalt'), - ('crimson_editor', 'Crimson Editor'), - ('dawn', 'Dawn'), - ('dreamweaver', 'Dreamweaver'), - ('eclipse', 'Eclipse'), - ('github', 'Github'), - ('idle_fingers', 'Idle Fingers'), - ('katzenmilch', 'Katzenmilch'), - ('kr_theme', 'KR Theme'), - ('kuroir', 'Kuroir'), - ('merbivore', 'Merbivore'), - ('merbivore_soft', 'Merbivore Soft'), - ('mono_industrial', 'Mono Industrial'), - ('monokai', 'Monokai'), - ('pastel_on_dark', 'Pastel on Dark'), - ('solarized_dark', 'Solarized Dark'), - ('solarized_light', 'Solarized Light'), - ('terminal', 'Terminal'), - ('textmate', 'Textmate'), - ('tomorrow', 'Tomorrow'), - ('tomorrow_night', 'Tomorrow Night'), - ('tomorrow_night_blue', 'Tomorrow Night Blue'), - ('tomorrow_night_bright', 'Tomorrow Night Bright'), - ('tomorrow_night_eighties', 'Tomorrow Night Eighties'), - ('twilight', 'Twilight'), - ('vibrant_ink', 'Vibrant Ink'), - ('xcode', 'XCode'), + ("ambiance", "Ambiance"), + ("chaos", "Chaos"), + ("chrome", "Chrome"), + ("clouds", "Clouds"), + ("clouds_midnight", "Clouds Midnight"), + ("cobalt", "Cobalt"), + ("crimson_editor", "Crimson Editor"), + ("dawn", "Dawn"), + ("dreamweaver", "Dreamweaver"), + ("eclipse", "Eclipse"), + ("github", "Github"), + ("idle_fingers", "Idle Fingers"), + ("katzenmilch", "Katzenmilch"), + ("kr_theme", "KR Theme"), + ("kuroir", "Kuroir"), + ("merbivore", "Merbivore"), + ("merbivore_soft", "Merbivore Soft"), + ("mono_industrial", "Mono Industrial"), + ("monokai", "Monokai"), + ("pastel_on_dark", "Pastel on Dark"), + ("solarized_dark", "Solarized Dark"), + ("solarized_light", "Solarized Light"), + ("terminal", "Terminal"), + ("textmate", "Textmate"), + ("tomorrow", "Tomorrow"), + ("tomorrow_night", "Tomorrow Night"), + ("tomorrow_night_blue", "Tomorrow Night Blue"), + ("tomorrow_night_bright", "Tomorrow Night Bright"), + ("tomorrow_night_eighties", "Tomorrow Night Eighties"), + ("twilight", "Twilight"), + ("vibrant_ink", "Vibrant Ink"), + ("xcode", "XCode"), ) MATH_ENGINES_CHOICES = ( - ('tex', _('Leave as LaTeX')), - ('svg', _('SVG with PNG fallback')), - ('mml', _('MathML only')), - ('jax', _('MathJax with SVG/PNG fallback')), - ('auto', _('Detect best quality')), + ("tex", _("Leave as LaTeX")), + ("svg", _("SVG with PNG fallback")), + ("mml", _("MathML only")), + ("jax", _("MathJax with SVG/PNG fallback")), + ("auto", _("Detect best quality")), ) -EFFECTIVE_MATH_ENGINES = ('svg', 'mml', 'tex', 'jax') +EFFECTIVE_MATH_ENGINES = ("svg", "mml", "tex", "jax") diff --git a/judge/models/comment.py b/judge/models/comment.py index 5d26500..a4ffc57 100644 --- a/judge/models/comment.py +++ b/judge/models/comment.py @@ -20,77 +20,98 @@ from judge.models.profile import Profile from judge.utils.cachedict import CacheDict -__all__ = ['Comment', 'CommentLock', 'CommentVote', 'Notification'] +__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"] -comment_validator = RegexValidator(r'^[pcs]:[a-z0-9]+$|^b:\d+$', - _(r'Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$')) +comment_validator = RegexValidator( + r"^[pcs]:[a-z0-9]+$|^b:\d+$", _(r"Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$") +) class VersionRelation(GenericRelation): def __init__(self): - super(VersionRelation, self).__init__(Version, object_id_field='object_id') + super(VersionRelation, self).__init__(Version, object_id_field="object_id") def get_extra_restriction(self, where_class, alias, remote_alias): - cond = super(VersionRelation, self).get_extra_restriction(where_class, alias, remote_alias) - field = self.remote_field.model._meta.get_field('db') - lookup = field.get_lookup('exact')(field.get_col(remote_alias), 'default') - cond.add(lookup, 'AND') + cond = super(VersionRelation, self).get_extra_restriction( + where_class, alias, remote_alias + ) + field = self.remote_field.model._meta.get_field("db") + lookup = field.get_lookup("exact")(field.get_col(remote_alias), "default") + cond.add(lookup, "AND") return cond class Comment(MPTTModel): - author = models.ForeignKey(Profile, verbose_name=_('commenter'), on_delete=CASCADE) - time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True) - page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True, - validators=[comment_validator]) - score = models.IntegerField(verbose_name=_('votes'), default=0) - body = models.TextField(verbose_name=_('body of comment'), max_length=8192) - hidden = models.BooleanField(verbose_name=_('hide the comment'), default=0) - parent = TreeForeignKey('self', verbose_name=_('parent'), null=True, blank=True, related_name='replies', - on_delete=CASCADE) + author = models.ForeignKey(Profile, verbose_name=_("commenter"), on_delete=CASCADE) + time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) + page = models.CharField( + max_length=30, + verbose_name=_("associated page"), + db_index=True, + validators=[comment_validator], + ) + score = models.IntegerField(verbose_name=_("votes"), default=0) + body = models.TextField(verbose_name=_("body of comment"), max_length=8192) + hidden = models.BooleanField(verbose_name=_("hide the comment"), default=0) + parent = TreeForeignKey( + "self", + verbose_name=_("parent"), + null=True, + blank=True, + related_name="replies", + on_delete=CASCADE, + ) versions = VersionRelation() class Meta: - verbose_name = _('comment') - verbose_name_plural = _('comments') + verbose_name = _("comment") + verbose_name_plural = _("comments") class MPTTMeta: - order_insertion_by = ['-time'] + order_insertion_by = ["-time"] @classmethod def most_recent(cls, user, n, batch=None): - queryset = cls.objects.filter(hidden=False).select_related('author__user') \ - .defer('author__about', 'body').order_by('-id') + queryset = ( + cls.objects.filter(hidden=False) + .select_related("author__user") + .defer("author__about", "body") + .order_by("-id") + ) - problem_access = CacheDict(lambda code: Problem.objects.get(code=code).is_accessible_by(user)) - contest_access = CacheDict(lambda key: Contest.objects.get(key=key).is_accessible_by(user)) + problem_access = CacheDict( + lambda code: Problem.objects.get(code=code).is_accessible_by(user) + ) + contest_access = CacheDict( + lambda key: Contest.objects.get(key=key).is_accessible_by(user) + ) blog_access = CacheDict(lambda id: BlogPost.objects.get(id=id).can_see(user)) - + if n == -1: - n = len(queryset) + n = len(queryset) if user.is_superuser: return queryset[:n] if batch is None: batch = 2 * n output = [] for i in itertools.count(0): - slice = queryset[i * batch:i * batch + batch] + slice = queryset[i * batch : i * batch + batch] if not slice: break for comment in slice: - if comment.page.startswith('p:') or comment.page.startswith('s:'): + if comment.page.startswith("p:") or comment.page.startswith("s:"): try: if problem_access[comment.page[2:]]: output.append(comment) except Problem.DoesNotExist: pass - elif comment.page.startswith('c:'): + elif comment.page.startswith("c:"): try: if contest_access[comment.page[2:]]: output.append(comment) except Contest.DoesNotExist: pass - elif comment.page.startswith('b:'): + elif comment.page.startswith("b:"): try: if blog_access[comment.page[2:]]: output.append(comment) @@ -106,50 +127,55 @@ class Comment(MPTTModel): def link(self): try: link = None - if self.page.startswith('p:'): - link = reverse('problem_detail', args=(self.page[2:],)) - elif self.page.startswith('c:'): - link = reverse('contest_view', args=(self.page[2:],)) - elif self.page.startswith('b:'): - key = 'blog_slug:%s' % self.page[2:] + if self.page.startswith("p:"): + link = reverse("problem_detail", args=(self.page[2:],)) + elif self.page.startswith("c:"): + link = reverse("contest_view", args=(self.page[2:],)) + elif self.page.startswith("b:"): + key = "blog_slug:%s" % self.page[2:] slug = cache.get(key) if slug is None: try: slug = BlogPost.objects.get(id=self.page[2:]).slug except ObjectDoesNotExist: - slug = '' + slug = "" cache.set(key, slug, 3600) - link = reverse('blog_post', args=(self.page[2:], slug)) - elif self.page.startswith('s:'): - link = reverse('problem_editorial', args=(self.page[2:],)) + link = reverse("blog_post", args=(self.page[2:], slug)) + elif self.page.startswith("s:"): + link = reverse("problem_editorial", args=(self.page[2:],)) except Exception: - link = 'invalid' + link = "invalid" return link @classmethod def get_page_title(cls, page): try: - if page.startswith('p:'): - return Problem.objects.values_list('name', flat=True).get(code=page[2:]) - elif page.startswith('c:'): - return Contest.objects.values_list('name', flat=True).get(key=page[2:]) - elif page.startswith('b:'): - return BlogPost.objects.values_list('title', flat=True).get(id=page[2:]) - elif page.startswith('s:'): - return _('Editorial for %s') % Problem.objects.values_list('name', flat=True).get(code=page[2:]) - return '' + if page.startswith("p:"): + return Problem.objects.values_list("name", flat=True).get(code=page[2:]) + elif page.startswith("c:"): + return Contest.objects.values_list("name", flat=True).get(key=page[2:]) + elif page.startswith("b:"): + return BlogPost.objects.values_list("title", flat=True).get(id=page[2:]) + elif page.startswith("s:"): + return _("Editorial for %s") % Problem.objects.values_list( + "name", flat=True + ).get(code=page[2:]) + return "" except ObjectDoesNotExist: - return '' + return "" @cached_property def page_title(self): return self.get_page_title(self.page) def get_absolute_url(self): - return '%s#comment-%d' % (self.link, self.id) + return "%s#comment-%d" % (self.link, self.id) def __str__(self): - return '%(page)s by %(user)s' % {'page': self.page, 'user': self.author.user.username} + return "%(page)s by %(user)s" % { + "page": self.page, + "user": self.author.user.username, + } # Only use this when queried with # .prefetch_related(Prefetch('votes', queryset=CommentVote.objects.filter(voter_id=profile_id))) @@ -165,34 +191,52 @@ class Comment(MPTTModel): class CommentVote(models.Model): - voter = models.ForeignKey(Profile, related_name='voted_comments', on_delete=CASCADE) - comment = models.ForeignKey(Comment, related_name='votes', on_delete=CASCADE) + voter = models.ForeignKey(Profile, related_name="voted_comments", on_delete=CASCADE) + comment = models.ForeignKey(Comment, related_name="votes", on_delete=CASCADE) score = models.IntegerField() class Meta: - unique_together = ['voter', 'comment'] - verbose_name = _('comment vote') - verbose_name_plural = _('comment votes') + unique_together = ["voter", "comment"] + verbose_name = _("comment vote") + verbose_name_plural = _("comment votes") class CommentLock(models.Model): - page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True, - validators=[comment_validator]) + page = models.CharField( + max_length=30, + verbose_name=_("associated page"), + db_index=True, + validators=[comment_validator], + ) class Meta: - permissions = ( - ('override_comment_lock', _('Override comment lock')), - ) + permissions = (("override_comment_lock", _("Override comment lock")),) def __str__(self): return str(self.page) class Notification(models.Model): - owner = models.ForeignKey(Profile, verbose_name=_('owner'), related_name="notifications", on_delete=CASCADE) - time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True) - comment = models.ForeignKey(Comment, null=True, verbose_name=_('comment'), on_delete=CASCADE) - read = models.BooleanField(verbose_name=_('read'), default=False) - category = models.CharField(verbose_name=_('category'), max_length=10) - html_link = models.TextField(default='', verbose_name=_('html link to comments, used for non-comments'), max_length=1000) - author = models.ForeignKey(Profile, null=True, verbose_name=_('who trigger, used for non-comment'), on_delete=CASCADE) \ No newline at end of file + owner = models.ForeignKey( + Profile, + verbose_name=_("owner"), + related_name="notifications", + on_delete=CASCADE, + ) + time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) + comment = models.ForeignKey( + Comment, null=True, verbose_name=_("comment"), on_delete=CASCADE + ) + read = models.BooleanField(verbose_name=_("read"), default=False) + category = models.CharField(verbose_name=_("category"), max_length=10) + html_link = models.TextField( + default="", + verbose_name=_("html link to comments, used for non-comments"), + max_length=1000, + ) + author = models.ForeignKey( + Profile, + null=True, + verbose_name=_("who trigger, used for non-comment"), + on_delete=CASCADE, + ) diff --git a/judge/models/contest.py b/judge/models/contest.py index 74c32ce..ef92815 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -8,7 +8,13 @@ from django.utils.functional import cached_property from django.utils.translation import gettext, gettext_lazy as _ from jsonfield import JSONField from lupa import LuaRuntime -from moss import MOSS_LANG_C, MOSS_LANG_CC, MOSS_LANG_JAVA, MOSS_LANG_PYTHON, MOSS_LANG_PASCAL +from moss import ( + MOSS_LANG_C, + MOSS_LANG_CC, + MOSS_LANG_JAVA, + MOSS_LANG_PYTHON, + MOSS_LANG_PASCAL, +) from judge import contest_format from judge.models.problem import Problem @@ -16,22 +22,39 @@ from judge.models.profile import Organization, Profile from judge.models.submission import Submission from judge.ratings import rate_contest -__all__ = ['Contest', 'ContestTag', 'ContestParticipation', 'ContestProblem', 'ContestSubmission', 'Rating'] +__all__ = [ + "Contest", + "ContestTag", + "ContestParticipation", + "ContestProblem", + "ContestSubmission", + "Rating", +] class ContestTag(models.Model): - color_validator = RegexValidator('^#(?:[A-Fa-f0-9]{3}){1,2}$', _('Invalid colour.')) + color_validator = RegexValidator("^#(?:[A-Fa-f0-9]{3}){1,2}$", _("Invalid colour.")) - name = models.CharField(max_length=20, verbose_name=_('tag name'), unique=True, - validators=[RegexValidator(r'^[a-z-]+$', message=_('Lowercase letters and hyphens only.'))]) - color = models.CharField(max_length=7, verbose_name=_('tag colour'), validators=[color_validator]) - description = models.TextField(verbose_name=_('tag description'), blank=True) + name = models.CharField( + max_length=20, + verbose_name=_("tag name"), + unique=True, + validators=[ + RegexValidator( + r"^[a-z-]+$", message=_("Lowercase letters and hyphens only.") + ) + ], + ) + color = models.CharField( + max_length=7, verbose_name=_("tag colour"), validators=[color_validator] + ) + description = models.TextField(verbose_name=_("tag description"), blank=True) def __str__(self): return self.name def get_absolute_url(self): - return reverse('contest_tag', args=[self.name]) + return reverse("contest_tag", args=[self.name]) @property def text_color(self, cache={}): @@ -40,105 +63,232 @@ class ContestTag(models.Model): r, g, b = [ord(bytes.fromhex(i * 2)) for i in self.color[1:]] else: r, g, b = [i for i in bytes.fromhex(self.color[1:])] - cache[self.color] = '#000' if 299 * r + 587 * g + 144 * b > 140000 else '#fff' + cache[self.color] = ( + "#000" if 299 * r + 587 * g + 144 * b > 140000 else "#fff" + ) return cache[self.color] class Meta: - verbose_name = _('contest tag') - verbose_name_plural = _('contest tags') + verbose_name = _("contest tag") + verbose_name_plural = _("contest tags") class Contest(models.Model): - SCOREBOARD_VISIBLE = 'V' - SCOREBOARD_AFTER_CONTEST = 'C' - SCOREBOARD_AFTER_PARTICIPATION = 'P' + SCOREBOARD_VISIBLE = "V" + SCOREBOARD_AFTER_CONTEST = "C" + SCOREBOARD_AFTER_PARTICIPATION = "P" SCOREBOARD_VISIBILITY = ( - (SCOREBOARD_VISIBLE, _('Visible')), - (SCOREBOARD_AFTER_CONTEST, _('Hidden for duration of contest')), - (SCOREBOARD_AFTER_PARTICIPATION, _('Hidden for duration of participation')), + (SCOREBOARD_VISIBLE, _("Visible")), + (SCOREBOARD_AFTER_CONTEST, _("Hidden for duration of contest")), + (SCOREBOARD_AFTER_PARTICIPATION, _("Hidden for duration of participation")), ) - key = models.CharField(max_length=20, 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) - authors = models.ManyToManyField(Profile, help_text=_('These users will be able to edit the contest.'), - related_name='authors+') - curators = models.ManyToManyField(Profile, help_text=_('These users will be able to edit the contest, ' - 'but will not be listed as authors.'), - related_name='curators+', blank=True) - testers = models.ManyToManyField(Profile, help_text=_('These users will be able to view the contest, ' - 'but not edit it.'), - blank=True, related_name='testers+') - description = models.TextField(verbose_name=_('description'), blank=True) - problems = models.ManyToManyField(Problem, verbose_name=_('problems'), through='ContestProblem') - start_time = models.DateTimeField(verbose_name=_('start time'), db_index=True) - end_time = models.DateTimeField(verbose_name=_('end time'), db_index=True) - time_limit = models.DurationField(verbose_name=_('time limit'), blank=True, null=True, - help_text=_('Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00')) - is_visible = models.BooleanField(verbose_name=_('publicly visible'), default=False, - help_text=_('Should be set even for organization-private contests, where it ' - 'determines whether the contest is visible to members of the ' - 'specified organizations.')) - is_rated = models.BooleanField(verbose_name=_('contest rated'), help_text=_('Whether this contest can be rated.'), - default=False) - scoreboard_visibility = models.CharField(verbose_name=_('scoreboard visibility'), default=SCOREBOARD_VISIBLE, - max_length=1, help_text=_('Scoreboard visibility through the duration ' - 'of the contest'), choices=SCOREBOARD_VISIBILITY) - view_contest_scoreboard = models.ManyToManyField(Profile, verbose_name=_('view contest scoreboard'), blank=True, - related_name='view_contest_scoreboard', - help_text=_('These users will be able to view the scoreboard.')) - use_clarifications = models.BooleanField(verbose_name=_('no comments'), - help_text=_("Use clarification system instead of comments."), - default=True) - rating_floor = models.IntegerField(verbose_name=('rating floor'), help_text=_('Rating floor for contest'), - null=True, blank=True) - rating_ceiling = models.IntegerField(verbose_name=('rating ceiling'), help_text=_('Rating ceiling for contest'), - null=True, blank=True) - rate_all = models.BooleanField(verbose_name=_('rate all'), help_text=_('Rate all users who joined.'), default=False) - rate_exclude = models.ManyToManyField(Profile, verbose_name=_('exclude from ratings'), blank=True, - related_name='rate_exclude+') - is_private = models.BooleanField(verbose_name=_('private to specific users'), default=False) - private_contestants = models.ManyToManyField(Profile, blank=True, verbose_name=_('private contestants'), - help_text=_('If private, only these users may see the contest'), - related_name='private_contestants+') - hide_problem_tags = models.BooleanField(verbose_name=_('hide problem tags'), - help_text=_('Whether problem tags should be hidden by default.'), - default=True) - run_pretests_only = models.BooleanField(verbose_name=_('run pretests only'), - help_text=_('Whether judges should grade pretests only, versus all ' - 'testcases. Commonly set during a contest, then unset ' - 'prior to rejudging user submissions when the contest ends.'), - default=False) - is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False) - organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'), - help_text=_('If private, only these organizations may see the contest')) - og_image = models.CharField(verbose_name=_('OpenGraph image'), default='', max_length=150, blank=True) - logo_override_image = models.CharField(verbose_name=_('Logo override image'), default='', max_length=150, - blank=True, - help_text=_('This image will replace the default site logo for users ' - 'inside the contest.')) - tags = models.ManyToManyField(ContestTag, verbose_name=_('contest tags'), blank=True, related_name='contests') - user_count = models.IntegerField(verbose_name=_('the amount of live participants'), default=0) - summary = models.TextField(blank=True, verbose_name=_('contest summary'), - help_text=_('Plain-text, shown in meta description tag, e.g. for social media.')) - access_code = models.CharField(verbose_name=_('access code'), blank=True, default='', max_length=255, - help_text=_('An optional code to prompt contestants before they are allowed ' - 'to join the contest. Leave it blank to disable.')) - banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True, - help_text=_('Bans the selected users from joining this contest.')) - format_name = models.CharField(verbose_name=_('contest format'), default='default', max_length=32, - choices=contest_format.choices(), help_text=_('The contest format module to use.')) - format_config = JSONField(verbose_name=_('contest format configuration'), null=True, blank=True, - help_text=_('A JSON object to serve as the configuration for the chosen contest format ' - 'module. Leave empty to use None. Exact format depends on the contest format ' - 'selected.')) - problem_label_script = models.TextField(verbose_name='contest problem label script', blank=True, - help_text='A custom Lua function to generate problem labels. Requires a ' - 'single function with an integer parameter, the zero-indexed ' - 'contest problem index, and returns a string, the label.') - points_precision = models.IntegerField(verbose_name=_('precision points'), default=2, - validators=[MinValueValidator(0), MaxValueValidator(10)], - help_text=_('Number of digits to round points to.')) - + key = models.CharField( + max_length=20, + 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 + ) + authors = models.ManyToManyField( + Profile, + help_text=_("These users will be able to edit the contest."), + related_name="authors+", + ) + curators = models.ManyToManyField( + Profile, + help_text=_( + "These users will be able to edit the contest, " + "but will not be listed as authors." + ), + related_name="curators+", + blank=True, + ) + testers = models.ManyToManyField( + Profile, + help_text=_( + "These users will be able to view the contest, " "but not edit it." + ), + blank=True, + related_name="testers+", + ) + description = models.TextField(verbose_name=_("description"), blank=True) + problems = models.ManyToManyField( + Problem, verbose_name=_("problems"), through="ContestProblem" + ) + start_time = models.DateTimeField(verbose_name=_("start time"), db_index=True) + end_time = models.DateTimeField(verbose_name=_("end time"), db_index=True) + time_limit = models.DurationField( + verbose_name=_("time limit"), + blank=True, + null=True, + help_text=_( + "Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00" + ), + ) + is_visible = models.BooleanField( + verbose_name=_("publicly visible"), + default=False, + help_text=_( + "Should be set even for organization-private contests, where it " + "determines whether the contest is visible to members of the " + "specified organizations." + ), + ) + is_rated = models.BooleanField( + verbose_name=_("contest rated"), + help_text=_("Whether this contest can be rated."), + default=False, + ) + scoreboard_visibility = models.CharField( + verbose_name=_("scoreboard visibility"), + default=SCOREBOARD_VISIBLE, + max_length=1, + help_text=_("Scoreboard visibility through the duration " "of the contest"), + choices=SCOREBOARD_VISIBILITY, + ) + view_contest_scoreboard = models.ManyToManyField( + Profile, + verbose_name=_("view contest scoreboard"), + blank=True, + related_name="view_contest_scoreboard", + help_text=_("These users will be able to view the scoreboard."), + ) + use_clarifications = models.BooleanField( + verbose_name=_("no comments"), + help_text=_("Use clarification system instead of comments."), + default=True, + ) + rating_floor = models.IntegerField( + verbose_name=("rating floor"), + help_text=_("Rating floor for contest"), + null=True, + blank=True, + ) + rating_ceiling = models.IntegerField( + verbose_name=("rating ceiling"), + help_text=_("Rating ceiling for contest"), + null=True, + blank=True, + ) + rate_all = models.BooleanField( + verbose_name=_("rate all"), + help_text=_("Rate all users who joined."), + default=False, + ) + rate_exclude = models.ManyToManyField( + Profile, + verbose_name=_("exclude from ratings"), + blank=True, + related_name="rate_exclude+", + ) + is_private = models.BooleanField( + verbose_name=_("private to specific users"), default=False + ) + private_contestants = models.ManyToManyField( + Profile, + blank=True, + verbose_name=_("private contestants"), + help_text=_("If private, only these users may see the contest"), + related_name="private_contestants+", + ) + hide_problem_tags = models.BooleanField( + verbose_name=_("hide problem tags"), + help_text=_("Whether problem tags should be hidden by default."), + default=True, + ) + run_pretests_only = models.BooleanField( + verbose_name=_("run pretests only"), + help_text=_( + "Whether judges should grade pretests only, versus all " + "testcases. Commonly set during a contest, then unset " + "prior to rejudging user submissions when the contest ends." + ), + default=False, + ) + is_organization_private = models.BooleanField( + verbose_name=_("private to organizations"), default=False + ) + organizations = models.ManyToManyField( + Organization, + blank=True, + verbose_name=_("organizations"), + help_text=_("If private, only these organizations may see the contest"), + ) + og_image = models.CharField( + verbose_name=_("OpenGraph image"), default="", max_length=150, blank=True + ) + logo_override_image = models.CharField( + verbose_name=_("Logo override image"), + default="", + max_length=150, + blank=True, + help_text=_( + "This image will replace the default site logo for users " + "inside the contest." + ), + ) + tags = models.ManyToManyField( + ContestTag, verbose_name=_("contest tags"), blank=True, related_name="contests" + ) + user_count = models.IntegerField( + verbose_name=_("the amount of live participants"), default=0 + ) + summary = models.TextField( + blank=True, + verbose_name=_("contest summary"), + help_text=_( + "Plain-text, shown in meta description tag, e.g. for social media." + ), + ) + access_code = models.CharField( + verbose_name=_("access code"), + blank=True, + default="", + max_length=255, + help_text=_( + "An optional code to prompt contestants before they are allowed " + "to join the contest. Leave it blank to disable." + ), + ) + banned_users = models.ManyToManyField( + Profile, + verbose_name=_("personae non gratae"), + blank=True, + help_text=_("Bans the selected users from joining this contest."), + ) + format_name = models.CharField( + verbose_name=_("contest format"), + default="default", + max_length=32, + choices=contest_format.choices(), + help_text=_("The contest format module to use."), + ) + format_config = JSONField( + verbose_name=_("contest format configuration"), + null=True, + blank=True, + help_text=_( + "A JSON object to serve as the configuration for the chosen contest format " + "module. Leave empty to use None. Exact format depends on the contest format " + "selected." + ), + ) + problem_label_script = models.TextField( + verbose_name="contest problem label script", + blank=True, + help_text="A custom Lua function to generate problem labels. Requires a " + "single function with an integer parameter, the zero-indexed " + "contest problem index, and returns a string, the label.", + ) + points_precision = models.IntegerField( + verbose_name=_("precision points"), + default=2, + validators=[MinValueValidator(0), MaxValueValidator(10)], + help_text=_("Number of digits to round points to."), + ) + @cached_property def format_class(self): return contest_format.formats[self.format_name] @@ -151,13 +301,20 @@ class Contest(models.Model): def get_label_for_problem(self): def DENY_ALL(obj, attr_name, is_setting): raise AttributeError() - lua = LuaRuntime(attribute_filter=DENY_ALL, register_eval=False, register_builtins=False) - return lua.eval(self.problem_label_script or self.format.get_contest_problem_label_script()) + + lua = LuaRuntime( + attribute_filter=DENY_ALL, register_eval=False, register_builtins=False + ) + return lua.eval( + self.problem_label_script or self.format.get_contest_problem_label_script() + ) def clean(self): # Django will complain if you didn't fill in start_time or end_time, so we don't have to. if self.start_time and self.end_time and self.start_time >= self.end_time: - raise ValidationError('What is this? A contest that ended before it starts?') + raise ValidationError( + "What is this? A contest that ended before it starts?" + ) self.format_class.validate(self.format_config) try: @@ -165,15 +322,21 @@ class Contest(models.Model): # so test it to see if the script returns a valid label. label = self.get_label_for_problem(0) except Exception as e: - raise ValidationError('Contest problem label script: %s' % e) + raise ValidationError("Contest problem label script: %s" % e) else: if not isinstance(label, str): - raise ValidationError('Contest problem label script: script should return a string.') + raise ValidationError( + "Contest problem label script: script should return a string." + ) def is_in_contest(self, user): if user.is_authenticated: profile = user.profile - return profile and profile.current_contest is not None and profile.current_contest.contest == self + return ( + profile + and profile.current_contest is not None + and profile.current_contest.contest == self + ) return False def can_see_own_scoreboard(self, user): @@ -190,19 +353,26 @@ class Contest(models.Model): return True if not user.is_authenticated: return False - if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'): + if user.has_perm("judge.see_private_contest") or user.has_perm( + "judge.edit_all_contest" + ): return True if user.profile.id in self.editor_ids: return True if self.view_contest_scoreboard.filter(id=user.profile.id).exists(): return True - if self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION and self.has_completed_contest(user): + if ( + self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION + and self.has_completed_contest(user) + ): return True return False def has_completed_contest(self, user): if user.is_authenticated: - participation = self.users.filter(virtual=ContestParticipation.LIVE, user=user.profile).first() + participation = self.users.filter( + virtual=ContestParticipation.LIVE, user=user.profile + ).first() if participation and participation.ended: return True return False @@ -211,8 +381,11 @@ class Contest(models.Model): def show_scoreboard(self): if not self.can_join: return False - if (self.scoreboard_visibility in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION) and - not self.ended): + if ( + self.scoreboard_visibility + in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION) + and not self.ended + ): return False return True @@ -249,22 +422,29 @@ class Contest(models.Model): @cached_property def author_ids(self): - return Contest.authors.through.objects.filter(contest=self).values_list('profile_id', flat=True) + return Contest.authors.through.objects.filter(contest=self).values_list( + "profile_id", flat=True + ) @cached_property def editor_ids(self): return self.author_ids.union( - Contest.curators.through.objects.filter(contest=self).values_list('profile_id', flat=True)) + Contest.curators.through.objects.filter(contest=self).values_list( + "profile_id", flat=True + ) + ) @cached_property def tester_ids(self): - return Contest.testers.through.objects.filter(contest=self).values_list('profile_id', flat=True) + return Contest.testers.through.objects.filter(contest=self).values_list( + "profile_id", flat=True + ) def __str__(self): return self.name def get_absolute_url(self): - return reverse('contest_view', args=(self.key,)) + return reverse("contest_view", args=(self.key,)) def update_user_count(self): self.user_count = self.users.filter(virtual=0).count() @@ -289,7 +469,9 @@ class Contest(models.Model): return # If the user can view or edit all contests - if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'): + if user.has_perm("judge.see_private_contest") or user.has_perm( + "judge.edit_all_contest" + ): return # User is organizer or curator for contest @@ -310,14 +492,16 @@ class Contest(models.Model): if self.view_contest_scoreboard.filter(id=user.profile.id).exists(): return - - in_org = self.organizations.filter(id__in=user.profile.organizations.all()).exists() + + in_org = self.organizations.filter( + id__in=user.profile.organizations.all() + ).exists() in_users = self.private_contestants.filter(id=user.profile.id).exists() if not self.is_private and self.is_organization_private: if in_org: return - raise self.PrivateContest() + raise self.PrivateContest() if self.is_private and not self.is_organization_private: if in_users: @@ -339,11 +523,14 @@ class Contest(models.Model): def is_editable_by(self, user): # If the user can edit all contests - if user.has_perm('judge.edit_all_contest'): + if user.has_perm("judge.edit_all_contest"): return True # If the user is a contest organizer or curator - if user.has_perm('judge.edit_own_contest') and user.profile.id in self.editor_ids: + if ( + user.has_perm("judge.edit_own_contest") + and user.profile.id in self.editor_ids + ): return True return False @@ -351,19 +538,39 @@ class Contest(models.Model): @classmethod def get_visible_contests(cls, user): if not user.is_authenticated: - return cls.objects.filter(is_visible=True, is_organization_private=False, is_private=False) \ - .defer('description').distinct() + return ( + cls.objects.filter( + is_visible=True, is_organization_private=False, is_private=False + ) + .defer("description") + .distinct() + ) - queryset = cls.objects.defer('description') - if not (user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest')): + queryset = cls.objects.defer("description") + if not ( + user.has_perm("judge.see_private_contest") + or user.has_perm("judge.edit_all_contest") + ): q = Q(is_visible=True) q &= ( - Q(view_contest_scoreboard=user.profile) | - Q(is_organization_private=False, is_private=False) | - Q(is_organization_private=False, is_private=True, private_contestants=user.profile) | - Q(is_organization_private=True, is_private=False, organizations__in=user.profile.organizations.all()) | - Q(is_organization_private=True, is_private=True, organizations__in=user.profile.organizations.all(), - private_contestants=user.profile) + Q(view_contest_scoreboard=user.profile) + | Q(is_organization_private=False, is_private=False) + | Q( + is_organization_private=False, + is_private=True, + private_contestants=user.profile, + ) + | Q( + is_organization_private=True, + is_private=False, + organizations__in=user.profile.organizations.all(), + ) + | Q( + is_organization_private=True, + is_private=True, + organizations__in=user.profile.organizations.all(), + private_contestants=user.profile, + ) ) q |= Q(authors=user.profile) @@ -373,51 +580,75 @@ class Contest(models.Model): return queryset.distinct() def rate(self): - Rating.objects.filter(contest__end_time__range=(self.end_time, self._now)).delete() + Rating.objects.filter( + contest__end_time__range=(self.end_time, self._now) + ).delete() for contest in Contest.objects.filter( - is_rated=True, end_time__range=(self.end_time, self._now), - ).order_by('end_time'): + is_rated=True, + end_time__range=(self.end_time, self._now), + ).order_by("end_time"): rate_contest(contest) class Meta: permissions = ( - ('see_private_contest', _('See private contests')), - ('edit_own_contest', _('Edit own contests')), - ('edit_all_contest', _('Edit all contests')), - ('clone_contest', _('Clone contest')), - ('moss_contest', _('MOSS contest')), - ('contest_rating', _('Rate contests')), - ('contest_access_code', _('Contest access codes')), - ('create_private_contest', _('Create private contests')), - ('change_contest_visibility', _('Change contest visibility')), - ('contest_problem_label', _('Edit contest problem label script')), + ("see_private_contest", _("See private contests")), + ("edit_own_contest", _("Edit own contests")), + ("edit_all_contest", _("Edit all contests")), + ("clone_contest", _("Clone contest")), + ("moss_contest", _("MOSS contest")), + ("contest_rating", _("Rate contests")), + ("contest_access_code", _("Contest access codes")), + ("create_private_contest", _("Create private contests")), + ("change_contest_visibility", _("Change contest visibility")), + ("contest_problem_label", _("Edit contest problem label script")), ) - verbose_name = _('contest') - verbose_name_plural = _('contests') + verbose_name = _("contest") + verbose_name_plural = _("contests") class ContestParticipation(models.Model): LIVE = 0 SPECTATE = -1 - contest = models.ForeignKey(Contest, verbose_name=_('associated contest'), related_name='users', on_delete=CASCADE) - user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='contest_history', on_delete=CASCADE) - real_start = models.DateTimeField(verbose_name=_('start time'), default=timezone.now, db_column='start') - score = models.FloatField(verbose_name=_('score'), default=0, db_index=True) - cumtime = models.PositiveIntegerField(verbose_name=_('cumulative time'), default=0) - is_disqualified = models.BooleanField(verbose_name=_('is disqualified'), default=False, - help_text=_('Whether this participation is disqualified.')) - tiebreaker = models.FloatField(verbose_name=_('tie-breaking field'), default=0.0) - virtual = models.IntegerField(verbose_name=_('virtual participation id'), default=LIVE, - help_text=_('0 means non-virtual, otherwise the n-th virtual participation.')) - format_data = JSONField(verbose_name=_('contest format specific data'), null=True, blank=True) + contest = models.ForeignKey( + Contest, + verbose_name=_("associated contest"), + related_name="users", + on_delete=CASCADE, + ) + user = models.ForeignKey( + Profile, + verbose_name=_("user"), + related_name="contest_history", + on_delete=CASCADE, + ) + real_start = models.DateTimeField( + verbose_name=_("start time"), default=timezone.now, db_column="start" + ) + score = models.FloatField(verbose_name=_("score"), default=0, db_index=True) + cumtime = models.PositiveIntegerField(verbose_name=_("cumulative time"), default=0) + is_disqualified = models.BooleanField( + verbose_name=_("is disqualified"), + default=False, + help_text=_("Whether this participation is disqualified."), + ) + tiebreaker = models.FloatField(verbose_name=_("tie-breaking field"), default=0.0) + virtual = models.IntegerField( + verbose_name=_("virtual participation id"), + default=LIVE, + help_text=_("0 means non-virtual, otherwise the n-th virtual participation."), + ) + format_data = JSONField( + verbose_name=_("contest format specific data"), null=True, blank=True + ) def recompute_results(self): with transaction.atomic(): self.contest.format.update_participation(self) if self.is_disqualified: self.score = -9999 - self.save(update_fields=['score']) + self.save(update_fields=["score"]) + recompute_results.alters_data = True def set_disqualified(self, disqualified): @@ -431,6 +662,7 @@ class ContestParticipation(models.Model): self.contest.banned_users.add(self.user) else: self.contest.banned_users.remove(self.user) + set_disqualified.alters_data = True @property @@ -444,7 +676,11 @@ class ContestParticipation(models.Model): @cached_property def start(self): contest = self.contest - return contest.start_time if contest.time_limit is None and (self.live or self.spectate) else self.real_start + return ( + contest.start_time + if contest.time_limit is None and (self.live or self.spectate) + else self.real_start + ) @cached_property def end_time(self): @@ -456,8 +692,11 @@ class ContestParticipation(models.Model): return self.real_start + contest.time_limit else: return self.real_start + (contest.end_time - contest.start_time) - return contest.end_time if contest.time_limit is None else \ - min(self.real_start + contest.time_limit, contest.end_time) + return ( + contest.end_time + if contest.time_limit is None + else min(self.real_start + contest.time_limit, contest.end_time) + ) @cached_property def _now(self): @@ -476,88 +715,140 @@ class ContestParticipation(models.Model): def __str__(self): if self.spectate: - return gettext('%s spectating in %s') % (self.user.username, self.contest.name) + return gettext("%s spectating in %s") % ( + self.user.username, + self.contest.name, + ) if self.virtual: - return gettext('%s in %s, v%d') % (self.user.username, self.contest.name, self.virtual) - return gettext('%s in %s') % (self.user.username, self.contest.name) + return gettext("%s in %s, v%d") % ( + self.user.username, + self.contest.name, + self.virtual, + ) + return gettext("%s in %s") % (self.user.username, self.contest.name) class Meta: - verbose_name = _('contest participation') - verbose_name_plural = _('contest participations') + verbose_name = _("contest participation") + verbose_name_plural = _("contest participations") - unique_together = ('contest', 'user', 'virtual') + unique_together = ("contest", "user", "virtual") class ContestProblem(models.Model): - problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='contests', on_delete=CASCADE) - contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='contest_problems', on_delete=CASCADE) - points = models.IntegerField(verbose_name=_('points')) - partial = models.BooleanField(default=True, verbose_name=_('partial')) - is_pretested = models.BooleanField(default=False, verbose_name=_('is pretested')) - order = models.PositiveIntegerField(db_index=True, verbose_name=_('order')) - output_prefix_override = models.IntegerField(help_text=_('0 to not show testcases, 1 to show'), - verbose_name=_('visible testcases'), null=True, blank=True, default=0) - max_submissions = models.IntegerField(help_text=_('Maximum number of submissions for this problem, ' - 'or 0 for no limit.'), default=0, - validators=[MinValueValidator(0, _('Why include a problem you ' - 'can\'t submit to?'))]) + problem = models.ForeignKey( + Problem, verbose_name=_("problem"), related_name="contests", on_delete=CASCADE + ) + contest = models.ForeignKey( + Contest, + verbose_name=_("contest"), + related_name="contest_problems", + on_delete=CASCADE, + ) + points = models.IntegerField(verbose_name=_("points")) + partial = models.BooleanField(default=True, verbose_name=_("partial")) + is_pretested = models.BooleanField(default=False, verbose_name=_("is pretested")) + order = models.PositiveIntegerField(db_index=True, verbose_name=_("order")) + output_prefix_override = models.IntegerField( + help_text=_("0 to not show testcases, 1 to show"), + verbose_name=_("visible testcases"), + null=True, + blank=True, + default=0, + ) + max_submissions = models.IntegerField( + help_text=_( + "Maximum number of submissions for this problem, " "or 0 for no limit." + ), + default=0, + validators=[ + MinValueValidator(0, _("Why include a problem you " "can't submit to?")) + ], + ) class Meta: - unique_together = ('problem', 'contest') - verbose_name = _('contest problem') - verbose_name_plural = _('contest problems') + unique_together = ("problem", "contest") + verbose_name = _("contest problem") + verbose_name_plural = _("contest problems") class ContestSubmission(models.Model): - submission = models.OneToOneField(Submission, verbose_name=_('submission'), - related_name='contest', on_delete=CASCADE) - problem = models.ForeignKey(ContestProblem, verbose_name=_('problem'), on_delete=CASCADE, - related_name='submissions', related_query_name='submission') - participation = models.ForeignKey(ContestParticipation, verbose_name=_('participation'), on_delete=CASCADE, - related_name='submissions', related_query_name='submission') - points = models.FloatField(default=0.0, verbose_name=_('points')) - is_pretest = models.BooleanField(verbose_name=_('is pretested'), - help_text=_('Whether this submission was ran only on pretests.'), - default=False) + submission = models.OneToOneField( + Submission, + verbose_name=_("submission"), + related_name="contest", + on_delete=CASCADE, + ) + problem = models.ForeignKey( + ContestProblem, + verbose_name=_("problem"), + on_delete=CASCADE, + related_name="submissions", + related_query_name="submission", + ) + participation = models.ForeignKey( + ContestParticipation, + verbose_name=_("participation"), + on_delete=CASCADE, + related_name="submissions", + related_query_name="submission", + ) + points = models.FloatField(default=0.0, verbose_name=_("points")) + is_pretest = models.BooleanField( + verbose_name=_("is pretested"), + help_text=_("Whether this submission was ran only on pretests."), + default=False, + ) class Meta: - verbose_name = _('contest submission') - verbose_name_plural = _('contest submissions') + verbose_name = _("contest submission") + verbose_name_plural = _("contest submissions") class Rating(models.Model): - user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='ratings', on_delete=CASCADE) - contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='ratings', on_delete=CASCADE) - participation = models.OneToOneField(ContestParticipation, verbose_name=_('participation'), - related_name='rating', on_delete=CASCADE) - rank = models.IntegerField(verbose_name=_('rank')) - rating = models.IntegerField(verbose_name=_('rating')) - mean = models.FloatField(verbose_name=_('raw rating')) - performance = models.FloatField(verbose_name=_('contest performance')) - last_rated = models.DateTimeField(db_index=True, verbose_name=_('last rated')) + user = models.ForeignKey( + Profile, verbose_name=_("user"), related_name="ratings", on_delete=CASCADE + ) + contest = models.ForeignKey( + Contest, verbose_name=_("contest"), related_name="ratings", on_delete=CASCADE + ) + participation = models.OneToOneField( + ContestParticipation, + verbose_name=_("participation"), + related_name="rating", + on_delete=CASCADE, + ) + rank = models.IntegerField(verbose_name=_("rank")) + rating = models.IntegerField(verbose_name=_("rating")) + mean = models.FloatField(verbose_name=_("raw rating")) + performance = models.FloatField(verbose_name=_("contest performance")) + last_rated = models.DateTimeField(db_index=True, verbose_name=_("last rated")) class Meta: - unique_together = ('user', 'contest') - verbose_name = _('contest rating') - verbose_name_plural = _('contest ratings') + unique_together = ("user", "contest") + verbose_name = _("contest rating") + verbose_name_plural = _("contest ratings") class ContestMoss(models.Model): LANG_MAPPING = [ - ('C', MOSS_LANG_C), - ('C++', MOSS_LANG_CC), - ('Java', MOSS_LANG_JAVA), - ('Python', MOSS_LANG_PYTHON), - ('Pascal', MOSS_LANG_PASCAL), + ("C", MOSS_LANG_C), + ("C++", MOSS_LANG_CC), + ("Java", MOSS_LANG_JAVA), + ("Python", MOSS_LANG_PYTHON), + ("Pascal", MOSS_LANG_PASCAL), ] - contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='moss', on_delete=CASCADE) - problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='moss', on_delete=CASCADE) + contest = models.ForeignKey( + Contest, verbose_name=_("contest"), related_name="moss", on_delete=CASCADE + ) + problem = models.ForeignKey( + Problem, verbose_name=_("problem"), related_name="moss", on_delete=CASCADE + ) language = models.CharField(max_length=10) submission_count = models.PositiveIntegerField(default=0) url = models.URLField(null=True, blank=True) class Meta: - unique_together = ('contest', 'problem', 'language') - verbose_name = _('contest moss result') - verbose_name_plural = _('contest moss results') + unique_together = ("contest", "problem", "language") + verbose_name = _("contest moss result") + verbose_name_plural = _("contest moss results") diff --git a/judge/models/interface.py b/judge/models/interface.py index 6d94d52..24b05de 100644 --- a/judge/models/interface.py +++ b/judge/models/interface.py @@ -10,7 +10,7 @@ from mptt.models import MPTTModel from judge.models.profile import Organization, Profile -__all__ = ['MiscConfig', 'validate_regex', 'NavigationBar', 'BlogPost'] +__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"] class MiscConfig(models.Model): @@ -21,32 +21,40 @@ class MiscConfig(models.Model): return self.key class Meta: - verbose_name = _('configuration item') - verbose_name_plural = _('miscellaneous configuration') + verbose_name = _("configuration item") + verbose_name_plural = _("miscellaneous configuration") def validate_regex(regex): try: re.compile(regex, re.VERBOSE) except re.error as e: - raise ValidationError('Invalid regex: %s' % e.message) + raise ValidationError("Invalid regex: %s" % e.message) class NavigationBar(MPTTModel): class Meta: - verbose_name = _('navigation item') - verbose_name_plural = _('navigation bar') + verbose_name = _("navigation item") + verbose_name_plural = _("navigation bar") class MPTTMeta: - order_insertion_by = ['order'] + order_insertion_by = ["order"] - order = models.PositiveIntegerField(db_index=True, verbose_name=_('order')) - key = models.CharField(max_length=10, unique=True, verbose_name=_('identifier')) - label = models.CharField(max_length=20, verbose_name=_('label')) - path = models.CharField(max_length=255, verbose_name=_('link path')) - regex = models.TextField(verbose_name=_('highlight regex'), validators=[validate_regex]) - parent = TreeForeignKey('self', verbose_name=_('parent item'), null=True, blank=True, - related_name='children', on_delete=models.CASCADE) + order = models.PositiveIntegerField(db_index=True, verbose_name=_("order")) + key = models.CharField(max_length=10, unique=True, verbose_name=_("identifier")) + label = models.CharField(max_length=20, verbose_name=_("label")) + path = models.CharField(max_length=255, verbose_name=_("link path")) + regex = models.TextField( + verbose_name=_("highlight regex"), validators=[validate_regex] + ) + parent = TreeForeignKey( + "self", + verbose_name=_("parent item"), + null=True, + blank=True, + related_name="children", + on_delete=models.CASCADE, + ) def __str__(self): return self.label @@ -63,46 +71,61 @@ class NavigationBar(MPTTModel): class BlogPost(models.Model): - title = models.CharField(verbose_name=_('post title'), max_length=100) - authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True) - slug = models.SlugField(verbose_name=_('slug')) - visible = models.BooleanField(verbose_name=_('public visibility'), default=False) - sticky = models.BooleanField(verbose_name=_('sticky'), default=False) - publish_on = models.DateTimeField(verbose_name=_('publish after')) - content = models.TextField(verbose_name=_('post content')) - summary = models.TextField(verbose_name=_('post summary'), blank=True) - og_image = models.CharField(verbose_name=_('openGraph image'), default='', max_length=150, blank=True) - organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'), - help_text=_('If private, only these organizations may see the blog post.')) - is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False) + title = models.CharField(verbose_name=_("post title"), max_length=100) + authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True) + slug = models.SlugField(verbose_name=_("slug")) + visible = models.BooleanField(verbose_name=_("public visibility"), default=False) + sticky = models.BooleanField(verbose_name=_("sticky"), default=False) + publish_on = models.DateTimeField(verbose_name=_("publish after")) + content = models.TextField(verbose_name=_("post content")) + summary = models.TextField(verbose_name=_("post summary"), blank=True) + og_image = models.CharField( + verbose_name=_("openGraph image"), default="", max_length=150, blank=True + ) + organizations = models.ManyToManyField( + Organization, + blank=True, + verbose_name=_("organizations"), + help_text=_("If private, only these organizations may see the blog post."), + ) + is_organization_private = models.BooleanField( + verbose_name=_("private to organizations"), default=False + ) def __str__(self): return self.title def get_absolute_url(self): - return reverse('blog_post', args=(self.id, self.slug)) + return reverse("blog_post", args=(self.id, self.slug)) def can_see(self, user): if self.visible and self.publish_on <= timezone.now(): if not self.is_organization_private: return True - if user.is_authenticated and \ - self.organizations.filter(id__in=user.profile.organizations.all()).exists(): + if ( + user.is_authenticated + and self.organizations.filter( + id__in=user.profile.organizations.all() + ).exists() + ): return True - if user.has_perm('judge.edit_all_post'): + if user.has_perm("judge.edit_all_post"): return True - return user.is_authenticated and self.authors.filter(id=user.profile.id).exists() + return ( + user.is_authenticated and self.authors.filter(id=user.profile.id).exists() + ) def is_editable_by(self, user): - if not user.is_authenticated: - return False - if user.has_perm('judge.edit_all_post'): - return True - return user.has_perm('judge.change_blogpost') and self.authors.filter(id=user.profile.id).exists() + if not user.is_authenticated: + return False + if user.has_perm("judge.edit_all_post"): + return True + return ( + user.has_perm("judge.change_blogpost") + and self.authors.filter(id=user.profile.id).exists() + ) class Meta: - permissions = ( - ('edit_all_post', _('Edit all posts')), - ) - verbose_name = _('blog post') - verbose_name_plural = _('blog posts') + permissions = (("edit_all_post", _("Edit all posts")),) + verbose_name = _("blog post") + verbose_name_plural = _("blog posts") diff --git a/judge/models/message.py b/judge/models/message.py index 541d63f..c2e8291 100644 --- a/judge/models/message.py +++ b/judge/models/message.py @@ -4,17 +4,31 @@ from django.utils.translation import gettext_lazy as _ from judge.models.profile import Profile -__all__ = ['PrivateMessage', 'PrivateMessageThread'] +__all__ = ["PrivateMessage", "PrivateMessageThread"] class PrivateMessage(models.Model): - title = models.CharField(verbose_name=_('message title'), max_length=50) - content = models.TextField(verbose_name=_('message body')) - sender = models.ForeignKey(Profile, verbose_name=_('sender'), related_name='sent_messages', on_delete=CASCADE) - target = models.ForeignKey(Profile, verbose_name=_('target'), related_name='received_messages', on_delete=CASCADE) - timestamp = models.DateTimeField(verbose_name=_('message timestamp'), auto_now_add=True) - read = models.BooleanField(verbose_name=_('read'), default=False) + title = models.CharField(verbose_name=_("message title"), max_length=50) + content = models.TextField(verbose_name=_("message body")) + sender = models.ForeignKey( + Profile, + verbose_name=_("sender"), + related_name="sent_messages", + on_delete=CASCADE, + ) + target = models.ForeignKey( + Profile, + verbose_name=_("target"), + related_name="received_messages", + on_delete=CASCADE, + ) + timestamp = models.DateTimeField( + verbose_name=_("message timestamp"), auto_now_add=True + ) + read = models.BooleanField(verbose_name=_("read"), default=False) class PrivateMessageThread(models.Model): - messages = models.ManyToManyField(PrivateMessage, verbose_name=_('messages in the thread')) + messages = models.ManyToManyField( + PrivateMessage, verbose_name=_("messages in the thread") + ) diff --git a/judge/models/problem.py b/judge/models/problem.py index ce66da5..bb99ae3 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -19,143 +19,286 @@ from judge.models.runtime import Language from judge.user_translations import gettext as user_gettext from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join -__all__ = ['ProblemGroup', 'ProblemType', 'Problem', 'ProblemTranslation', 'ProblemClarification', - 'License', 'Solution', 'TranslatedProblemQuerySet', 'TranslatedProblemForeignKeyQuerySet'] +__all__ = [ + "ProblemGroup", + "ProblemType", + "Problem", + "ProblemTranslation", + "ProblemClarification", + "License", + "Solution", + "TranslatedProblemQuerySet", + "TranslatedProblemForeignKeyQuerySet", +] class ProblemType(models.Model): - name = models.CharField(max_length=20, verbose_name=_('problem category ID'), unique=True) - full_name = models.CharField(max_length=100, verbose_name=_('problem category name')) + name = models.CharField( + max_length=20, verbose_name=_("problem category ID"), unique=True + ) + full_name = models.CharField( + max_length=100, verbose_name=_("problem category name") + ) def __str__(self): return self.full_name class Meta: - ordering = ['full_name'] - verbose_name = _('problem type') - verbose_name_plural = _('problem types') + ordering = ["full_name"] + verbose_name = _("problem type") + verbose_name_plural = _("problem types") class ProblemGroup(models.Model): - name = models.CharField(max_length=20, verbose_name=_('problem group ID'), unique=True) - full_name = models.CharField(max_length=100, verbose_name=_('problem group name')) + name = models.CharField( + max_length=20, verbose_name=_("problem group ID"), unique=True + ) + full_name = models.CharField(max_length=100, verbose_name=_("problem group name")) def __str__(self): return self.full_name class Meta: - ordering = ['full_name'] - verbose_name = _('problem group') - verbose_name_plural = _('problem groups') + ordering = ["full_name"] + verbose_name = _("problem group") + verbose_name_plural = _("problem groups") class License(models.Model): - key = models.CharField(max_length=20, unique=True, verbose_name=_('key'), - validators=[RegexValidator(r'^[-\w.]+$', r'License key must be ^[-\w.]+$')]) - link = models.CharField(max_length=256, verbose_name=_('link')) - name = models.CharField(max_length=256, verbose_name=_('full name')) - display = models.CharField(max_length=256, blank=True, verbose_name=_('short name'), - help_text=_('Displayed on pages under this license')) - icon = models.CharField(max_length=256, blank=True, verbose_name=_('icon'), help_text=_('URL to the icon')) - text = models.TextField(verbose_name=_('license text')) + key = models.CharField( + max_length=20, + unique=True, + verbose_name=_("key"), + validators=[RegexValidator(r"^[-\w.]+$", r"License key must be ^[-\w.]+$")], + ) + link = models.CharField(max_length=256, verbose_name=_("link")) + name = models.CharField(max_length=256, verbose_name=_("full name")) + display = models.CharField( + max_length=256, + blank=True, + verbose_name=_("short name"), + help_text=_("Displayed on pages under this license"), + ) + icon = models.CharField( + max_length=256, + blank=True, + verbose_name=_("icon"), + help_text=_("URL to the icon"), + ) + text = models.TextField(verbose_name=_("license text")) def __str__(self): return self.name def get_absolute_url(self): - return reverse('license', args=(self.key,)) + return reverse("license", args=(self.key,)) class Meta: - verbose_name = _('license') - verbose_name_plural = _('licenses') + verbose_name = _("license") + verbose_name_plural = _("licenses") class TranslatedProblemQuerySet(SearchQuerySet): def __init__(self, **kwargs): - super(TranslatedProblemQuerySet, self).__init__(('code', 'name', 'description'), **kwargs) + super(TranslatedProblemQuerySet, self).__init__( + ("code", "name", "description"), **kwargs + ) def add_i18n_name(self, language): queryset = self._clone() - alias = unique_together_left_join(queryset, ProblemTranslation, 'problem', 'language', language) - return queryset.annotate(i18n_name=Coalesce(RawSQL('%s.name' % alias, ()), F('name'), - output_field=models.CharField())) + alias = unique_together_left_join( + queryset, ProblemTranslation, "problem", "language", language + ) + return queryset.annotate( + i18n_name=Coalesce( + RawSQL("%s.name" % alias, ()), + F("name"), + output_field=models.CharField(), + ) + ) class TranslatedProblemForeignKeyQuerySet(QuerySet): def add_problem_i18n_name(self, key, language, name_field=None): - queryset = self._clone() if name_field is None else self.annotate(_name=F(name_field)) - alias = unique_together_left_join(queryset, ProblemTranslation, 'problem', 'language', language, - parent_model=Problem) + queryset = ( + self._clone() if name_field is None else self.annotate(_name=F(name_field)) + ) + alias = unique_together_left_join( + queryset, + ProblemTranslation, + "problem", + "language", + language, + parent_model=Problem, + ) # You must specify name_field if Problem is not yet joined into the QuerySet. - kwargs = {key: Coalesce(RawSQL('%s.name' % alias, ()), - F(name_field) if name_field else RawSQLColumn(Problem, 'name'), - output_field=models.CharField())} + kwargs = { + key: Coalesce( + RawSQL("%s.name" % alias, ()), + F(name_field) if name_field else RawSQLColumn(Problem, "name"), + output_field=models.CharField(), + ) + } return queryset.annotate(**kwargs) class Problem(models.Model): - code = models.CharField(max_length=20, verbose_name=_('problem code'), unique=True, - validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))], - help_text=_('A short, unique code for the problem, ' - 'used in the url after /problem/')) - name = models.CharField(max_length=100, verbose_name=_('problem name'), db_index=True, - help_text=_('The full name of the problem, ' - 'as shown in the problem list.')) - description = models.TextField(verbose_name=_('problem body')) - authors = models.ManyToManyField(Profile, verbose_name=_('creators'), blank=True, related_name='authored_problems', - help_text=_('These users will be able to edit the problem, ' - 'and be listed as authors.')) - curators = models.ManyToManyField(Profile, verbose_name=_('curators'), blank=True, related_name='curated_problems', - help_text=_('These users will be able to edit the problem, ' - 'but not be listed as authors.')) - testers = models.ManyToManyField(Profile, verbose_name=_('testers'), blank=True, related_name='tested_problems', - help_text=_( - 'These users will be able to view the private problem, but not edit it.')) - types = models.ManyToManyField(ProblemType, verbose_name=_('problem types'), - help_text=_('The type of problem, ' - "as shown on the problem's page.")) - group = models.ForeignKey(ProblemGroup, verbose_name=_('problem group'), on_delete=CASCADE, - help_text=_('The group of problem, shown under Category in the problem list.')) - time_limit = models.FloatField(verbose_name=_('time limit'), - help_text=_('The time limit for this problem, in seconds. ' - 'Fractional seconds (e.g. 1.5) are supported.'), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT), - MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)]) - memory_limit = models.PositiveIntegerField(verbose_name=_('memory limit'), - help_text=_('The memory limit for this problem, in kilobytes ' - '(e.g. 64mb = 65536 kilobytes).'), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT), - MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)]) + code = models.CharField( + max_length=20, + verbose_name=_("problem code"), + unique=True, + validators=[ + RegexValidator("^[a-z0-9]+$", _("Problem code must be ^[a-z0-9]+$")) + ], + help_text=_( + "A short, unique code for the problem, " "used in the url after /problem/" + ), + ) + name = models.CharField( + max_length=100, + verbose_name=_("problem name"), + db_index=True, + help_text=_("The full name of the problem, " "as shown in the problem list."), + ) + description = models.TextField(verbose_name=_("problem body")) + authors = models.ManyToManyField( + Profile, + verbose_name=_("creators"), + blank=True, + related_name="authored_problems", + help_text=_( + "These users will be able to edit the problem, " "and be listed as authors." + ), + ) + curators = models.ManyToManyField( + Profile, + verbose_name=_("curators"), + blank=True, + related_name="curated_problems", + help_text=_( + "These users will be able to edit the problem, " + "but not be listed as authors." + ), + ) + testers = models.ManyToManyField( + Profile, + verbose_name=_("testers"), + blank=True, + related_name="tested_problems", + help_text=_( + "These users will be able to view the private problem, but not edit it." + ), + ) + types = models.ManyToManyField( + ProblemType, + verbose_name=_("problem types"), + help_text=_("The type of problem, " "as shown on the problem's page."), + ) + group = models.ForeignKey( + ProblemGroup, + verbose_name=_("problem group"), + on_delete=CASCADE, + help_text=_("The group of problem, shown under Category in the problem list."), + ) + time_limit = models.FloatField( + verbose_name=_("time limit"), + help_text=_( + "The time limit for this problem, in seconds. " + "Fractional seconds (e.g. 1.5) are supported." + ), + validators=[ + MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT), + MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT), + ], + ) + memory_limit = models.PositiveIntegerField( + verbose_name=_("memory limit"), + help_text=_( + "The memory limit for this problem, in kilobytes " + "(e.g. 64mb = 65536 kilobytes)." + ), + validators=[ + MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT), + MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT), + ], + ) short_circuit = models.BooleanField(default=False) - points = models.FloatField(verbose_name=_('points'), - help_text=_('Points awarded for problem completion. ' - "Points are displayed with a 'p' suffix if partial."), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_PROBLEM_POINTS)]) - partial = models.BooleanField(verbose_name=_('allows partial points'), default=False) - allowed_languages = models.ManyToManyField(Language, verbose_name=_('allowed languages'), - help_text=_('List of allowed submission languages.')) - is_public = models.BooleanField(verbose_name=_('publicly visible'), db_index=True, default=False) - is_manually_managed = models.BooleanField(verbose_name=_('manually managed'), db_index=True, default=False, - help_text=_('Whether judges should be allowed to manage data or not.')) - date = models.DateTimeField(verbose_name=_('date of publishing'), null=True, blank=True, db_index=True, - help_text=_("Doesn't have magic ability to auto-publish due to backward compatibility")) - banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True, - help_text=_('Bans the selected users from submitting to this problem.')) - license = models.ForeignKey(License, null=True, blank=True, on_delete=SET_NULL, - help_text=_('The license under which this problem is published.')) - og_image = models.CharField(verbose_name=_('OpenGraph image'), max_length=150, blank=True) - summary = models.TextField(blank=True, verbose_name=_('problem summary'), - help_text=_('Plain-text, shown in meta description tag, e.g. for social media.')) - user_count = models.IntegerField(verbose_name=_('number of users'), default=0, - help_text=_('The number of users who solved the problem.')) - ac_rate = models.FloatField(verbose_name=_('solve rate'), default=0) + points = models.FloatField( + verbose_name=_("points"), + help_text=_( + "Points awarded for problem completion. " + "Points are displayed with a 'p' suffix if partial." + ), + validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_PROBLEM_POINTS)], + ) + partial = models.BooleanField( + verbose_name=_("allows partial points"), default=False + ) + allowed_languages = models.ManyToManyField( + Language, + verbose_name=_("allowed languages"), + help_text=_("List of allowed submission languages."), + ) + is_public = models.BooleanField( + verbose_name=_("publicly visible"), db_index=True, default=False + ) + is_manually_managed = models.BooleanField( + verbose_name=_("manually managed"), + db_index=True, + default=False, + help_text=_("Whether judges should be allowed to manage data or not."), + ) + date = models.DateTimeField( + verbose_name=_("date of publishing"), + null=True, + blank=True, + db_index=True, + help_text=_( + "Doesn't have magic ability to auto-publish due to backward compatibility" + ), + ) + banned_users = models.ManyToManyField( + Profile, + verbose_name=_("personae non gratae"), + blank=True, + help_text=_("Bans the selected users from submitting to this problem."), + ) + license = models.ForeignKey( + License, + null=True, + blank=True, + on_delete=SET_NULL, + help_text=_("The license under which this problem is published."), + ) + og_image = models.CharField( + verbose_name=_("OpenGraph image"), max_length=150, blank=True + ) + summary = models.TextField( + blank=True, + verbose_name=_("problem summary"), + help_text=_( + "Plain-text, shown in meta description tag, e.g. for social media." + ), + ) + user_count = models.IntegerField( + verbose_name=_("number of users"), + default=0, + help_text=_("The number of users who solved the problem."), + ) + ac_rate = models.FloatField(verbose_name=_("solve rate"), default=0) objects = TranslatedProblemQuerySet.as_manager() - tickets = GenericRelation('Ticket') + tickets = GenericRelation("Ticket") - organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'), - help_text=_('If private, only these organizations may see the problem.')) - is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False) + organizations = models.ManyToManyField( + Organization, + blank=True, + verbose_name=_("organizations"), + help_text=_("If private, only these organizations may see the problem."), + ) + is_organization_private = models.BooleanField( + verbose_name=_("private to organizations"), default=False + ) def __init__(self, *args, **kwargs): super(Problem, self).__init__(*args, **kwargs) @@ -165,20 +308,30 @@ class Problem(models.Model): @cached_property def types_list(self): - return list(map(user_gettext, map(attrgetter('full_name'), self.types.all()))) + return list(map(user_gettext, map(attrgetter("full_name"), self.types.all()))) def languages_list(self): - return self.allowed_languages.values_list('common_name', flat=True).distinct().order_by('common_name') + return ( + self.allowed_languages.values_list("common_name", flat=True) + .distinct() + .order_by("common_name") + ) def is_editor(self, profile): - return (self.authors.filter(id=profile.id) | self.curators.filter(id=profile.id)).exists() + return ( + self.authors.filter(id=profile.id) | self.curators.filter(id=profile.id) + ).exists() def is_editable_by(self, user): if not user.is_authenticated: return False - if user.has_perm('judge.edit_all_problem') or user.has_perm('judge.edit_public_problem') and self.is_public: + if ( + user.has_perm("judge.edit_all_problem") + or user.has_perm("judge.edit_public_problem") + and self.is_public + ): return True - return user.has_perm('judge.edit_own_problem') and self.is_editor(user.profile) + return user.has_perm("judge.edit_own_problem") and self.is_editor(user.profile) def is_accessible_by(self, user): # Problem is public. @@ -188,23 +341,24 @@ class Problem(models.Model): return True # If the user can see all organization private problems. - if user.has_perm('judge.see_organization_problem'): + if user.has_perm("judge.see_organization_problem"): return True # If the user is in the organization. - if user.is_authenticated and \ - self.organizations.filter(id__in=user.profile.organizations.all()): + if user.is_authenticated and self.organizations.filter( + id__in=user.profile.organizations.all() + ): return True # If the user can view all problems. - if user.has_perm('judge.see_private_problem'): + if user.has_perm("judge.see_private_problem"): return True if not user.is_authenticated: return False # If the user authored the problem or is a curator. - if user.has_perm('judge.edit_own_problem') and self.is_editor(user.profile): + if user.has_perm("judge.edit_own_problem") and self.is_editor(user.profile): return True # If user is a tester. @@ -216,11 +370,18 @@ class Problem(models.Model): if current is None: return False from judge.models import ContestProblem - return ContestProblem.objects.filter(problem_id=self.id, contest__users__id=current).exists() + + return ContestProblem.objects.filter( + problem_id=self.id, contest__users__id=current + ).exists() def is_subs_manageable_by(self, user): - return user.is_staff and user.has_perm('judge.rejudge_submission') and self.is_editable_by(user) - + return ( + user.is_staff + and user.has_perm("judge.rejudge_submission") + and self.is_editable_by(user) + ) + @classmethod def get_visible_problems(cls, user): # Do unauthenticated check here so we can skip authentication checks later on. @@ -235,15 +396,18 @@ class Problem(models.Model): # - is_public problems # - not is_organization_private or in organization or `judge.see_organization_problem` # - author or curator or tester - queryset = cls.objects.defer('description') + queryset = cls.objects.defer("description") - if not (user.has_perm('judge.see_private_problem') or user.has_perm('judge.edit_all_problem')): + if not ( + user.has_perm("judge.see_private_problem") + or user.has_perm("judge.edit_all_problem") + ): q = Q(is_public=True) - if not user.has_perm('judge.see_organization_problem'): + if not user.has_perm("judge.see_organization_problem"): # Either not organization private or in the organization. - q &= ( - Q(is_organization_private=False) | - Q(is_organization_private=True, organizations__in=user.profile.organizations.all()) + q &= Q(is_organization_private=False) | Q( + is_organization_private=True, + organizations__in=user.profile.organizations.all(), ) # Authors, curators, and testers should always have access, so OR at the very end. @@ -256,40 +420,46 @@ class Problem(models.Model): @classmethod def get_public_problems(cls): - return cls.objects.filter(is_public=True, is_organization_private=False).defer('description') + return cls.objects.filter(is_public=True, is_organization_private=False).defer( + "description" + ) def __str__(self): return self.name def get_absolute_url(self): - return reverse('problem_detail', args=(self.code,)) + return reverse("problem_detail", args=(self.code,)) @cached_property def author_ids(self): - return self.authors.values_list('id', flat=True) + return self.authors.values_list("id", flat=True) @cached_property def editor_ids(self): - return self.author_ids | self.curators.values_list('id', flat=True) + return self.author_ids | self.curators.values_list("id", flat=True) @cached_property def tester_ids(self): - return self.testers.values_list('id', flat=True) + return self.testers.values_list("id", flat=True) @cached_property def usable_common_names(self): - return set(self.usable_languages.values_list('common_name', flat=True)) + return set(self.usable_languages.values_list("common_name", flat=True)) @property def usable_languages(self): - return self.allowed_languages.filter(judges__in=self.judges.filter(online=True)).distinct() + return self.allowed_languages.filter( + judges__in=self.judges.filter(online=True) + ).distinct() def translated_name(self, language): if language in self._translated_name_cache: return self._translated_name_cache[language] # Hits database despite prefetch_related. try: - name = self.translations.filter(language=language).values_list('name', flat=True)[0] + name = self.translations.filter(language=language).values_list( + "name", flat=True + )[0] except IndexError: name = self.name self._translated_name_cache[language] = name @@ -310,12 +480,23 @@ class Problem(models.Model): return ProblemClarification.objects.filter(problem=self) def update_stats(self): - self.user_count = self.submission_set.filter(points__gte=self.points, result='AC', - user__is_unlisted=False).values('user').distinct().count() + self.user_count = ( + self.submission_set.filter( + points__gte=self.points, result="AC", user__is_unlisted=False + ) + .values("user") + .distinct() + .count() + ) submissions = self.submission_set.count() if submissions: - self.ac_rate = 100.0 * self.submission_set.filter(points__gte=self.points, result='AC', - user__is_unlisted=False).count() / submissions + self.ac_rate = ( + 100.0 + * self.submission_set.filter( + points__gte=self.points, result="AC", user__is_unlisted=False + ).count() + / submissions + ) else: self.ac_rate = 0 self.save() @@ -324,9 +505,13 @@ class Problem(models.Model): def _get_limits(self, key): global_limit = getattr(self, key) - limits = {limit['language_id']: (limit['language__name'], limit[key]) - for limit in self.language_limits.values('language_id', 'language__name', key) - if limit[key] != global_limit} + limits = { + limit["language_id"]: (limit["language__name"], limit[key]) + for limit in self.language_limits.values( + "language_id", "language__name", key + ) + if limit[key] != global_limit + } limit_ids = set(limits.keys()) common = [] @@ -346,21 +531,21 @@ class Problem(models.Model): @property def language_time_limit(self): - key = 'problem_tls:%d' % self.id + key = "problem_tls:%d" % self.id result = cache.get(key) if result is not None: return result - result = self._get_limits('time_limit') + result = self._get_limits("time_limit") cache.set(key, result) return result @property def language_memory_limit(self): - key = 'problem_mls:%d' % self.id + key = "problem_mls:%d" % self.id result = cache.get(key) if result is not None: return result - result = self._get_limits('memory_limit') + result = self._get_limits("memory_limit") cache.set(key, result) return result @@ -395,105 +580,143 @@ class Problem(models.Model): return False # If the user has a full AC submission to the problem (solved the problem). - return self.submission_set.filter(user=user.profile, result='AC', points=F('problem__points')).exists() + return self.submission_set.filter( + user=user.profile, result="AC", points=F("problem__points") + ).exists() class Meta: permissions = ( - ('see_private_problem', 'See hidden problems'), - ('edit_own_problem', 'Edit own problems'), - ('edit_all_problem', 'Edit all problems'), - ('edit_public_problem', 'Edit all public problems'), - ('clone_problem', 'Clone problem'), - ('change_public_visibility', 'Change is_public field'), - ('change_manually_managed', 'Change is_manually_managed field'), - ('see_organization_problem', 'See organization-private problems'), - ('suggest_problem_changes', 'Suggest changes to problem'), + ("see_private_problem", "See hidden problems"), + ("edit_own_problem", "Edit own problems"), + ("edit_all_problem", "Edit all problems"), + ("edit_public_problem", "Edit all public problems"), + ("clone_problem", "Clone problem"), + ("change_public_visibility", "Change is_public field"), + ("change_manually_managed", "Change is_manually_managed field"), + ("see_organization_problem", "See organization-private problems"), + ("suggest_problem_changes", "Suggest changes to problem"), ) - verbose_name = _('problem') - verbose_name_plural = _('problems') + verbose_name = _("problem") + verbose_name_plural = _("problems") class ProblemTranslation(models.Model): - problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='translations', on_delete=CASCADE) - language = models.CharField(verbose_name=_('language'), max_length=7, choices=settings.LANGUAGES) - name = models.CharField(verbose_name=_('translated name'), max_length=100, db_index=True) - description = models.TextField(verbose_name=_('translated description')) + problem = models.ForeignKey( + Problem, + verbose_name=_("problem"), + related_name="translations", + on_delete=CASCADE, + ) + language = models.CharField( + verbose_name=_("language"), max_length=7, choices=settings.LANGUAGES + ) + name = models.CharField( + verbose_name=_("translated name"), max_length=100, db_index=True + ) + description = models.TextField(verbose_name=_("translated description")) class Meta: - unique_together = ('problem', 'language') - verbose_name = _('problem translation') - verbose_name_plural = _('problem translations') + unique_together = ("problem", "language") + verbose_name = _("problem translation") + verbose_name_plural = _("problem translations") class ProblemClarification(models.Model): - problem = models.ForeignKey(Problem, verbose_name=_('clarified problem'), on_delete=CASCADE) - description = models.TextField(verbose_name=_('clarification body')) - date = models.DateTimeField(verbose_name=_('clarification timestamp'), auto_now_add=True) + problem = models.ForeignKey( + Problem, verbose_name=_("clarified problem"), on_delete=CASCADE + ) + description = models.TextField(verbose_name=_("clarification body")) + date = models.DateTimeField( + verbose_name=_("clarification timestamp"), auto_now_add=True + ) class LanguageLimit(models.Model): - problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='language_limits', on_delete=CASCADE) - language = models.ForeignKey(Language, verbose_name=_('language'), on_delete=CASCADE) - time_limit = models.FloatField(verbose_name=_('time limit'), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT), - MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)]) - memory_limit = models.IntegerField(verbose_name=_('memory limit'), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT), - MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)]) + problem = models.ForeignKey( + Problem, + verbose_name=_("problem"), + related_name="language_limits", + on_delete=CASCADE, + ) + language = models.ForeignKey( + Language, verbose_name=_("language"), on_delete=CASCADE + ) + time_limit = models.FloatField( + verbose_name=_("time limit"), + validators=[ + MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT), + MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT), + ], + ) + memory_limit = models.IntegerField( + verbose_name=_("memory limit"), + validators=[ + MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT), + MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT), + ], + ) class Meta: - unique_together = ('problem', 'language') - verbose_name = _('language-specific resource limit') - verbose_name_plural = _('language-specific resource limits') + unique_together = ("problem", "language") + verbose_name = _("language-specific resource limit") + verbose_name_plural = _("language-specific resource limits") class Solution(models.Model): - problem = models.OneToOneField(Problem, on_delete=SET_NULL, verbose_name=_('associated problem'), - null=True, blank=True, related_name='solution') - is_public = models.BooleanField(verbose_name=_('public visibility'), default=False) - publish_on = models.DateTimeField(verbose_name=_('publish date')) - authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True) - content = models.TextField(verbose_name=_('editorial content')) + problem = models.OneToOneField( + Problem, + on_delete=SET_NULL, + verbose_name=_("associated problem"), + null=True, + blank=True, + related_name="solution", + ) + is_public = models.BooleanField(verbose_name=_("public visibility"), default=False) + publish_on = models.DateTimeField(verbose_name=_("publish date")) + authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True) + content = models.TextField(verbose_name=_("editorial content")) def get_absolute_url(self): problem = self.problem if problem is None: - return reverse('home') + return reverse("home") else: - return reverse('problem_editorial', args=[problem.code]) + return reverse("problem_editorial", args=[problem.code]) def __str__(self): - return _('Editorial for %s') % self.problem.name + return _("Editorial for %s") % self.problem.name class Meta: - permissions = ( - ('see_private_solution', 'See hidden solutions'), - ) - verbose_name = _('solution') - verbose_name_plural = _('solutions') + permissions = (("see_private_solution", "See hidden solutions"),) + verbose_name = _("solution") + verbose_name_plural = _("solutions") class ProblemPointsVote(models.Model): points = models.IntegerField( - verbose_name=_('proposed point value'), - help_text=_('The amount of points you think this problem deserves.'), + verbose_name=_("proposed point value"), + help_text=_("The amount of points you think this problem deserves."), validators=[ MinValueValidator(100), MaxValueValidator(600), ], ) - voter = models.ForeignKey(Profile, related_name='problem_points_votes', on_delete=CASCADE, db_index=True) - problem = models.ForeignKey(Problem, related_name='problem_points_votes', on_delete=CASCADE, db_index=True) + voter = models.ForeignKey( + Profile, related_name="problem_points_votes", on_delete=CASCADE, db_index=True + ) + problem = models.ForeignKey( + Problem, related_name="problem_points_votes", on_delete=CASCADE, db_index=True + ) vote_time = models.DateTimeField( - verbose_name=_('The time this vote was cast'), + verbose_name=_("The time this vote was cast"), auto_now_add=True, blank=True, ) class Meta: - verbose_name = _('vote') - verbose_name_plural = _('votes') + verbose_name = _("vote") + verbose_name_plural = _("votes") def __str__(self): - return f'{self.voter}: {self.points} for {self.problem.code}' \ No newline at end of file + return f"{self.voter}: {self.points} for {self.problem.code}" diff --git a/judge/models/problem_data.py b/judge/models/problem_data.py index 3735552..8617a61 100644 --- a/judge/models/problem_data.py +++ b/judge/models/problem_data.py @@ -9,7 +9,13 @@ from django.utils.translation import gettext_lazy as _ from judge.utils.problem_data import ProblemDataStorage, get_file_cachekey -__all__ = ['problem_data_storage', 'problem_directory_file', 'ProblemData', 'ProblemTestCase', 'CHECKERS'] +__all__ = [ + "problem_data_storage", + "problem_directory_file", + "ProblemData", + "ProblemTestCase", + "CHECKERS", +] problem_data_storage = ProblemDataStorage() @@ -23,52 +29,83 @@ def problem_directory_file(data, filename): CHECKERS = ( - ('standard', _('Standard')), - ('floats', _('Floats')), - ('floatsabs', _('Floats (absolute)')), - ('floatsrel', _('Floats (relative)')), - ('rstripped', _('Non-trailing spaces')), - ('sorted', _('Unordered')), - ('identical', _('Byte identical')), - ('linecount', _('Line-by-line')), - ('custom', _('Custom checker (PY)')), - ('customval', _('Custom validator (CPP)')), - ('interact', _('Interactive')), + ("standard", _("Standard")), + ("floats", _("Floats")), + ("floatsabs", _("Floats (absolute)")), + ("floatsrel", _("Floats (relative)")), + ("rstripped", _("Non-trailing spaces")), + ("sorted", _("Unordered")), + ("identical", _("Byte identical")), + ("linecount", _("Line-by-line")), + ("custom", _("Custom checker (PY)")), + ("customval", _("Custom validator (CPP)")), + ("interact", _("Interactive")), ) class ProblemData(models.Model): - problem = models.OneToOneField('Problem', verbose_name=_('problem'), related_name='data_files', - on_delete=models.CASCADE) - zipfile = models.FileField(verbose_name=_('data zip file'), storage=problem_data_storage, null=True, blank=True, - upload_to=problem_directory_file) - generator = models.FileField(verbose_name=_('generator file'), storage=problem_data_storage, null=True, blank=True, - upload_to=problem_directory_file) - output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True) - output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True) - feedback = models.TextField(verbose_name=_('init.yml generation feedback'), blank=True) - checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True) - checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True, - help_text=_('checker arguments as a JSON object')) - custom_checker = models.FileField(verbose_name=_('custom checker file'), - storage=problem_data_storage, - null=True, - blank=True, - upload_to=problem_directory_file, - validators=[FileExtensionValidator(allowed_extensions=['py'])]) - custom_validator = models.FileField(verbose_name=_('custom validator file'), - storage=problem_data_storage, - null=True, - blank=True, - upload_to=problem_directory_file, - validators=[FileExtensionValidator(allowed_extensions=['cpp'])]) - interactive_judge = models.FileField(verbose_name=_('interactive judge'), - storage=problem_data_storage, - null=True, - blank=True, - upload_to=problem_directory_file, - validators=[FileExtensionValidator(allowed_extensions=['cpp'])]) - + problem = models.OneToOneField( + "Problem", + verbose_name=_("problem"), + related_name="data_files", + on_delete=models.CASCADE, + ) + zipfile = models.FileField( + verbose_name=_("data zip file"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + ) + generator = models.FileField( + verbose_name=_("generator file"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + ) + output_prefix = models.IntegerField( + verbose_name=_("output prefix length"), blank=True, null=True + ) + output_limit = models.IntegerField( + verbose_name=_("output limit length"), blank=True, null=True + ) + feedback = models.TextField( + verbose_name=_("init.yml generation feedback"), blank=True + ) + checker = models.CharField( + max_length=10, verbose_name=_("checker"), choices=CHECKERS, blank=True + ) + checker_args = models.TextField( + verbose_name=_("checker arguments"), + blank=True, + help_text=_("checker arguments as a JSON object"), + ) + custom_checker = models.FileField( + verbose_name=_("custom checker file"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + validators=[FileExtensionValidator(allowed_extensions=["py"])], + ) + custom_validator = models.FileField( + verbose_name=_("custom validator file"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + validators=[FileExtensionValidator(allowed_extensions=["cpp"])], + ) + interactive_judge = models.FileField( + verbose_name=_("interactive judge"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + validators=[FileExtensionValidator(allowed_extensions=["cpp"])], + ) + __original_zipfile = None def __init__(self, *args, **kwargs): @@ -78,10 +115,13 @@ class ProblemData(models.Model): def save(self, *args, **kwargs): # Delete caches if self.__original_zipfile: - try: + try: files = ZipFile(self.__original_zipfile.path).namelist() for file in files: - cache_key = 'problem_archive:%s:%s' % (self.problem.code, get_file_cachekey(file)) + cache_key = "problem_archive:%s:%s" % ( + self.problem.code, + get_file_cachekey(file), + ) cache.delete(cache_key) except BadZipFile: pass @@ -90,7 +130,7 @@ class ProblemData(models.Model): return super(ProblemData, self).save(*args, **kwargs) def has_yml(self): - return problem_data_storage.exists('%s/init.yml' % self.problem.code) + return problem_data_storage.exists("%s/init.yml" % self.problem.code) def _update_code(self, original, new): try: @@ -103,31 +143,60 @@ class ProblemData(models.Model): if self.generator: self.generator.name = _problem_directory_file(new, self.generator.name) if self.custom_checker: - self.custom_checker.name = _problem_directory_file(new, self.custom_checker.name) + self.custom_checker.name = _problem_directory_file( + new, self.custom_checker.name + ) if self.custom_checker: - self.custom_checker.name = _problem_directory_file(new, self.custom_checker.name) + self.custom_checker.name = _problem_directory_file( + new, self.custom_checker.name + ) if self.custom_validator: - self.custom_validator.name = _problem_directory_file(new, self.custom_validator.name) + self.custom_validator.name = _problem_directory_file( + new, self.custom_validator.name + ) self.save() + _update_code.alters_data = True class ProblemTestCase(models.Model): - dataset = models.ForeignKey('Problem', verbose_name=_('problem data set'), related_name='cases', - on_delete=models.CASCADE) - order = models.IntegerField(verbose_name=_('case position')) - type = models.CharField(max_length=1, verbose_name=_('case type'), - choices=(('C', _('Normal case')), - ('S', _('Batch start')), - ('E', _('Batch end'))), - default='C') - input_file = models.CharField(max_length=100, verbose_name=_('input file name'), blank=True) - output_file = models.CharField(max_length=100, verbose_name=_('output file name'), blank=True) - generator_args = models.TextField(verbose_name=_('generator arguments'), blank=True) - points = models.IntegerField(verbose_name=_('point value'), blank=True, null=True) - is_pretest = models.BooleanField(verbose_name=_('case is pretest?')) - output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True) - output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True) - checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True) - checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True, - help_text=_('checker arguments as a JSON object')) + dataset = models.ForeignKey( + "Problem", + verbose_name=_("problem data set"), + related_name="cases", + on_delete=models.CASCADE, + ) + order = models.IntegerField(verbose_name=_("case position")) + type = models.CharField( + max_length=1, + verbose_name=_("case type"), + choices=( + ("C", _("Normal case")), + ("S", _("Batch start")), + ("E", _("Batch end")), + ), + default="C", + ) + input_file = models.CharField( + max_length=100, verbose_name=_("input file name"), blank=True + ) + output_file = models.CharField( + max_length=100, verbose_name=_("output file name"), blank=True + ) + generator_args = models.TextField(verbose_name=_("generator arguments"), blank=True) + points = models.IntegerField(verbose_name=_("point value"), blank=True, null=True) + is_pretest = models.BooleanField(verbose_name=_("case is pretest?")) + output_prefix = models.IntegerField( + verbose_name=_("output prefix length"), blank=True, null=True + ) + output_limit = models.IntegerField( + verbose_name=_("output limit length"), blank=True, null=True + ) + checker = models.CharField( + max_length=10, verbose_name=_("checker"), choices=CHECKERS, blank=True + ) + checker_args = models.TextField( + verbose_name=_("checker arguments"), + blank=True, + help_text=_("checker arguments as a JSON object"), + ) diff --git a/judge/models/profile.py b/judge/models/profile.py index b00dc50..e155a20 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -16,7 +16,7 @@ from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE from judge.models.runtime import Language from judge.ratings import rating_class -__all__ = ['Organization', 'Profile', 'OrganizationRequest', 'Friend'] +__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"] class EncryptedNullCharField(EncryptedCharField): @@ -27,28 +27,65 @@ class EncryptedNullCharField(EncryptedCharField): class Organization(models.Model): - name = models.CharField(max_length=128, verbose_name=_('organization title')) - slug = models.SlugField(max_length=128, verbose_name=_('organization slug'), - help_text=_('Organization name shown in URL')) - short_name = models.CharField(max_length=20, verbose_name=_('short name'), - help_text=_('Displayed beside user name during contests')) - about = models.TextField(verbose_name=_('organization description')) - registrant = models.ForeignKey('Profile', verbose_name=_('registrant'), on_delete=models.CASCADE, - related_name='registrant+', help_text=_('User who registered this organization')) - admins = models.ManyToManyField('Profile', verbose_name=_('administrators'), related_name='admin_of', - help_text=_('Those who can edit this organization')) - creation_date = models.DateTimeField(verbose_name=_('creation date'), auto_now_add=True) - is_open = models.BooleanField(verbose_name=_('is open organization?'), - help_text=_('Allow joining organization'), default=True) - slots = models.IntegerField(verbose_name=_('maximum size'), null=True, blank=True, - help_text=_('Maximum amount of users in this organization, ' - 'only applicable to private organizations')) - access_code = models.CharField(max_length=7, help_text=_('Student access code'), - verbose_name=_('access code'), null=True, blank=True) - logo_override_image = models.CharField(verbose_name=_('Logo override image'), default='', max_length=150, - blank=True, - help_text=_('This image will replace the default site logo for users ' - 'viewing the organization.')) + name = models.CharField(max_length=128, verbose_name=_("organization title")) + slug = models.SlugField( + max_length=128, + verbose_name=_("organization slug"), + help_text=_("Organization name shown in URL"), + ) + short_name = models.CharField( + max_length=20, + verbose_name=_("short name"), + help_text=_("Displayed beside user name during contests"), + ) + about = models.TextField(verbose_name=_("organization description")) + registrant = models.ForeignKey( + "Profile", + verbose_name=_("registrant"), + on_delete=models.CASCADE, + related_name="registrant+", + help_text=_("User who registered this organization"), + ) + admins = models.ManyToManyField( + "Profile", + verbose_name=_("administrators"), + related_name="admin_of", + help_text=_("Those who can edit this organization"), + ) + creation_date = models.DateTimeField( + verbose_name=_("creation date"), auto_now_add=True + ) + is_open = models.BooleanField( + verbose_name=_("is open organization?"), + help_text=_("Allow joining organization"), + default=True, + ) + slots = models.IntegerField( + verbose_name=_("maximum size"), + null=True, + blank=True, + help_text=_( + "Maximum amount of users in this organization, " + "only applicable to private organizations" + ), + ) + access_code = models.CharField( + max_length=7, + help_text=_("Student access code"), + verbose_name=_("access code"), + null=True, + blank=True, + ) + logo_override_image = models.CharField( + verbose_name=_("Logo override image"), + default="", + max_length=150, + blank=True, + help_text=_( + "This image will replace the default site logo for users " + "viewing the organization." + ), + ) def __contains__(self, item): if isinstance(item, int): @@ -56,69 +93,128 @@ class Organization(models.Model): elif isinstance(item, Profile): return self.members.filter(id=item.id).exists() else: - raise TypeError('Organization membership test must be Profile or primany key') + raise TypeError( + "Organization membership test must be Profile or primany key" + ) def __str__(self): return self.name def get_absolute_url(self): - return reverse('organization_home', args=(self.id, self.slug)) + return reverse("organization_home", args=(self.id, self.slug)) def get_users_url(self): - return reverse('organization_users', args=(self.id, self.slug)) + return reverse("organization_users", args=(self.id, self.slug)) class Meta: - ordering = ['name'] + ordering = ["name"] permissions = ( - ('organization_admin', 'Administer organizations'), - ('edit_all_organization', 'Edit all organizations'), + ("organization_admin", "Administer organizations"), + ("edit_all_organization", "Edit all organizations"), ) - verbose_name = _('organization') - verbose_name_plural = _('organizations') + verbose_name = _("organization") + verbose_name_plural = _("organizations") class Profile(models.Model): - user = models.OneToOneField(User, verbose_name=_('user associated'), on_delete=models.CASCADE) - about = models.TextField(verbose_name=_('self-description'), null=True, blank=True) - timezone = models.CharField(max_length=50, verbose_name=_('location'), choices=TIMEZONE, - default=settings.DEFAULT_USER_TIME_ZONE) - language = models.ForeignKey('Language', verbose_name=_('preferred language'), on_delete=models.SET_DEFAULT, - default=Language.get_default_language_pk) + user = models.OneToOneField( + User, verbose_name=_("user associated"), on_delete=models.CASCADE + ) + about = models.TextField(verbose_name=_("self-description"), null=True, blank=True) + timezone = models.CharField( + max_length=50, + verbose_name=_("location"), + choices=TIMEZONE, + default=settings.DEFAULT_USER_TIME_ZONE, + ) + language = models.ForeignKey( + "Language", + verbose_name=_("preferred language"), + on_delete=models.SET_DEFAULT, + default=Language.get_default_language_pk, + ) points = models.FloatField(default=0, db_index=True) performance_points = models.FloatField(default=0, db_index=True) problem_count = models.IntegerField(default=0, db_index=True) - ace_theme = models.CharField(max_length=30, choices=ACE_THEMES, default='github') - last_access = models.DateTimeField(verbose_name=_('last access time'), default=now) - ip = models.GenericIPAddressField(verbose_name=_('last IP'), blank=True, null=True) - organizations = SortedManyToManyField(Organization, verbose_name=_('organization'), blank=True, - related_name='members', related_query_name='member') - display_rank = models.CharField(max_length=10, default='user', verbose_name=_('display rank'), - choices=(('user', 'Normal User'), ('setter', 'Problem Setter'), ('admin', 'Admin'))) - mute = models.BooleanField(verbose_name=_('comment mute'), help_text=_('Some users are at their best when silent.'), - default=False) - is_unlisted = models.BooleanField(verbose_name=_('unlisted user'), help_text=_('User will not be ranked.'), - default=False) + ace_theme = models.CharField(max_length=30, choices=ACE_THEMES, default="github") + last_access = models.DateTimeField(verbose_name=_("last access time"), default=now) + ip = models.GenericIPAddressField(verbose_name=_("last IP"), blank=True, null=True) + organizations = SortedManyToManyField( + Organization, + verbose_name=_("organization"), + blank=True, + related_name="members", + related_query_name="member", + ) + display_rank = models.CharField( + max_length=10, + default="user", + verbose_name=_("display rank"), + choices=( + ("user", "Normal User"), + ("setter", "Problem Setter"), + ("admin", "Admin"), + ), + ) + mute = models.BooleanField( + verbose_name=_("comment mute"), + help_text=_("Some users are at their best when silent."), + default=False, + ) + is_unlisted = models.BooleanField( + verbose_name=_("unlisted user"), + help_text=_("User will not be ranked."), + default=False, + ) is_banned_problem_voting = models.BooleanField( - verbose_name=_('banned from voting'), + verbose_name=_("banned from voting"), help_text=_("User will not be able to vote on problems' point values."), default=False, ) rating = models.IntegerField(null=True, default=None) - user_script = models.TextField(verbose_name=_('user script'), default='', blank=True, max_length=65536, - help_text=_('User-defined JavaScript for site customization.')) - current_contest = models.OneToOneField('ContestParticipation', verbose_name=_('current contest'), - null=True, blank=True, related_name='+', on_delete=models.SET_NULL) - math_engine = models.CharField(verbose_name=_('math engine'), choices=MATH_ENGINES_CHOICES, max_length=4, - default=settings.MATHOID_DEFAULT_TYPE, - help_text=_('the rendering engine used to render math')) - is_totp_enabled = models.BooleanField(verbose_name=_('2FA enabled'), default=False, - help_text=_('check to enable TOTP-based two factor authentication')) - totp_key = EncryptedNullCharField(max_length=32, null=True, blank=True, verbose_name=_('TOTP key'), - help_text=_('32 character base32-encoded key for TOTP'), - validators=[RegexValidator('^$|^[A-Z2-7]{32}$', - _('TOTP key must be empty or base32'))]) - notes = models.TextField(verbose_name=_('internal notes'), null=True, blank=True, - help_text=_('Notes for administrators regarding this user.')) + user_script = models.TextField( + verbose_name=_("user script"), + default="", + blank=True, + max_length=65536, + help_text=_("User-defined JavaScript for site customization."), + ) + current_contest = models.OneToOneField( + "ContestParticipation", + verbose_name=_("current contest"), + null=True, + blank=True, + related_name="+", + on_delete=models.SET_NULL, + ) + math_engine = models.CharField( + verbose_name=_("math engine"), + choices=MATH_ENGINES_CHOICES, + max_length=4, + default=settings.MATHOID_DEFAULT_TYPE, + help_text=_("the rendering engine used to render math"), + ) + is_totp_enabled = models.BooleanField( + verbose_name=_("2FA enabled"), + default=False, + help_text=_("check to enable TOTP-based two factor authentication"), + ) + totp_key = EncryptedNullCharField( + max_length=32, + null=True, + blank=True, + verbose_name=_("TOTP key"), + help_text=_("32 character base32-encoded key for TOTP"), + validators=[ + RegexValidator("^$|^[A-Z2-7]{32}$", _("TOTP key must be empty or base32")) + ], + ) + notes = models.TextField( + verbose_name=_("internal notes"), + null=True, + blank=True, + help_text=_("Notes for administrators regarding this user."), + ) @cached_property def organization(self): @@ -133,33 +229,45 @@ class Profile(models.Model): @cached_property def count_unseen_notifications(self): query = { - 'read': False, + "read": False, } return self.notifications.filter(**query).count() - + _pp_table = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)] def calculate_points(self, table=_pp_table): from judge.models import Problem + public_problems = Problem.get_public_problems() data = ( - public_problems.filter(submission__user=self, submission__points__isnull=False) - .annotate(max_points=Max('submission__points')).order_by('-max_points') - .values_list('max_points', flat=True).filter(max_points__gt=0) + public_problems.filter( + submission__user=self, submission__points__isnull=False + ) + .annotate(max_points=Max("submission__points")) + .order_by("-max_points") + .values_list("max_points", flat=True) + .filter(max_points__gt=0) ) extradata = ( - public_problems.filter(submission__user=self, submission__result='AC').values('id').distinct().count() + public_problems.filter(submission__user=self, submission__result="AC") + .values("id") + .distinct() + .count() ) bonus_function = settings.DMOJ_PP_BONUS_FUNCTION points = sum(data) problems = len(data) entries = min(len(data), len(table)) pp = sum(map(mul, table[:entries], data[:entries])) + bonus_function(extradata) - if self.points != points or problems != self.problem_count or self.performance_points != pp: + if ( + self.points != points + or problems != self.problem_count + or self.performance_points != pp + ): self.points = points self.problem_count = problems self.performance_points = pp - self.save(update_fields=['points', 'problem_count', 'performance_points']) + self.save(update_fields=["points", "problem_count", "performance_points"]) return points calculate_points.alters_data = True @@ -172,9 +280,12 @@ class Profile(models.Model): def update_contest(self): from judge.models import ContestParticipation + try: contest = self.current_contest - if contest is not None and (contest.ended or not contest.contest.is_accessible_by(self.user)): + if contest is not None and ( + contest.ended or not contest.contest.is_accessible_by(self.user) + ): self.remove_contest() except ContestParticipation.DoesNotExist: self.remove_contest() @@ -182,86 +293,105 @@ class Profile(models.Model): update_contest.alters_data = True def get_absolute_url(self): - return reverse('user_page', args=(self.user.username,)) + return reverse("user_page", args=(self.user.username,)) def __str__(self): return self.user.username @classmethod - def get_user_css_class(cls, display_rank, rating, rating_colors=settings.DMOJ_RATING_COLORS): + def get_user_css_class( + cls, display_rank, rating, rating_colors=settings.DMOJ_RATING_COLORS + ): if rating_colors: - return 'rating %s %s' % (rating_class(rating) if rating is not None else 'rate-none', display_rank) + return "rating %s %s" % ( + rating_class(rating) if rating is not None else "rate-none", + display_rank, + ) return display_rank @cached_property def css_class(self): return self.get_user_css_class(self.display_rank, self.rating) - def get_friends(self): #list of usernames, including you + def get_friends(self): # list of usernames, including you friend_obj = self.following_users.all() ret = set() - if (friend_obj): + if friend_obj: ret = set(friend.username for friend in friend_obj[0].users.all()) - + ret.add(self.username) return ret class Meta: permissions = ( - ('test_site', 'Shows in-progress development stuff'), - ('totp', 'Edit TOTP settings'), + ("test_site", "Shows in-progress development stuff"), + ("totp", "Edit TOTP settings"), ) - verbose_name = _('user profile') - verbose_name_plural = _('user profiles') + verbose_name = _("user profile") + verbose_name_plural = _("user profiles") class OrganizationRequest(models.Model): - user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='requests', on_delete=models.CASCADE) - organization = models.ForeignKey(Organization, verbose_name=_('organization'), related_name='requests', - on_delete=models.CASCADE) - time = models.DateTimeField(verbose_name=_('request time'), auto_now_add=True) - state = models.CharField(max_length=1, verbose_name=_('state'), choices=( - ('P', 'Pending'), - ('A', 'Approved'), - ('R', 'Rejected'), - )) - reason = models.TextField(verbose_name=_('reason')) + user = models.ForeignKey( + Profile, + verbose_name=_("user"), + related_name="requests", + on_delete=models.CASCADE, + ) + organization = models.ForeignKey( + Organization, + verbose_name=_("organization"), + related_name="requests", + on_delete=models.CASCADE, + ) + time = models.DateTimeField(verbose_name=_("request time"), auto_now_add=True) + state = models.CharField( + max_length=1, + verbose_name=_("state"), + choices=( + ("P", "Pending"), + ("A", "Approved"), + ("R", "Rejected"), + ), + ) + reason = models.TextField(verbose_name=_("reason")) class Meta: - verbose_name = _('organization join request') - verbose_name_plural = _('organization join requests') + verbose_name = _("organization join request") + verbose_name_plural = _("organization join requests") class Friend(models.Model): users = models.ManyToManyField(Profile) - current_user = models.ForeignKey(Profile, related_name="following_users", on_delete=CASCADE) + current_user = models.ForeignKey( + Profile, related_name="following_users", on_delete=CASCADE + ) @classmethod def is_friend(self, current_user, new_friend): try: - return current_user.following_users.get().users \ - .filter(user=new_friend.user).exists() + return ( + current_user.following_users.get() + .users.filter(user=new_friend.user) + .exists() + ) except: return False @classmethod def make_friend(self, current_user, new_friend): - friend, created = self.objects.get_or_create( - current_user = current_user - ) + friend, created = self.objects.get_or_create(current_user=current_user) friend.users.add(new_friend) @classmethod def remove_friend(self, current_user, new_friend): - friend, created = self.objects.get_or_create( - current_user = current_user - ) + friend, created = self.objects.get_or_create(current_user=current_user) friend.users.remove(new_friend) @classmethod def toggle_friend(self, current_user, new_friend): - if (self.is_friend(current_user, new_friend)): + if self.is_friend(current_user, new_friend): self.remove_friend(current_user, new_friend) else: self.make_friend(current_user, new_friend) diff --git a/judge/models/runtime.py b/judge/models/runtime.py index 16cdd66..99825ec 100644 --- a/judge/models/runtime.py +++ b/judge/models/runtime.py @@ -12,38 +12,82 @@ from django.utils.translation import gettext_lazy as _ from judge.judgeapi import disconnect_judge -__all__ = ['Language', 'RuntimeVersion', 'Judge'] +__all__ = ["Language", "RuntimeVersion", "Judge"] class Language(models.Model): - key = models.CharField(max_length=6, verbose_name=_('short identifier'), - help_text=_('The identifier for this language; the same as its executor id for judges.'), - unique=True) - name = models.CharField(max_length=20, verbose_name=_('long name'), - help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".')) - short_name = models.CharField(max_length=10, verbose_name=_('short name'), - help_text=_('More readable, but short, name to display publicly; e.g. "PY2" or ' - '"C++11". If left blank, it will default to the ' - 'short identifier.'), - null=True, blank=True) - common_name = models.CharField(max_length=10, verbose_name=_('common name'), - help_text=_('Common name for the language. For example, the common name for C++03, ' - 'C++11, and C++14 would be "C++"')) - ace = models.CharField(max_length=20, verbose_name=_('ace mode name'), - help_text=_('Language ID for Ace.js editor highlighting, appended to "mode-" to determine ' - 'the Ace JavaScript file to use, e.g., "python".')) - pygments = models.CharField(max_length=20, verbose_name=_('pygments name'), - help_text=_('Language ID for Pygments highlighting in source windows.')) - template = models.TextField(verbose_name=_('code template'), - help_text=_('Code template to display in submission editor.'), blank=True) - info = models.CharField(max_length=50, verbose_name=_('runtime info override'), blank=True, - help_text=_("Do not set this unless you know what you're doing! It will override the " - "usually more specific, judge-provided runtime info!")) - description = models.TextField(verbose_name=_('language description'), - help_text=_('Use this field to inform users of quirks with your environment, ' - 'additional restrictions, etc.'), blank=True) - extension = models.CharField(max_length=10, verbose_name=_('extension'), - help_text=_('The extension of source files, e.g., "py" or "cpp".')) + key = models.CharField( + max_length=6, + verbose_name=_("short identifier"), + help_text=_( + "The identifier for this language; the same as its executor id for judges." + ), + unique=True, + ) + name = models.CharField( + max_length=20, + verbose_name=_("long name"), + help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".'), + ) + short_name = models.CharField( + max_length=10, + verbose_name=_("short name"), + help_text=_( + 'More readable, but short, name to display publicly; e.g. "PY2" or ' + '"C++11". If left blank, it will default to the ' + "short identifier." + ), + null=True, + blank=True, + ) + common_name = models.CharField( + max_length=10, + verbose_name=_("common name"), + help_text=_( + "Common name for the language. For example, the common name for C++03, " + 'C++11, and C++14 would be "C++"' + ), + ) + ace = models.CharField( + max_length=20, + verbose_name=_("ace mode name"), + help_text=_( + 'Language ID for Ace.js editor highlighting, appended to "mode-" to determine ' + 'the Ace JavaScript file to use, e.g., "python".' + ), + ) + pygments = models.CharField( + max_length=20, + verbose_name=_("pygments name"), + help_text=_("Language ID for Pygments highlighting in source windows."), + ) + template = models.TextField( + verbose_name=_("code template"), + help_text=_("Code template to display in submission editor."), + blank=True, + ) + info = models.CharField( + max_length=50, + verbose_name=_("runtime info override"), + blank=True, + help_text=_( + "Do not set this unless you know what you're doing! It will override the " + "usually more specific, judge-provided runtime info!" + ), + ) + description = models.TextField( + verbose_name=_("language description"), + help_text=_( + "Use this field to inform users of quirks with your environment, " + "additional restrictions, etc." + ), + blank=True, + ) + extension = models.CharField( + max_length=10, + verbose_name=_("extension"), + help_text=_('The extension of source files, e.g., "py" or "cpp".'), + ) def runtime_versions(self): runtimes = OrderedDict() @@ -52,25 +96,29 @@ class Language(models.Model): id = runtime.name if id not in runtimes: runtimes[id] = set() - if not runtime.version: # empty str == error determining version on judge side + if ( + not runtime.version + ): # empty str == error determining version on judge side continue runtimes[id].add(runtime.version) lang_versions = [] for id, version_list in runtimes.items(): - lang_versions.append((id, sorted(version_list, key=lambda a: tuple(map(int, a.split('.')))))) + lang_versions.append( + (id, sorted(version_list, key=lambda a: tuple(map(int, a.split("."))))) + ) return lang_versions @classmethod def get_common_name_map(cls): - result = cache.get('lang:cn_map') + result = cache.get("lang:cn_map") if result is not None: return result result = defaultdict(set) - for id, cn in Language.objects.values_list('id', 'common_name'): + for id, cn in Language.objects.values_list("id", "common_name"): result[cn].add(id) result = {id: cns for id, cns in result.items() if len(cns) > 1} - cache.set('lang:cn_map', result, 86400) + cache.set("lang:cn_map", result, 86400) return result @cached_property @@ -83,17 +131,19 @@ class Language(models.Model): @cached_property def display_name(self): if self.info: - return '%s (%s)' % (self.name, self.info) + return "%s (%s)" % (self.name, self.info) else: return self.name @classmethod def get_python3(cls): # We really need a default language, and this app is in Python 3 - return Language.objects.get_or_create(key='PY3', defaults={'name': 'Python 3'})[0] + return Language.objects.get_or_create(key="PY3", defaults={"name": "Python 3"})[ + 0 + ] def get_absolute_url(self): - return reverse('runtime_list') + '#' + self.key + return reverse("runtime_list") + "#" + self.key @classmethod def get_default_language(cls): @@ -107,36 +157,67 @@ class Language(models.Model): return cls.get_default_language().pk class Meta: - ordering = ['key'] - verbose_name = _('language') - verbose_name_plural = _('languages') + ordering = ["key"] + verbose_name = _("language") + verbose_name_plural = _("languages") class RuntimeVersion(models.Model): - language = models.ForeignKey(Language, verbose_name=_('language to which this runtime belongs'), on_delete=CASCADE) - judge = models.ForeignKey('Judge', verbose_name=_('judge on which this runtime exists'), on_delete=CASCADE) - name = models.CharField(max_length=64, verbose_name=_('runtime name')) - version = models.CharField(max_length=64, verbose_name=_('runtime version'), blank=True) - priority = models.IntegerField(verbose_name=_('order in which to display this runtime'), default=0) + language = models.ForeignKey( + Language, + verbose_name=_("language to which this runtime belongs"), + on_delete=CASCADE, + ) + judge = models.ForeignKey( + "Judge", verbose_name=_("judge on which this runtime exists"), on_delete=CASCADE + ) + name = models.CharField(max_length=64, verbose_name=_("runtime name")) + version = models.CharField( + max_length=64, verbose_name=_("runtime version"), blank=True + ) + priority = models.IntegerField( + verbose_name=_("order in which to display this runtime"), default=0 + ) class Judge(models.Model): - name = models.CharField(max_length=50, help_text=_('Server name, hostname-style'), unique=True) - created = models.DateTimeField(auto_now_add=True, verbose_name=_('time of creation')) - auth_key = models.CharField(max_length=100, help_text=_('A key to authenticate this judge'), - verbose_name=_('authentication key')) - is_blocked = models.BooleanField(verbose_name=_('block judge'), default=False, - help_text=_('Whether this judge should be blocked from connecting, ' - 'even if its key is correct.')) - online = models.BooleanField(verbose_name=_('judge online status'), default=False) - start_time = models.DateTimeField(verbose_name=_('judge start time'), null=True) - ping = models.FloatField(verbose_name=_('response time'), null=True) - load = models.FloatField(verbose_name=_('system load'), null=True, - help_text=_('Load for the last minute, divided by processors to be fair.')) - description = models.TextField(blank=True, verbose_name=_('description')) - last_ip = models.GenericIPAddressField(verbose_name='Last connected IP', blank=True, null=True) - problems = models.ManyToManyField('Problem', verbose_name=_('problems'), related_name='judges') - runtimes = models.ManyToManyField(Language, verbose_name=_('judges'), related_name='judges') + name = models.CharField( + max_length=50, help_text=_("Server name, hostname-style"), unique=True + ) + created = models.DateTimeField( + auto_now_add=True, verbose_name=_("time of creation") + ) + auth_key = models.CharField( + max_length=100, + help_text=_("A key to authenticate this judge"), + verbose_name=_("authentication key"), + ) + is_blocked = models.BooleanField( + verbose_name=_("block judge"), + default=False, + help_text=_( + "Whether this judge should be blocked from connecting, " + "even if its key is correct." + ), + ) + online = models.BooleanField(verbose_name=_("judge online status"), default=False) + start_time = models.DateTimeField(verbose_name=_("judge start time"), null=True) + ping = models.FloatField(verbose_name=_("response time"), null=True) + load = models.FloatField( + verbose_name=_("system load"), + null=True, + help_text=_("Load for the last minute, divided by processors to be fair."), + ) + description = models.TextField(blank=True, verbose_name=_("description")) + last_ip = models.GenericIPAddressField( + verbose_name="Last connected IP", blank=True, null=True + ) + problems = models.ManyToManyField( + "Problem", verbose_name=_("problems"), related_name="judges" + ) + runtimes = models.ManyToManyField( + Language, verbose_name=_("judges"), related_name="judges" + ) def __str__(self): return self.name @@ -148,22 +229,23 @@ class Judge(models.Model): @cached_property def runtime_versions(self): - qs = (self.runtimeversion_set.values('language__key', 'language__name', 'version', 'name') - .order_by('language__key', 'priority')) + qs = self.runtimeversion_set.values( + "language__key", "language__name", "version", "name" + ).order_by("language__key", "priority") ret = OrderedDict() for data in qs: - key = data['language__key'] + key = data["language__key"] if key not in ret: - ret[key] = {'name': data['language__name'], 'runtime': []} - ret[key]['runtime'].append((data['name'], (data['version'],))) + ret[key] = {"name": data["language__name"], "runtime": []} + ret[key]["runtime"].append((data["name"], (data["version"],))) return list(ret.items()) @cached_property def uptime(self): - return timezone.now() - self.start_time if self.online else 'N/A' + return timezone.now() - self.start_time if self.online else "N/A" @cached_property def ping_ms(self): @@ -171,9 +253,9 @@ class Judge(models.Model): @cached_property def runtime_list(self): - return map(attrgetter('name'), self.runtimes.all()) + return map(attrgetter("name"), self.runtimes.all()) class Meta: - ordering = ['name'] - verbose_name = _('judge') - verbose_name_plural = _('judges') + ordering = ["name"] + verbose_name = _("judge") + verbose_name_plural = _("judges") diff --git a/judge/models/submission.py b/judge/models/submission.py index e6c0208..51bb1ee 100644 --- a/judge/models/submission.py +++ b/judge/models/submission.py @@ -14,92 +14,130 @@ from judge.models.profile import Profile from judge.models.runtime import Language from judge.utils.unicode import utf8bytes -__all__ = ['SUBMISSION_RESULT', 'Submission', 'SubmissionSource', 'SubmissionTestCase'] +__all__ = ["SUBMISSION_RESULT", "Submission", "SubmissionSource", "SubmissionTestCase"] SUBMISSION_RESULT = ( - ('AC', _('Accepted')), - ('WA', _('Wrong Answer')), - ('TLE', _('Time Limit Exceeded')), - ('MLE', _('Memory Limit Exceeded')), - ('OLE', _('Output Limit Exceeded')), - ('IR', _('Invalid Return')), - ('RTE', _('Runtime Error')), - ('CE', _('Compile Error')), - ('IE', _('Internal Error')), - ('SC', _('Short circuit')), - ('AB', _('Aborted')), + ("AC", _("Accepted")), + ("WA", _("Wrong Answer")), + ("TLE", _("Time Limit Exceeded")), + ("MLE", _("Memory Limit Exceeded")), + ("OLE", _("Output Limit Exceeded")), + ("IR", _("Invalid Return")), + ("RTE", _("Runtime Error")), + ("CE", _("Compile Error")), + ("IE", _("Internal Error")), + ("SC", _("Short circuit")), + ("AB", _("Aborted")), ) class Submission(models.Model): STATUS = ( - ('QU', _('Queued')), - ('P', _('Processing')), - ('G', _('Grading')), - ('D', _('Completed')), - ('IE', _('Internal Error')), - ('CE', _('Compile Error')), - ('AB', _('Aborted')), + ("QU", _("Queued")), + ("P", _("Processing")), + ("G", _("Grading")), + ("D", _("Completed")), + ("IE", _("Internal Error")), + ("CE", _("Compile Error")), + ("AB", _("Aborted")), ) - IN_PROGRESS_GRADING_STATUS = ('QU', 'P', 'G') + IN_PROGRESS_GRADING_STATUS = ("QU", "P", "G") RESULT = SUBMISSION_RESULT USER_DISPLAY_CODES = { - 'AC': _('Accepted'), - 'WA': _('Wrong Answer'), - 'SC': "Short Circuited", - 'TLE': _('Time Limit Exceeded'), - 'MLE': _('Memory Limit Exceeded'), - 'OLE': _('Output Limit Exceeded'), - 'IR': _('Invalid Return'), - 'RTE': _('Runtime Error'), - 'CE': _('Compile Error'), - 'IE': _('Internal Error (judging server error)'), - 'QU': _('Queued'), - 'P': _('Processing'), - 'G': _('Grading'), - 'D': _('Completed'), - 'AB': _('Aborted'), + "AC": _("Accepted"), + "WA": _("Wrong Answer"), + "SC": "Short Circuited", + "TLE": _("Time Limit Exceeded"), + "MLE": _("Memory Limit Exceeded"), + "OLE": _("Output Limit Exceeded"), + "IR": _("Invalid Return"), + "RTE": _("Runtime Error"), + "CE": _("Compile Error"), + "IE": _("Internal Error (judging server error)"), + "QU": _("Queued"), + "P": _("Processing"), + "G": _("Grading"), + "D": _("Completed"), + "AB": _("Aborted"), } user = models.ForeignKey(Profile, on_delete=models.CASCADE) problem = models.ForeignKey(Problem, on_delete=models.CASCADE) - date = models.DateTimeField(verbose_name=_('submission time'), auto_now_add=True, db_index=True) - time = models.FloatField(verbose_name=_('execution time'), null=True, db_index=True) - memory = models.FloatField(verbose_name=_('memory usage'), null=True) - points = models.FloatField(verbose_name=_('points granted'), null=True, db_index=True) - language = models.ForeignKey(Language, verbose_name=_('submission language'), on_delete=models.CASCADE) - status = models.CharField(verbose_name=_('status'), max_length=2, choices=STATUS, default='QU', db_index=True) - result = models.CharField(verbose_name=_('result'), max_length=3, choices=SUBMISSION_RESULT, - default=None, null=True, blank=True, db_index=True) - error = models.TextField(verbose_name=_('compile errors'), null=True, blank=True) + date = models.DateTimeField( + verbose_name=_("submission time"), auto_now_add=True, db_index=True + ) + time = models.FloatField(verbose_name=_("execution time"), null=True, db_index=True) + memory = models.FloatField(verbose_name=_("memory usage"), null=True) + points = models.FloatField( + verbose_name=_("points granted"), null=True, db_index=True + ) + language = models.ForeignKey( + Language, verbose_name=_("submission language"), on_delete=models.CASCADE + ) + status = models.CharField( + verbose_name=_("status"), + max_length=2, + choices=STATUS, + default="QU", + db_index=True, + ) + result = models.CharField( + verbose_name=_("result"), + max_length=3, + choices=SUBMISSION_RESULT, + default=None, + null=True, + blank=True, + db_index=True, + ) + error = models.TextField(verbose_name=_("compile errors"), null=True, blank=True) current_testcase = models.IntegerField(default=0) - batch = models.BooleanField(verbose_name=_('batched cases'), default=False) - case_points = models.FloatField(verbose_name=_('test case points'), default=0) - case_total = models.FloatField(verbose_name=_('test case total points'), default=0) - judged_on = models.ForeignKey('Judge', verbose_name=_('judged on'), null=True, blank=True, - on_delete=models.SET_NULL) - judged_date = models.DateTimeField(verbose_name=_('submission judge time'), default=None, null=True) - was_rejudged = models.BooleanField(verbose_name=_('was rejudged by admin'), default=False) - is_pretested = models.BooleanField(verbose_name=_('was ran on pretests only'), default=False) - contest_object = models.ForeignKey('Contest', verbose_name=_('contest'), null=True, blank=True, - on_delete=models.SET_NULL, related_name='+') + batch = models.BooleanField(verbose_name=_("batched cases"), default=False) + case_points = models.FloatField(verbose_name=_("test case points"), default=0) + case_total = models.FloatField(verbose_name=_("test case total points"), default=0) + judged_on = models.ForeignKey( + "Judge", + verbose_name=_("judged on"), + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + judged_date = models.DateTimeField( + verbose_name=_("submission judge time"), default=None, null=True + ) + was_rejudged = models.BooleanField( + verbose_name=_("was rejudged by admin"), default=False + ) + is_pretested = models.BooleanField( + verbose_name=_("was ran on pretests only"), default=False + ) + contest_object = models.ForeignKey( + "Contest", + verbose_name=_("contest"), + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) objects = TranslatedProblemForeignKeyQuerySet.as_manager() @classmethod def result_class_from_code(cls, result, case_points, case_total): - if result == 'AC': + if result == "AC": if case_points == case_total: - return 'AC' - return '_AC' + return "AC" + return "_AC" return result @property def result_class(self): # This exists to save all these conditionals from being executed (slowly) in each row.jade template - if self.status in ('IE', 'CE'): + if self.status in ("IE", "CE"): return self.status - return Submission.result_class_from_code(self.result, self.case_points, self.case_total) + return Submission.result_class_from_code( + self.result, self.case_points, self.case_total + ) @property def memory_bytes(self): @@ -111,12 +149,11 @@ class Submission(models.Model): @property def long_status(self): - return Submission.USER_DISPLAY_CODES.get(self.short_status, '') + return Submission.USER_DISPLAY_CODES.get(self.short_status, "") def judge(self, *args, **kwargs): judge_submission(self, *args, **kwargs) - judge.alters_data = True def abort(self): @@ -131,8 +168,12 @@ class Submission(models.Model): return contest_problem = contest.problem - contest.points = round(self.case_points / self.case_total * contest_problem.points - if self.case_total > 0 else 0, 3) + contest.points = round( + self.case_points / self.case_total * contest_problem.points + if self.case_total > 0 + else 0, + 3, + ) if not contest_problem.partial and contest.points != contest_problem.points: contest.points = 0 contest.save() @@ -142,18 +183,22 @@ class Submission(models.Model): @property def is_graded(self): - return self.status not in ('QU', 'P', 'G') + return self.status not in ("QU", "P", "G") @cached_property def contest_key(self): - if hasattr(self, 'contest'): + if hasattr(self, "contest"): return self.contest_object.key def __str__(self): - return 'Submission %d of %s by %s' % (self.id, self.problem, self.user.user.username) + return "Submission %d of %s by %s" % ( + self.id, + self.problem, + self.user.user.username, + ) def get_absolute_url(self): - return reverse('submission_status', args=(self.id,)) + return reverse("submission_status", args=(self.id,)) @cached_property def contest_or_none(self): @@ -164,8 +209,14 @@ class Submission(models.Model): @classmethod def get_id_secret(cls, sub_id): - return (hmac.new(utf8bytes(settings.EVENT_DAEMON_SUBMISSION_KEY), b'%d' % sub_id, hashlib.sha512) - .hexdigest()[:16] + '%08x' % sub_id) + return ( + hmac.new( + utf8bytes(settings.EVENT_DAEMON_SUBMISSION_KEY), + b"%d" % sub_id, + hashlib.sha512, + ).hexdigest()[:16] + + "%08x" % sub_id + ) @cached_property def id_secret(self): @@ -173,47 +224,61 @@ class Submission(models.Model): class Meta: permissions = ( - ('abort_any_submission', 'Abort any submission'), - ('rejudge_submission', 'Rejudge the submission'), - ('rejudge_submission_lot', 'Rejudge a lot of submissions'), - ('spam_submission', 'Submit without limit'), - ('view_all_submission', 'View all submission'), - ('resubmit_other', "Resubmit others' submission"), + ("abort_any_submission", "Abort any submission"), + ("rejudge_submission", "Rejudge the submission"), + ("rejudge_submission_lot", "Rejudge a lot of submissions"), + ("spam_submission", "Submit without limit"), + ("view_all_submission", "View all submission"), + ("resubmit_other", "Resubmit others' submission"), ) - verbose_name = _('submission') - verbose_name_plural = _('submissions') + verbose_name = _("submission") + verbose_name_plural = _("submissions") class SubmissionSource(models.Model): - submission = models.OneToOneField(Submission, on_delete=models.CASCADE, verbose_name=_('associated submission'), - related_name='source') - source = models.TextField(verbose_name=_('source code'), max_length=65536) + submission = models.OneToOneField( + Submission, + on_delete=models.CASCADE, + verbose_name=_("associated submission"), + related_name="source", + ) + source = models.TextField(verbose_name=_("source code"), max_length=65536) def __str__(self): - return 'Source of %s' % self.submission + return "Source of %s" % self.submission class SubmissionTestCase(models.Model): RESULT = SUBMISSION_RESULT - submission = models.ForeignKey(Submission, verbose_name=_('associated submission'), - related_name='test_cases', on_delete=models.CASCADE) - case = models.IntegerField(verbose_name=_('test case ID')) - status = models.CharField(max_length=3, verbose_name=_('status flag'), choices=SUBMISSION_RESULT) - time = models.FloatField(verbose_name=_('execution time'), null=True) - memory = models.FloatField(verbose_name=_('memory usage'), null=True) - points = models.FloatField(verbose_name=_('points granted'), null=True) - total = models.FloatField(verbose_name=_('points possible'), null=True) - batch = models.IntegerField(verbose_name=_('batch number'), null=True) - feedback = models.CharField(max_length=50, verbose_name=_('judging feedback'), blank=True) - extended_feedback = models.TextField(verbose_name=_('extended judging feedback'), blank=True) - output = models.TextField(verbose_name=_('program output'), blank=True) + submission = models.ForeignKey( + Submission, + verbose_name=_("associated submission"), + related_name="test_cases", + on_delete=models.CASCADE, + ) + case = models.IntegerField(verbose_name=_("test case ID")) + status = models.CharField( + max_length=3, verbose_name=_("status flag"), choices=SUBMISSION_RESULT + ) + time = models.FloatField(verbose_name=_("execution time"), null=True) + memory = models.FloatField(verbose_name=_("memory usage"), null=True) + points = models.FloatField(verbose_name=_("points granted"), null=True) + total = models.FloatField(verbose_name=_("points possible"), null=True) + batch = models.IntegerField(verbose_name=_("batch number"), null=True) + feedback = models.CharField( + max_length=50, verbose_name=_("judging feedback"), blank=True + ) + extended_feedback = models.TextField( + verbose_name=_("extended judging feedback"), blank=True + ) + output = models.TextField(verbose_name=_("program output"), blank=True) @property def long_status(self): - return Submission.USER_DISPLAY_CODES.get(self.status, '') + return Submission.USER_DISPLAY_CODES.get(self.status, "") class Meta: - unique_together = ('submission', 'case') - verbose_name = _('submission test case') - verbose_name_plural = _('submission test cases') + unique_together = ("submission", "case") + verbose_name = _("submission test case") + verbose_name_plural = _("submission test cases") diff --git a/judge/models/ticket.py b/judge/models/ticket.py index 9b3fc54..27d6fb4 100644 --- a/judge/models/ticket.py +++ b/judge/models/ticket.py @@ -7,24 +7,43 @@ from judge.models.profile import Profile class Ticket(models.Model): - title = models.CharField(max_length=100, verbose_name=_('ticket title')) - user = models.ForeignKey(Profile, verbose_name=_('ticket creator'), related_name='tickets', - on_delete=models.CASCADE) - time = models.DateTimeField(verbose_name=_('creation time'), auto_now_add=True) - assignees = models.ManyToManyField(Profile, verbose_name=_('assignees'), related_name='assigned_tickets') - notes = models.TextField(verbose_name=_('quick notes'), blank=True, - help_text=_('Staff notes for this issue to aid in processing.')) - content_type = models.ForeignKey(ContentType, verbose_name=_('linked item type'), - on_delete=models.CASCADE) - object_id = models.PositiveIntegerField(verbose_name=_('linked item ID')) + title = models.CharField(max_length=100, verbose_name=_("ticket title")) + user = models.ForeignKey( + Profile, + verbose_name=_("ticket creator"), + related_name="tickets", + on_delete=models.CASCADE, + ) + time = models.DateTimeField(verbose_name=_("creation time"), auto_now_add=True) + assignees = models.ManyToManyField( + Profile, verbose_name=_("assignees"), related_name="assigned_tickets" + ) + notes = models.TextField( + verbose_name=_("quick notes"), + blank=True, + help_text=_("Staff notes for this issue to aid in processing."), + ) + content_type = models.ForeignKey( + ContentType, verbose_name=_("linked item type"), on_delete=models.CASCADE + ) + object_id = models.PositiveIntegerField(verbose_name=_("linked item ID")) linked_item = GenericForeignKey() - is_open = models.BooleanField(verbose_name=_('is ticket open?'), default=True) + is_open = models.BooleanField(verbose_name=_("is ticket open?"), default=True) class TicketMessage(models.Model): - ticket = models.ForeignKey(Ticket, verbose_name=_('ticket'), related_name='messages', - related_query_name='message', on_delete=models.CASCADE) - user = models.ForeignKey(Profile, verbose_name=_('poster'), related_name='ticket_messages', - on_delete=models.CASCADE) - body = models.TextField(verbose_name=_('message body')) - time = models.DateTimeField(verbose_name=_('message time'), auto_now_add=True) + ticket = models.ForeignKey( + Ticket, + verbose_name=_("ticket"), + related_name="messages", + related_query_name="message", + on_delete=models.CASCADE, + ) + user = models.ForeignKey( + Profile, + verbose_name=_("poster"), + related_name="ticket_messages", + on_delete=models.CASCADE, + ) + body = models.TextField(verbose_name=_("message body")) + time = models.DateTimeField(verbose_name=_("message time"), auto_now_add=True) diff --git a/judge/models/volunteer.py b/judge/models/volunteer.py index 90c351c..5d3babd 100644 --- a/judge/models/volunteer.py +++ b/judge/models/volunteer.py @@ -4,25 +4,36 @@ from django.utils.translation import gettext_lazy as _ from judge.models import Profile, Problem, ProblemType -__all__ = ['VolunteerProblemVote'] +__all__ = ["VolunteerProblemVote"] + class VolunteerProblemVote(models.Model): - voter = models.ForeignKey(Profile, related_name='volunteer_problem_votes', on_delete=CASCADE) - problem = models.ForeignKey(Problem, related_name='volunteer_user_votes', on_delete=CASCADE) + voter = models.ForeignKey( + Profile, related_name="volunteer_problem_votes", on_delete=CASCADE + ) + problem = models.ForeignKey( + Problem, related_name="volunteer_user_votes", on_delete=CASCADE + ) time = models.DateTimeField(auto_now_add=True) - knowledge_points = models.PositiveIntegerField(verbose_name=_('knowledge points'), - help_text=_('Points awarded by knowledge difficulty')) - thinking_points = models.PositiveIntegerField(verbose_name=_('thinking points'), - help_text=_('Points awarded by thinking difficulty')) - types = models.ManyToManyField(ProblemType, verbose_name=_('problem types'), - help_text=_('The type of problem, ' - "as shown on the problem's page.")) - feedback = models.TextField(verbose_name=_('feedback'), blank=True) + knowledge_points = models.PositiveIntegerField( + verbose_name=_("knowledge points"), + help_text=_("Points awarded by knowledge difficulty"), + ) + thinking_points = models.PositiveIntegerField( + verbose_name=_("thinking points"), + help_text=_("Points awarded by thinking difficulty"), + ) + types = models.ManyToManyField( + ProblemType, + verbose_name=_("problem types"), + help_text=_("The type of problem, " "as shown on the problem's page."), + ) + feedback = models.TextField(verbose_name=_("feedback"), blank=True) class Meta: - verbose_name = _('volunteer vote') - verbose_name_plural = _('volunteer votes') - unique_together = ['voter', 'problem'] + verbose_name = _("volunteer vote") + verbose_name_plural = _("volunteer votes") + unique_together = ["voter", "problem"] def __str__(self): - return f'{self.voter} for {self.problem.code}' + return f"{self.voter} for {self.problem.code}" diff --git a/judge/pdf_problems.py b/judge/pdf_problems.py index a5b318f..4cd4a14 100644 --- a/judge/pdf_problems.py +++ b/judge/pdf_problems.py @@ -11,7 +11,7 @@ import uuid from django.conf import settings from django.utils.translation import gettext -logger = logging.getLogger('judge.problem.pdf') +logger = logging.getLogger("judge.problem.pdf") HAS_SELENIUM = False if settings.USE_SELENIUM: @@ -21,9 +21,10 @@ if settings.USE_SELENIUM: from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait + HAS_SELENIUM = True except ImportError: - logger.warning('Failed to import Selenium', exc_info=True) + logger.warning("Failed to import Selenium", exc_info=True) HAS_PHANTOMJS = os.access(settings.PHANTOMJS, os.X_OK) HAS_SLIMERJS = os.access(settings.SLIMERJS, os.X_OK) @@ -32,27 +33,30 @@ NODE_PATH = settings.NODEJS PUPPETEER_MODULE = settings.PUPPETEER_MODULE HAS_PUPPETEER = os.access(NODE_PATH, os.X_OK) and os.path.isdir(PUPPETEER_MODULE) -HAS_PDF = (os.path.isdir(settings.DMOJ_PDF_PROBLEM_CACHE) and - (HAS_PHANTOMJS or HAS_SLIMERJS or HAS_PUPPETEER or HAS_SELENIUM)) +HAS_PDF = os.path.isdir(settings.DMOJ_PDF_PROBLEM_CACHE) and ( + HAS_PHANTOMJS or HAS_SLIMERJS or HAS_PUPPETEER or HAS_SELENIUM +) EXIFTOOL = settings.EXIFTOOL HAS_EXIFTOOL = os.access(EXIFTOOL, os.X_OK) class BasePdfMaker(object): - math_engine = 'jax' + math_engine = "jax" title = None def __init__(self, dir=None, clean_up=True): - self.dir = dir or os.path.join(settings.DMOJ_PDF_PROBLEM_TEMP_DIR, str(uuid.uuid1())) + self.dir = dir or os.path.join( + settings.DMOJ_PDF_PROBLEM_TEMP_DIR, str(uuid.uuid1()) + ) self.proc = None self.log = None - self.htmlfile = os.path.join(self.dir, 'input.html') - self.pdffile = os.path.join(self.dir, 'output.pdf') + self.htmlfile = os.path.join(self.dir, "input.html") + self.pdffile = os.path.join(self.dir, "output.pdf") self.clean_up = clean_up def load(self, file, source): - with open(os.path.join(self.dir, file), 'w') as target, open(source) as source: + with open(os.path.join(self.dir, file), "w") as target, open(source) as source: target.write(source.read()) def make(self, debug=False): @@ -60,21 +64,27 @@ class BasePdfMaker(object): if self.title and HAS_EXIFTOOL: try: - subprocess.check_output([EXIFTOOL, '-Title=%s' % (self.title,), self.pdffile]) + subprocess.check_output( + [EXIFTOOL, "-Title=%s" % (self.title,), self.pdffile] + ) except subprocess.CalledProcessError as e: - logger.error('Failed to run exiftool to set title for: %s\n%s', self.title, e.output) + logger.error( + "Failed to run exiftool to set title for: %s\n%s", + self.title, + e.output, + ) def _make(self, debug): raise NotImplementedError() @property def html(self): - with io.open(self.htmlfile, encoding='utf-8') as f: + with io.open(self.htmlfile, encoding="utf-8") as f: return f.read() @html.setter def html(self, data): - with io.open(self.htmlfile, 'w', encoding='utf-8') as f: + with io.open(self.htmlfile, "w", encoding="utf-8") as f: f.write(data) @property @@ -99,7 +109,7 @@ class BasePdfMaker(object): class PhantomJSPdfMaker(BasePdfMaker): - template = '''\ + template = """\ "use strict"; var page = require('webpage').create(); var param = {params}; @@ -136,29 +146,37 @@ page.open(param.input, function (status) { }, param.timeout); } }); -''' +""" def get_render_script(self): - return self.template.replace('{params}', json.dumps({ - 'zoom': settings.PHANTOMJS_PDF_ZOOM, - 'timeout': int(settings.PHANTOMJS_PDF_TIMEOUT * 1000), - 'input': 'input.html', 'output': 'output.pdf', - 'paper': settings.PHANTOMJS_PAPER_SIZE, - 'footer': gettext('Page [page] of [topage]'), - })) + return self.template.replace( + "{params}", + json.dumps( + { + "zoom": settings.PHANTOMJS_PDF_ZOOM, + "timeout": int(settings.PHANTOMJS_PDF_TIMEOUT * 1000), + "input": "input.html", + "output": "output.pdf", + "paper": settings.PHANTOMJS_PAPER_SIZE, + "footer": gettext("Page [page] of [topage]"), + } + ), + ) def _make(self, debug): - with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f: + with io.open(os.path.join(self.dir, "_render.js"), "w", encoding="utf-8") as f: f.write(self.get_render_script()) - cmdline = [settings.PHANTOMJS, '_render.js'] - self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir) + cmdline = [settings.PHANTOMJS, "_render.js"] + self.proc = subprocess.Popen( + cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir + ) self.log = self.proc.communicate()[0] class SlimerJSPdfMaker(BasePdfMaker): - math_engine = 'mml' + math_engine = "mml" - template = '''\ + template = """\ "use strict"; try { var param = {params}; @@ -189,33 +207,47 @@ try { console.error(e); slimer.exit(1); } -''' +""" def get_render_script(self): - return self.template.replace('{params}', json.dumps({ - 'zoom': settings.SLIMERJS_PDF_ZOOM, - 'input': 'input.html', 'output': 'output.pdf', - 'paper': settings.SLIMERJS_PAPER_SIZE, - 'footer': gettext('Page [page] of [topage]').replace('[page]', '&P').replace('[topage]', '&L'), - })) + return self.template.replace( + "{params}", + json.dumps( + { + "zoom": settings.SLIMERJS_PDF_ZOOM, + "input": "input.html", + "output": "output.pdf", + "paper": settings.SLIMERJS_PAPER_SIZE, + "footer": gettext("Page [page] of [topage]") + .replace("[page]", "&P") + .replace("[topage]", "&L"), + } + ), + ) def _make(self, debug): - with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f: + with io.open(os.path.join(self.dir, "_render.js"), "w", encoding="utf-8") as f: f.write(self.get_render_script()) env = None firefox = settings.SLIMERJS_FIREFOX_PATH if firefox: env = os.environ.copy() - env['SLIMERJSLAUNCHER'] = firefox + env["SLIMERJSLAUNCHER"] = firefox - cmdline = [settings.SLIMERJS, '--headless', '_render.js'] - self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env) + cmdline = [settings.SLIMERJS, "--headless", "_render.js"] + self.proc = subprocess.Popen( + cmdline, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=self.dir, + env=env, + ) self.log = self.proc.communicate()[0] class PuppeteerPDFRender(BasePdfMaker): - template = '''\ + template = """\ "use strict"; const param = {params}; const puppeteer = require('puppeteer'); @@ -249,67 +281,81 @@ puppeteer.launch().then(browser => Promise.resolve() console.error(e); process.exit(1); }); -''' +""" def get_render_script(self): - return self.template.replace('{params}', json.dumps({ - 'input': 'file://%s' % self.htmlfile, - 'output': self.pdffile, - 'paper': settings.PUPPETEER_PAPER_SIZE, - 'footer': gettext('Page [page] of [topage]'), - })) + return self.template.replace( + "{params}", + json.dumps( + { + "input": "file://%s" % self.htmlfile, + "output": self.pdffile, + "paper": settings.PUPPETEER_PAPER_SIZE, + "footer": gettext("Page [page] of [topage]"), + } + ), + ) def _make(self, debug): - with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f: + with io.open(os.path.join(self.dir, "_render.js"), "w", encoding="utf-8") as f: f.write(self.get_render_script()) env = os.environ.copy() - env['NODE_PATH'] = os.path.dirname(PUPPETEER_MODULE) + env["NODE_PATH"] = os.path.dirname(PUPPETEER_MODULE) - cmdline = [NODE_PATH, '_render.js'] - self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env) + cmdline = [NODE_PATH, "_render.js"] + self.proc = subprocess.Popen( + cmdline, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=self.dir, + env=env, + ) self.log = self.proc.communicate()[0] + class SeleniumPDFRender(BasePdfMaker): success = False template = { - 'printBackground': True, - 'displayHeaderFooter': True, - 'headerTemplate': '
', - 'footerTemplate': '
' + - gettext('Page %s of %s') % - ('', '') + - '
', + "printBackground": True, + "displayHeaderFooter": True, + "headerTemplate": "
", + "footerTemplate": '
' + + gettext("Page %s of %s") + % ('', '') + + "
", } def get_log(self, driver): - return '\n'.join(map(str, driver.get_log('driver') + driver.get_log('browser'))) + return "\n".join(map(str, driver.get_log("driver") + driver.get_log("browser"))) def _make(self, debug): options = webdriver.ChromeOptions() options.add_argument("--headless") - options.add_argument("--no-sandbox") # for root + options.add_argument("--no-sandbox") # for root options.binary_location = settings.SELENIUM_CUSTOM_CHROME_PATH browser = webdriver.Chrome(settings.SELENIUM_CHROMEDRIVER_PATH, options=options) - browser.get('file://%s' % self.htmlfile) + browser.get("file://%s" % self.htmlfile) self.log = self.get_log(browser) try: - WebDriverWait(browser, 15).until(EC.presence_of_element_located((By.CLASS_NAME, 'math-loaded'))) + WebDriverWait(browser, 15).until( + EC.presence_of_element_located((By.CLASS_NAME, "math-loaded")) + ) except TimeoutException: - logger.error('PDF math rendering timed out') - self.log = self.get_log(browser) + '\nPDF math rendering timed out' + logger.error("PDF math rendering timed out") + self.log = self.get_log(browser) + "\nPDF math rendering timed out" browser.quit() return - response = browser.execute_cdp_cmd('Page.printToPDF', self.template) + response = browser.execute_cdp_cmd("Page.printToPDF", self.template) self.log = self.get_log(browser) if not response: browser.quit() return - with open(self.pdffile, 'wb') as f: - f.write(base64.b64decode(response['data'])) + with open(self.pdffile, "wb") as f: + f.write(base64.b64decode(response["data"])) self.success = True browser.quit() diff --git a/judge/performance_points.py b/judge/performance_points.py index eee3c10..63a7636 100644 --- a/judge/performance_points.py +++ b/judge/performance_points.py @@ -6,16 +6,22 @@ from django.db import connection from judge.models import Submission from judge.timezone import from_database_time -PP_WEIGHT_TABLE = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)] +PP_WEIGHT_TABLE = [ + pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES) +] -PPBreakdown = namedtuple('PPBreakdown', 'points weight scaled_points problem_name problem_code ' - 'sub_id sub_date sub_points sub_total sub_result_class ' - 'sub_short_status sub_long_status sub_lang') +PPBreakdown = namedtuple( + "PPBreakdown", + "points weight scaled_points problem_name problem_code " + "sub_id sub_date sub_points sub_total sub_result_class " + "sub_short_status sub_long_status sub_lang", +) def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES): with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT max_points_table.problem_code, max_points_table.problem_name, max_points_table.max_points, @@ -47,32 +53,49 @@ def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES): GROUP BY max_points_table.problem_id ORDER BY max_points DESC, judge_submission.date DESC LIMIT %s OFFSET %s - ''', (user.id, user.id, end - start + 1, start)) + """, + (user.id, user.id, end - start + 1, start), + ) data = cursor.fetchall() breakdown = [] for weight, contrib in zip(PP_WEIGHT_TABLE[start:end], data): - code, name, points, id, date, case_points, case_total, result, lang_short_name, lang_key = contrib + ( + code, + name, + points, + id, + date, + case_points, + case_total, + result, + lang_short_name, + lang_key, + ) = contrib # Replicates a lot of the logic usually done on Submission objects lang_short_display_name = lang_short_name or lang_key - result_class = Submission.result_class_from_code(result, case_points, case_total) - long_status = Submission.USER_DISPLAY_CODES.get(result, '') + result_class = Submission.result_class_from_code( + result, case_points, case_total + ) + long_status = Submission.USER_DISPLAY_CODES.get(result, "") - breakdown.append(PPBreakdown( - points=points, - weight=weight * 100, - scaled_points=points * weight, - problem_name=name, - problem_code=code, - sub_id=id, - sub_date=from_database_time(date), - sub_points=case_points, - sub_total=case_total, - sub_short_status=result, - sub_long_status=long_status, - sub_result_class=result_class, - sub_lang=lang_short_display_name, - )) + breakdown.append( + PPBreakdown( + points=points, + weight=weight * 100, + scaled_points=points * weight, + problem_name=name, + problem_code=code, + sub_id=id, + sub_date=from_database_time(date), + sub_points=case_points, + sub_total=case_total, + sub_short_status=result, + sub_long_status=long_status, + sub_result_class=result_class, + sub_lang=lang_short_display_name, + ) + ) has_more = end < min(len(PP_WEIGHT_TABLE), start + len(data)) return breakdown, has_more diff --git a/judge/ratings.py b/judge/ratings.py index 9d0f36d..de2c313 100644 --- a/judge/ratings.py +++ b/judge/ratings.py @@ -8,19 +8,21 @@ from django.db.models.functions import Coalesce from django.utils import timezone -BETA2 = 328.33 ** 2 -RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling -MEAN_INIT = 1400. +BETA2 = 328.33**2 +RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling +MEAN_INIT = 1400.0 VAR_INIT = 250**2 * (BETA2 / 212**2) SD_INIT = sqrt(VAR_INIT) VALID_RANGE = MEAN_INIT - 20 * SD_INIT, MEAN_INIT + 20 * SD_INIT VAR_PER_CONTEST = 1219.047619 * (BETA2 / 212**2) -VAR_LIM = (sqrt(VAR_PER_CONTEST**2 + 4 * BETA2 * VAR_PER_CONTEST) - VAR_PER_CONTEST) / 2 +VAR_LIM = ( + sqrt(VAR_PER_CONTEST**2 + 4 * BETA2 * VAR_PER_CONTEST) - VAR_PER_CONTEST +) / 2 SD_LIM = sqrt(VAR_LIM) TANH_C = sqrt(3) / pi -def tie_ranker(iterable, key=attrgetter('points')): +def tie_ranker(iterable, key=attrgetter("points")): rank = 0 delta = 1 last = None @@ -71,15 +73,15 @@ def solve(tanh_terms, y_tg, lin_factor=0, bounds=VALID_RANGE): def get_var(times_ranked, cache=[VAR_INIT]): while times_ranked >= len(cache): - next_var = 1. / (1. / (cache[-1] + VAR_PER_CONTEST) + 1. / BETA2) + next_var = 1.0 / (1.0 / (cache[-1] + VAR_PER_CONTEST) + 1.0 / BETA2) cache.append(next_var) return cache[times_ranked] def recalculate_ratings(ranking, old_mean, times_ranked, historical_p): n = len(ranking) - new_p = [0.] * n - new_mean = [0.] * n + new_p = [0.0] * n + new_mean = [0.0] * n # Note: pre-multiply delta by TANH_C to improve efficiency. delta = [TANH_C * sqrt(get_var(t) + VAR_PER_CONTEST + BETA2) for t in times_ranked] @@ -90,10 +92,10 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p): r = ranking[i] y_tg = 0 for d, s in zip(delta, ranking): - if s > r: # s loses to r - y_tg += 1. / d - elif s < r: # s beats r - y_tg -= 1. / d + if s > r: # s loses to r + y_tg += 1.0 / d + elif s < r: # s beats r + y_tg -= 1.0 / d # Otherwise, this is a tie that counts as half a win, as per Elo-MMR. new_p[i] = solve(p_tanh_terms, y_tg, bounds=bounds) @@ -117,10 +119,10 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p): # Calculate mean. for i, r in enumerate(ranking): tanh_terms = [] - w_prev = 1. - w_sum = 0. + w_prev = 1.0 + w_sum = 0.0 for j, h in enumerate([new_p[i]] + historical_p[i]): - gamma2 = (VAR_PER_CONTEST if j > 0 else 0) + gamma2 = VAR_PER_CONTEST if j > 0 else 0 h_var = get_var(times_ranked[i] + 1 - j) k = h_var / (h_var + gamma2) w = w_prev * k**2 @@ -128,13 +130,16 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p): tanh_terms.append((h, sqrt(BETA2) * TANH_C, w)) w_prev = w w_sum += w / BETA2 - w0 = 1. / get_var(times_ranked[i] + 1) - w_sum + w0 = 1.0 / get_var(times_ranked[i] + 1) - w_sum p0 = eval_tanhs(tanh_terms[1:], old_mean[i]) / w0 + old_mean[i] new_mean[i] = solve(tanh_terms, w0 * p0, lin_factor=w0) # Display a slightly lower rating to incentivize participation. # As times_ranked increases, new_rating converges to new_mean. - new_rating = [max(1, round(m - (sqrt(get_var(t + 1)) - SD_LIM))) for m, t in zip(new_mean, times_ranked)] + new_rating = [ + max(1, round(m - (sqrt(get_var(t + 1)) - SD_LIM))) + for m, t in zip(new_mean, times_ranked) + ] return new_rating, new_mean, new_p @@ -142,17 +147,39 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p): def rate_contest(contest): from judge.models import Rating, Profile - rating_subquery = Rating.objects.filter(user=OuterRef('user')) - rating_sorted = rating_subquery.order_by('-contest__end_time') - users = contest.users.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker') \ - .annotate(submissions=Count('submission'), - last_rating=Coalesce(Subquery(rating_sorted.values('rating')[:1]), RATING_INIT), - last_mean=Coalesce(Subquery(rating_sorted.values('mean')[:1]), MEAN_INIT), - times=Coalesce(Subquery(rating_subquery.order_by().values('user_id') - .annotate(count=Count('id')).values('count')), 0)) \ - .exclude(user_id__in=contest.rate_exclude.all()) \ - .filter(virtual=0).values('id', 'user_id', 'score', 'cumtime', 'tiebreaker', - 'last_rating', 'last_mean', 'times') + rating_subquery = Rating.objects.filter(user=OuterRef("user")) + rating_sorted = rating_subquery.order_by("-contest__end_time") + users = ( + contest.users.order_by("is_disqualified", "-score", "cumtime", "tiebreaker") + .annotate( + submissions=Count("submission"), + last_rating=Coalesce( + Subquery(rating_sorted.values("rating")[:1]), RATING_INIT + ), + last_mean=Coalesce(Subquery(rating_sorted.values("mean")[:1]), MEAN_INIT), + times=Coalesce( + Subquery( + rating_subquery.order_by() + .values("user_id") + .annotate(count=Count("id")) + .values("count") + ), + 0, + ), + ) + .exclude(user_id__in=contest.rate_exclude.all()) + .filter(virtual=0) + .values( + "id", + "user_id", + "score", + "cumtime", + "tiebreaker", + "last_rating", + "last_mean", + "times", + ) + ) if not contest.rate_all: users = users.filter(submissions__gt=0) if contest.rating_floor is not None: @@ -161,38 +188,76 @@ def rate_contest(contest): users = users.exclude(last_rating__gt=contest.rating_ceiling) users = list(users) - participation_ids = list(map(itemgetter('id'), users)) - user_ids = list(map(itemgetter('user_id'), users)) - ranking = list(tie_ranker(users, key=itemgetter('score', 'cumtime', 'tiebreaker'))) - old_mean = list(map(itemgetter('last_mean'), users)) - times_ranked = list(map(itemgetter('times'), users)) + participation_ids = list(map(itemgetter("id"), users)) + user_ids = list(map(itemgetter("user_id"), users)) + ranking = list(tie_ranker(users, key=itemgetter("score", "cumtime", "tiebreaker"))) + old_mean = list(map(itemgetter("last_mean"), users)) + times_ranked = list(map(itemgetter("times"), users)) historical_p = [[] for _ in users] user_id_to_idx = {uid: i for i, uid in enumerate(user_ids)} - for h in Rating.objects.filter(user_id__in=user_ids) \ - .order_by('-contest__end_time') \ - .values('user_id', 'performance'): - idx = user_id_to_idx[h['user_id']] - historical_p[idx].append(h['performance']) + for h in ( + Rating.objects.filter(user_id__in=user_ids) + .order_by("-contest__end_time") + .values("user_id", "performance") + ): + idx = user_id_to_idx[h["user_id"]] + historical_p[idx].append(h["performance"]) - rating, mean, performance = recalculate_ratings(ranking, old_mean, times_ranked, historical_p) + rating, mean, performance = recalculate_ratings( + ranking, old_mean, times_ranked, historical_p + ) now = timezone.now() - ratings = [Rating(user_id=i, contest=contest, rating=r, mean=m, performance=perf, - last_rated=now, participation_id=pid, rank=z) - for i, pid, r, m, perf, z in zip(user_ids, participation_ids, rating, mean, performance, ranking)] + ratings = [ + Rating( + user_id=i, + contest=contest, + rating=r, + mean=m, + performance=perf, + last_rated=now, + participation_id=pid, + rank=z, + ) + for i, pid, r, m, perf, z in zip( + user_ids, participation_ids, rating, mean, performance, ranking + ) + ] with transaction.atomic(): Rating.objects.bulk_create(ratings) - Profile.objects.filter(contest_history__contest=contest, contest_history__virtual=0).update( - rating=Subquery(Rating.objects.filter(user=OuterRef('id')) - .order_by('-contest__end_time').values('rating')[:1])) + Profile.objects.filter( + contest_history__contest=contest, contest_history__virtual=0 + ).update( + rating=Subquery( + Rating.objects.filter(user=OuterRef("id")) + .order_by("-contest__end_time") + .values("rating")[:1] + ) + ) -RATING_LEVELS = ['Newbie', 'Amateur', 'Expert', 'Candidate Master', 'Master', 'Grandmaster', 'Target'] +RATING_LEVELS = [ + "Newbie", + "Amateur", + "Expert", + "Candidate Master", + "Master", + "Grandmaster", + "Target", +] RATING_VALUES = [1000, 1400, 1700, 1900, 2100, 2400, 3000] -RATING_CLASS = ['rate-newbie', 'rate-amateur', 'rate-specialist', 'rate-expert', 'rate-candidate-master', - 'rate-master', 'rate-grandmaster', 'rate-target'] +RATING_CLASS = [ + "rate-newbie", + "rate-amateur", + "rate-specialist", + "rate-expert", + "rate-candidate-master", + "rate-master", + "rate-grandmaster", + "rate-target", +] def rating_level(rating): @@ -213,4 +278,4 @@ def rating_progress(rating): return 1.0 prev = 0 if not level else RATING_VALUES[level - 1] next = RATING_VALUES[level] - return (rating - prev + 0.0) / (next - prev) \ No newline at end of file + return (rating - prev + 0.0) / (next - prev) diff --git a/judge/signals.py b/judge/signals.py index e03c17c..e308f98 100644 --- a/judge/signals.py +++ b/judge/signals.py @@ -9,8 +9,21 @@ from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from .caching import finished_submission -from .models import BlogPost, Comment, Contest, ContestSubmission, EFFECTIVE_MATH_ENGINES, Judge, Language, License, \ - MiscConfig, Organization, Problem, Profile, Submission +from .models import ( + BlogPost, + Comment, + Contest, + ContestSubmission, + EFFECTIVE_MATH_ENGINES, + Judge, + Language, + License, + MiscConfig, + Organization, + Problem, + Profile, + Submission, +) def get_pdf_path(basename): @@ -27,75 +40,109 @@ def unlink_if_exists(file): @receiver(post_save, sender=Problem) def problem_update(sender, instance, **kwargs): - if hasattr(instance, '_updating_stats_only'): + if hasattr(instance, "_updating_stats_only"): return - cache.delete_many([ - make_template_fragment_key('submission_problem', (instance.id,)), - make_template_fragment_key('problem_feed', (instance.id,)), - 'problem_tls:%s' % instance.id, 'problem_mls:%s' % instance.id, - ]) - cache.delete_many([make_template_fragment_key('problem_html', (instance.id, engine, lang)) - for lang, _ in settings.LANGUAGES for engine in EFFECTIVE_MATH_ENGINES]) - cache.delete_many([make_template_fragment_key('problem_authors', (instance.id, lang)) - for lang, _ in settings.LANGUAGES]) - cache.delete_many(['generated-meta-problem:%s:%d' % (lang, instance.id) for lang, _ in settings.LANGUAGES]) + cache.delete_many( + [ + make_template_fragment_key("submission_problem", (instance.id,)), + make_template_fragment_key("problem_feed", (instance.id,)), + "problem_tls:%s" % instance.id, + "problem_mls:%s" % instance.id, + ] + ) + cache.delete_many( + [ + make_template_fragment_key("problem_html", (instance.id, engine, lang)) + for lang, _ in settings.LANGUAGES + for engine in EFFECTIVE_MATH_ENGINES + ] + ) + cache.delete_many( + [ + make_template_fragment_key("problem_authors", (instance.id, lang)) + for lang, _ in settings.LANGUAGES + ] + ) + cache.delete_many( + [ + "generated-meta-problem:%s:%d" % (lang, instance.id) + for lang, _ in settings.LANGUAGES + ] + ) for lang, _ in settings.LANGUAGES: - unlink_if_exists(get_pdf_path('%s.%s.pdf' % (instance.code, lang))) + unlink_if_exists(get_pdf_path("%s.%s.pdf" % (instance.code, lang))) @receiver(post_save, sender=Profile) def profile_update(sender, instance, **kwargs): - if hasattr(instance, '_updating_stats_only'): + if hasattr(instance, "_updating_stats_only"): return - cache.delete_many([make_template_fragment_key('user_about', (instance.id, engine)) - for engine in EFFECTIVE_MATH_ENGINES] + - [make_template_fragment_key('org_member_count', (org_id,)) - for org_id in instance.organizations.values_list('id', flat=True)]) + cache.delete_many( + [ + make_template_fragment_key("user_about", (instance.id, engine)) + for engine in EFFECTIVE_MATH_ENGINES + ] + + [ + make_template_fragment_key("org_member_count", (org_id,)) + for org_id in instance.organizations.values_list("id", flat=True) + ] + ) @receiver(post_save, sender=Contest) def contest_update(sender, instance, **kwargs): - if hasattr(instance, '_updating_stats_only'): + if hasattr(instance, "_updating_stats_only"): return - cache.delete_many(['generated-meta-contest:%d' % instance.id] + - [make_template_fragment_key('contest_html', (instance.id, engine)) - for engine in EFFECTIVE_MATH_ENGINES]) + cache.delete_many( + ["generated-meta-contest:%d" % instance.id] + + [ + make_template_fragment_key("contest_html", (instance.id, engine)) + for engine in EFFECTIVE_MATH_ENGINES + ] + ) @receiver(post_save, sender=License) def license_update(sender, instance, **kwargs): - cache.delete(make_template_fragment_key('license_html', (instance.id,))) + cache.delete(make_template_fragment_key("license_html", (instance.id,))) @receiver(post_save, sender=Language) def language_update(sender, instance, **kwargs): - cache.delete_many([make_template_fragment_key('language_html', (instance.id,)), - 'lang:cn_map']) + cache.delete_many( + [make_template_fragment_key("language_html", (instance.id,)), "lang:cn_map"] + ) @receiver(post_save, sender=Judge) def judge_update(sender, instance, **kwargs): - cache.delete(make_template_fragment_key('judge_html', (instance.id,))) + cache.delete(make_template_fragment_key("judge_html", (instance.id,))) @receiver(post_save, sender=Comment) def comment_update(sender, instance, **kwargs): - cache.delete('comment_feed:%d' % instance.id) + cache.delete("comment_feed:%d" % instance.id) @receiver(post_save, sender=BlogPost) def post_update(sender, instance, **kwargs): - cache.delete_many([ - make_template_fragment_key('post_summary', (instance.id,)), - 'blog_slug:%d' % instance.id, - 'blog_feed:%d' % instance.id, - ]) - cache.delete_many([make_template_fragment_key('post_content', (instance.id, engine)) - for engine in EFFECTIVE_MATH_ENGINES]) + cache.delete_many( + [ + make_template_fragment_key("post_summary", (instance.id,)), + "blog_slug:%d" % instance.id, + "blog_feed:%d" % instance.id, + ] + ) + cache.delete_many( + [ + make_template_fragment_key("post_content", (instance.id, engine)) + for engine in EFFECTIVE_MATH_ENGINES + ] + ) @receiver(post_delete, sender=Submission) @@ -112,21 +159,31 @@ def contest_submission_delete(sender, instance, **kwargs): @receiver(post_save, sender=Organization) def organization_update(sender, instance, **kwargs): - cache.delete_many([make_template_fragment_key('organization_html', (instance.id, engine)) - for engine in EFFECTIVE_MATH_ENGINES]) + cache.delete_many( + [ + make_template_fragment_key("organization_html", (instance.id, engine)) + for engine in EFFECTIVE_MATH_ENGINES + ] + ) _misc_config_i18n = [code for code, _ in settings.LANGUAGES] -_misc_config_i18n.append('') +_misc_config_i18n.append("") @receiver(post_save, sender=MiscConfig) def misc_config_update(sender, instance, **kwargs): - cache.delete_many(['misc_config:%s:%s:%s' % (domain, lang, instance.key.split('.')[0]) - for lang in _misc_config_i18n - for domain in Site.objects.values_list('domain', flat=True)]) + cache.delete_many( + [ + "misc_config:%s:%s:%s" % (domain, lang, instance.key.split(".")[0]) + for lang in _misc_config_i18n + for domain in Site.objects.values_list("domain", flat=True) + ] + ) @receiver(post_save, sender=ContestSubmission) def contest_submission_update(sender, instance, **kwargs): - Submission.objects.filter(id=instance.submission_id).update(contest_object_id=instance.participation.contest_id) + Submission.objects.filter(id=instance.submission_id).update( + contest_object_id=instance.participation.contest_id + ) diff --git a/judge/sitemap.py b/judge/sitemap.py index 2deea97..dd71525 100644 --- a/judge/sitemap.py +++ b/judge/sitemap.py @@ -7,80 +7,83 @@ from judge.models import BlogPost, Contest, Organization, Problem, Solution class ProblemSitemap(Sitemap): - changefreq = 'daily' + changefreq = "daily" priority = 0.8 def items(self): - return Problem.get_public_problems().values_list('code') + return Problem.get_public_problems().values_list("code") def location(self, obj): - return reverse('problem_detail', args=obj) + return reverse("problem_detail", args=obj) class UserSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.5 def items(self): - return User.objects.values_list('username') + return User.objects.values_list("username") def location(self, obj): - return reverse('user_page', args=obj) + return reverse("user_page", args=obj) class ContestSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.5 def items(self): - return Contest.objects.filter(is_visible=True, is_private=False, - is_organization_private=False).values_list('key') + return Contest.objects.filter( + is_visible=True, is_private=False, is_organization_private=False + ).values_list("key") def location(self, obj): - return reverse('contest_view', args=obj) + return reverse("contest_view", args=obj) class OrganizationSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.5 def items(self): - return Organization.objects.values_list('id', 'slug') + return Organization.objects.values_list("id", "slug") def location(self, obj): - return reverse('organization_home', args=obj) + return reverse("organization_home", args=obj) class BlogPostSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.7 def items(self): - return (BlogPost.objects.filter(visible=True, is_organization_private=False, publish_on__lte=timezone.now()) - .values_list('id', 'slug')) - + return BlogPost.objects.filter( + visible=True, is_organization_private=False, publish_on__lte=timezone.now() + ).values_list("id", "slug") + def location(self, obj): - return reverse('blog_post', args=obj) + return reverse("blog_post", args=obj) class SolutionSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.8 def items(self): - return (Solution.objects.filter(is_public=True, publish_on__lte=timezone.now(), problem__isnull=False) - .values_list('problem__code')) + return Solution.objects.filter( + is_public=True, publish_on__lte=timezone.now(), problem__isnull=False + ).values_list("problem__code") def location(self, obj): - return reverse('problem_editorial', args=obj) + return reverse("problem_editorial", args=obj) class HomePageSitemap(Sitemap): priority = 1.0 - changefreq = 'daily' + changefreq = "daily" def items(self): - return ['home'] + return ["home"] def location(self, obj): return reverse(obj) @@ -94,10 +97,10 @@ class UrlSitemap(Sitemap): return self.pages def location(self, obj): - return obj['location'] if isinstance(obj, dict) else obj + return obj["location"] if isinstance(obj, dict) else obj def priority(self, obj): - return obj.get('priority', 0.5) if isinstance(obj, dict) else 0.5 + return obj.get("priority", 0.5) if isinstance(obj, dict) else 0.5 def changefreq(self, obj): - return obj.get('changefreq', 'daily') if isinstance(obj, dict) else 'daily' + return obj.get("changefreq", "daily") if isinstance(obj, dict) else "daily" diff --git a/judge/social_auth.py b/judge/social_auth.py index 7546010..71a12bb 100644 --- a/judge/social_auth.py +++ b/judge/social_auth.py @@ -14,53 +14,65 @@ from reversion import revisions from social_core.backends.github import GithubOAuth2 from social_core.exceptions import InvalidEmail, SocialAuthBaseException from social_core.pipeline.partial import partial -from social_django.middleware import SocialAuthExceptionMiddleware as OldSocialAuthExceptionMiddleware +from social_django.middleware import ( + SocialAuthExceptionMiddleware as OldSocialAuthExceptionMiddleware, +) from judge.forms import ProfileForm from judge.models import Language, Profile -logger = logging.getLogger('judge.social_auth') +logger = logging.getLogger("judge.social_auth") class GitHubSecureEmailOAuth2(GithubOAuth2): - name = 'github-secure' + name = "github-secure" def user_data(self, access_token, *args, **kwargs): data = self._user_data(access_token) try: - emails = self._user_data(access_token, '/emails') + emails = self._user_data(access_token, "/emails") except (HTTPError, ValueError, TypeError): emails = [] - emails = [(e.get('email'), e.get('primary'), 0) for e in emails if isinstance(e, dict) and e.get('verified')] + emails = [ + (e.get("email"), e.get("primary"), 0) + for e in emails + if isinstance(e, dict) and e.get("verified") + ] emails.sort(key=itemgetter(1), reverse=True) emails = list(map(itemgetter(0), emails)) if emails: - data['email'] = emails[0] + data["email"] = emails[0] else: - data['email'] = None + data["email"] = None return data -def slugify_username(username, renotword=re.compile(r'[^\w]')): - return renotword.sub('', username.replace('-', '_')) +def slugify_username(username, renotword=re.compile(r"[^\w]")): + return renotword.sub("", username.replace("-", "_")) def verify_email(backend, details, *args, **kwargs): - if not details['email']: + if not details["email"]: raise InvalidEmail(backend) class UsernameForm(forms.Form): - username = forms.RegexField(regex=r'^\w+$', max_length=30, label='Username', - error_messages={'invalid': 'A username must contain letters, numbers, or underscores'}) + username = forms.RegexField( + regex=r"^\w+$", + max_length=30, + label="Username", + error_messages={ + "invalid": "A username must contain letters, numbers, or underscores" + }, + ) def clean_username(self): - if User.objects.filter(username=self.cleaned_data['username']).exists(): - raise forms.ValidationError('Sorry, the username is taken.') - return self.cleaned_data['username'] + if User.objects.filter(username=self.cleaned_data["username"]).exists(): + raise forms.ValidationError("Sorry, the username is taken.") + return self.cleaned_data["username"] @partial @@ -70,21 +82,26 @@ def choose_username(backend, user, username=None, *args, **kwargs): if request.POST: form = UsernameForm(request.POST) if form.is_valid(): - return {'username': form.cleaned_data['username']} + return {"username": form.cleaned_data["username"]} else: - form = UsernameForm(initial={'username': username}) - return render(request, 'registration/username_select.html', { - 'title': 'Choose a username', 'form': form, - }) + form = UsernameForm(initial={"username": username}) + return render( + request, + "registration/username_select.html", + { + "title": "Choose a username", + "form": form, + }, + ) @partial def make_profile(backend, user, response, is_new=False, *args, **kwargs): if is_new: - if not hasattr(user, 'profile'): + if not hasattr(user, "profile"): profile = Profile(user=user) profile.language = Language.get_default_language() - logger.info('Info from %s: %s', backend.name, response) + logger.info("Info from %s: %s", backend.name, response) profile.save() form = ProfileForm(instance=profile, user=user) else: @@ -95,15 +112,25 @@ def make_profile(backend, user, response, is_new=False, *args, **kwargs): with transaction.atomic(), revisions.create_revision(): form.save() revisions.set_user(user) - revisions.set_comment('Updated on registration') + revisions.set_comment("Updated on registration") return - return render(backend.strategy.request, 'registration/profile_creation.html', { - 'title': 'Create your profile', 'form': form, - }) + return render( + backend.strategy.request, + "registration/profile_creation.html", + { + "title": "Create your profile", + "form": form, + }, + ) class SocialAuthExceptionMiddleware(OldSocialAuthExceptionMiddleware): def process_exception(self, request, exception): if isinstance(exception, SocialAuthBaseException): - return HttpResponseRedirect('%s?message=%s' % (reverse('social_auth_error'), - quote(self.get_message(request, exception)))) + return HttpResponseRedirect( + "%s?message=%s" + % ( + reverse("social_auth_error"), + quote(self.get_message(request, exception)), + ) + ) diff --git a/judge/tasks/contest.py b/judge/tasks/contest.py index 0c36e23..ca896a9 100644 --- a/judge/tasks/contest.py +++ b/judge/tasks/contest.py @@ -7,7 +7,7 @@ from moss import MOSS from judge.models import Contest, ContestMoss, ContestParticipation, Submission from judge.utils.celery import Progress -__all__ = ('rescore_contest', 'run_moss') +__all__ = ("rescore_contest", "run_moss") @shared_task(bind=True) @@ -16,7 +16,9 @@ def rescore_contest(self, contest_key): participations = contest.users rescored = 0 - with Progress(self, participations.count(), stage=_('Recalculating contest scores')) as p: + with Progress( + self, participations.count(), stage=_("Recalculating contest scores") + ) as p: for participation in participations.iterator(): participation.recompute_results() rescored += 1 @@ -29,7 +31,7 @@ def rescore_contest(self, contest_key): def run_moss(self, contest_key): moss_api_key = settings.MOSS_API_KEY if moss_api_key is None: - raise ImproperlyConfigured('No MOSS API Key supplied') + raise ImproperlyConfigured("No MOSS API Key supplied") contest = Contest.objects.get(key=contest_key) ContestMoss.objects.filter(contest=contest).delete() @@ -37,21 +39,34 @@ def run_moss(self, contest_key): length = len(ContestMoss.LANG_MAPPING) * contest.problems.count() moss_results = [] - with Progress(self, length, stage=_('Running MOSS')) as p: + with Progress(self, length, stage=_("Running MOSS")) as p: for problem in contest.problems.all(): for dmoj_lang, moss_lang in ContestMoss.LANG_MAPPING: - result = ContestMoss(contest=contest, problem=problem, language=dmoj_lang) + result = ContestMoss( + contest=contest, problem=problem, language=dmoj_lang + ) - subs = Submission.objects.filter( - contest__participation__virtual__in=(ContestParticipation.LIVE, ContestParticipation.SPECTATE), - contest_object=contest, - problem=problem, - language__common_name=dmoj_lang, - ).order_by('-points').values_list('user__user__username', 'source__source') + subs = ( + Submission.objects.filter( + contest__participation__virtual__in=( + ContestParticipation.LIVE, + ContestParticipation.SPECTATE, + ), + contest_object=contest, + problem=problem, + language__common_name=dmoj_lang, + ) + .order_by("-points") + .values_list("user__user__username", "source__source") + ) if subs.exists(): - moss_call = MOSS(moss_api_key, language=moss_lang, matching_file_limit=100, - comment='%s - %s' % (contest.key, problem.code)) + moss_call = MOSS( + moss_api_key, + language=moss_lang, + matching_file_limit=100, + comment="%s - %s" % (contest.key, problem.code), + ) users = set() @@ -59,7 +74,7 @@ def run_moss(self, contest_key): if username in users: continue users.add(username) - moss_call.add_file_from_memory(username, source.encode('utf-8')) + moss_call.add_file_from_memory(username, source.encode("utf-8")) result.url = moss_call.process() result.submission_count = len(users) diff --git a/judge/tasks/demo.py b/judge/tasks/demo.py index c09dcbf..bd97401 100644 --- a/judge/tasks/demo.py +++ b/judge/tasks/demo.py @@ -4,7 +4,7 @@ from celery import shared_task from judge.utils.celery import Progress -__all__ = ('success', 'failure', 'progress') +__all__ = ("success", "failure", "progress") @shared_task @@ -14,7 +14,7 @@ def success(): @shared_task def failure(): - raise RuntimeError('This task always fails.') + raise RuntimeError("This task always fails.") @shared_task(bind=True) diff --git a/judge/tasks/experiment.py b/judge/tasks/experiment.py index 0e991c4..978c643 100644 --- a/judge/tasks/experiment.py +++ b/judge/tasks/experiment.py @@ -2,19 +2,20 @@ from judge.models import SubmissionTestCase, Problem from collections import defaultdict + def generate_report(problem): testcases = SubmissionTestCase.objects.filter(submission__problem=problem).all() - + score = defaultdict(int) total = defaultdict(int) rate = defaultdict(int) for case in testcases.iterator(): - score[case.case] += int(case.status == 'AC') + score[case.case] += int(case.status == "AC") total[case.case] += 1 for i in score: rate[i] = score[i] / total[i] for i, _ in sorted(rate.items(), key=lambda x: x[1], reverse=True): - print(i, score[i], total[i], rate[i]) \ No newline at end of file + print(i, score[i], total[i], rate[i]) diff --git a/judge/tasks/import_users.py b/judge/tasks/import_users.py index aeaeca5..db8d5c5 100644 --- a/judge/tasks/import_users.py +++ b/judge/tasks/import_users.py @@ -7,20 +7,23 @@ from django.contrib.auth.models import User from judge.models import Profile, Language, Organization -fields = ['username', 'password', 'name', 'school', 'email', 'organizations'] -descriptions = ['my_username(edit old one if exist)', - '123456 (must have)', - 'Le Van A (can be empty)', - 'Le Quy Don (can be empty)', - 'email@email.com (can be empty)', - 'org1&org2&org3&... (can be empty - org slug in URL)'] +fields = ["username", "password", "name", "school", "email", "organizations"] +descriptions = [ + "my_username(edit old one if exist)", + "123456 (must have)", + "Le Van A (can be empty)", + "Le Quy Don (can be empty)", + "email@email.com (can be empty)", + "org1&org2&org3&... (can be empty - org slug in URL)", +] + def csv_to_dict(csv_file): - rows = csv.reader(csv_file.read().decode().split('\n')) + rows = csv.reader(csv_file.read().decode().split("\n")) header = next(rows) header = [i.lower() for i in header] - if 'username' not in header: + if "username" not in header: return [] res = [] @@ -28,55 +31,61 @@ def csv_to_dict(csv_file): for row in rows: if len(row) != len(header): continue - cur_dict = {i: '' for i in fields} + cur_dict = {i: "" for i in fields} for i in range(len(header)): if header[i] not in fields: continue cur_dict[header[i]] = row[i] - if cur_dict['username']: + if cur_dict["username"]: res.append(cur_dict) return res - + # return result log def import_users(users): - log = '' + log = "" for i, row in enumerate(users): - cur_log = str(i + 1) + '. ' + cur_log = str(i + 1) + ". " - username = row['username'] - cur_log += username + ': ' + username = row["username"] + cur_log += username + ": " - pwd = row['password'] - - user, created = User.objects.get_or_create(username=username, defaults={ - 'is_active': True, - }) + pwd = row["password"] - profile, _ = Profile.objects.get_or_create(user=user, defaults={ - 'language': Language.get_python3(), - 'timezone': settings.DEFAULT_USER_TIME_ZONE, - }) + user, created = User.objects.get_or_create( + username=username, + defaults={ + "is_active": True, + }, + ) + + profile, _ = Profile.objects.get_or_create( + user=user, + defaults={ + "language": Language.get_python3(), + "timezone": settings.DEFAULT_USER_TIME_ZONE, + }, + ) if created: - cur_log += 'Create new - ' + cur_log += "Create new - " else: - cur_log += 'Edit - ' + cur_log += "Edit - " if pwd: user.set_password(pwd) elif created: - user.set_password('lqdoj') - cur_log += 'Missing password, set password = lqdoj - ' + user.set_password("lqdoj") + cur_log += "Missing password, set password = lqdoj - " - if 'name' in row.keys() and row['name']: - user.first_name = row['name'] + if "name" in row.keys() and row["name"]: + user.first_name = row["name"] - if 'school' in row.keys() and row['school']: - user.last_name = row['school'] + if "school" in row.keys() and row["school"]: + user.last_name = row["school"] - if row['organizations']: - orgs = row['organizations'].split('&') + if row["organizations"]: + orgs = row["organizations"].split("&") added_orgs = [] for o in orgs: try: @@ -86,15 +95,15 @@ def import_users(users): except Organization.DoesNotExist: continue if added_orgs: - cur_log += 'Added to ' + ', '.join(added_orgs) + ' - ' + cur_log += "Added to " + ", ".join(added_orgs) + " - " + + if row["email"]: + user.email = row["email"] - if row['email']: - user.email = row['email'] - user.save() profile.save() - cur_log += 'Saved\n' + cur_log += "Saved\n" log += cur_log - log += 'FINISH' + log += "FINISH" - return log \ No newline at end of file + return log diff --git a/judge/tasks/submission.py b/judge/tasks/submission.py index a11de8c..5a5a8a9 100644 --- a/judge/tasks/submission.py +++ b/judge/tasks/submission.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext as _ from judge.models import Problem, Profile, Submission from judge.utils.celery import Progress -__all__ = ('apply_submission_filter', 'rejudge_problem_filter', 'rescore_problem') +__all__ = ("apply_submission_filter", "rejudge_problem_filter", "rescore_problem") def apply_submission_filter(queryset, id_range, languages, results): @@ -21,7 +21,9 @@ def apply_submission_filter(queryset, id_range, languages, results): @shared_task(bind=True) -def rejudge_problem_filter(self, problem_id, id_range=None, languages=None, results=None): +def rejudge_problem_filter( + self, problem_id, id_range=None, languages=None, results=None +): queryset = Submission.objects.filter(problem_id=problem_id) queryset = apply_submission_filter(queryset, id_range, languages, results) @@ -40,27 +42,37 @@ def rescore_problem(self, problem_id): problem = Problem.objects.get(id=problem_id) submissions = Submission.objects.filter(problem_id=problem_id) - with Progress(self, submissions.count(), stage=_('Modifying submissions')) as p: + with Progress(self, submissions.count(), stage=_("Modifying submissions")) as p: rescored = 0 for submission in submissions.iterator(): - submission.points = round(submission.case_points / submission.case_total * problem.points - if submission.case_total else 0, 1) + submission.points = round( + submission.case_points / submission.case_total * problem.points + if submission.case_total + else 0, + 1, + ) if not problem.partial and submission.points < problem.points: submission.points = 0 - submission.save(update_fields=['points']) + submission.save(update_fields=["points"]) submission.update_contest() rescored += 1 if rescored % 10 == 0: p.done = rescored - with Progress(self, submissions.values('user_id').distinct().count(), stage=_('Recalculating user points')) as p: + with Progress( + self, + submissions.values("user_id").distinct().count(), + stage=_("Recalculating user points"), + ) as p: users = 0 - profiles = Profile.objects.filter(id__in=submissions.values_list('user_id', flat=True).distinct()) + profiles = Profile.objects.filter( + id__in=submissions.values_list("user_id", flat=True).distinct() + ) for profile in profiles.iterator(): profile._updating_stats_only = True profile.calculate_points() - cache.delete('user_complete:%d' % profile.id) - cache.delete('user_attempted:%d' % profile.id) + cache.delete("user_complete:%d" % profile.id) + cache.delete("user_attempted:%d" % profile.id) users += 1 if users % 10 == 0: p.done = users diff --git a/judge/template_context.py b/judge/template_context.py index 6348407..3c1900d 100644 --- a/judge/template_context.py +++ b/judge/template_context.py @@ -11,26 +11,26 @@ from .models import MiscConfig, NavigationBar, Profile class FixedSimpleLazyObject(SimpleLazyObject): - if not hasattr(SimpleLazyObject, '__iter__'): + if not hasattr(SimpleLazyObject, "__iter__"): __iter__ = new_method_proxy(iter) def get_resource(request): use_https = settings.DMOJ_SSL if use_https == 1: - scheme = 'https' if request.is_secure() else 'http' + scheme = "https" if request.is_secure() else "http" elif use_https > 1: - scheme = 'https' + scheme = "https" else: - scheme = 'http' + scheme = "http" return { - 'PYGMENT_THEME': settings.PYGMENT_THEME, - 'INLINE_JQUERY': settings.INLINE_JQUERY, - 'INLINE_FONTAWESOME': settings.INLINE_FONTAWESOME, - 'JQUERY_JS': settings.JQUERY_JS, - 'FONTAWESOME_CSS': settings.FONTAWESOME_CSS, - 'DMOJ_SCHEME': scheme, - 'DMOJ_CANONICAL': settings.DMOJ_CANONICAL, + "PYGMENT_THEME": settings.PYGMENT_THEME, + "INLINE_JQUERY": settings.INLINE_JQUERY, + "INLINE_FONTAWESOME": settings.INLINE_FONTAWESOME, + "JQUERY_JS": settings.JQUERY_JS, + "FONTAWESOME_CSS": settings.FONTAWESOME_CSS, + "DMOJ_SCHEME": scheme, + "DMOJ_CANONICAL": settings.DMOJ_CANONICAL, } @@ -42,56 +42,63 @@ def get_profile(request): def comet_location(request): if request.is_secure(): - websocket = getattr(settings, 'EVENT_DAEMON_GET_SSL', settings.EVENT_DAEMON_GET) - poll = getattr(settings, 'EVENT_DAEMON_POLL_SSL', settings.EVENT_DAEMON_POLL) + websocket = getattr(settings, "EVENT_DAEMON_GET_SSL", settings.EVENT_DAEMON_GET) + poll = getattr(settings, "EVENT_DAEMON_POLL_SSL", settings.EVENT_DAEMON_POLL) else: websocket = settings.EVENT_DAEMON_GET poll = settings.EVENT_DAEMON_POLL - return {'EVENT_DAEMON_LOCATION': websocket, - 'EVENT_DAEMON_POLL_LOCATION': poll} + return {"EVENT_DAEMON_LOCATION": websocket, "EVENT_DAEMON_POLL_LOCATION": poll} def __nav_tab(path): - result = list(NavigationBar.objects.extra(where=['%s REGEXP BINARY regex'], params=[path])[:1]) - return result[0].get_ancestors(include_self=True).values_list('key', flat=True) if result else [] + result = list( + NavigationBar.objects.extra(where=["%s REGEXP BINARY regex"], params=[path])[:1] + ) + return ( + result[0].get_ancestors(include_self=True).values_list("key", flat=True) + if result + else [] + ) def general_info(request): path = request.get_full_path() return { - 'nav_tab': FixedSimpleLazyObject(partial(__nav_tab, request.path)), - 'nav_bar': NavigationBar.objects.all(), - 'LOGIN_RETURN_PATH': '' if path.startswith('/accounts/') else path, - 'perms': PermWrapper(request.user), + "nav_tab": FixedSimpleLazyObject(partial(__nav_tab, request.path)), + "nav_bar": NavigationBar.objects.all(), + "LOGIN_RETURN_PATH": "" if path.startswith("/accounts/") else path, + "perms": PermWrapper(request.user), } def site(request): - return {'site': get_current_site(request)} + return {"site": get_current_site(request)} class MiscConfigDict(dict): - __slots__ = ('language', 'site') + __slots__ = ("language", "site") - def __init__(self, language='', domain=None): + def __init__(self, language="", domain=None): self.language = language self.site = domain super(MiscConfigDict, self).__init__() def __missing__(self, key): - cache_key = 'misc_config:%s:%s:%s' % (self.site, self.language, key) + cache_key = "misc_config:%s:%s:%s" % (self.site, self.language, key) value = cache.get(cache_key) if value is None: - keys = ['%s.%s' % (key, self.language), key] if self.language else [key] + keys = ["%s.%s" % (key, self.language), key] if self.language else [key] if self.site is not None: - keys = ['%s:%s' % (self.site, key) for key in keys] + keys - map = dict(MiscConfig.objects.values_list('key', 'value').filter(key__in=keys)) + keys = ["%s:%s" % (self.site, key) for key in keys] + keys + map = dict( + MiscConfig.objects.values_list("key", "value").filter(key__in=keys) + ) for item in keys: if item in map: value = map[item] break else: - value = '' + value = "" cache.set(cache_key, value, 86400) self[key] = value return value @@ -99,23 +106,29 @@ class MiscConfigDict(dict): def misc_config(request): domain = get_current_site(request).domain - return {'misc_config': MiscConfigDict(domain=domain), - 'i18n_config': MiscConfigDict(language=request.LANGUAGE_CODE, domain=domain)} + return { + "misc_config": MiscConfigDict(domain=domain), + "i18n_config": MiscConfigDict(language=request.LANGUAGE_CODE, domain=domain), + } def site_name(request): - return {'SITE_NAME': settings.SITE_NAME, - 'SITE_LONG_NAME': settings.SITE_LONG_NAME, - 'SITE_ADMIN_EMAIL': settings.SITE_ADMIN_EMAIL} + return { + "SITE_NAME": settings.SITE_NAME, + "SITE_LONG_NAME": settings.SITE_LONG_NAME, + "SITE_ADMIN_EMAIL": settings.SITE_ADMIN_EMAIL, + } def math_setting(request): - caniuse = CanIUse(request.META.get('HTTP_USER_AGENT', '')) + caniuse = CanIUse(request.META.get("HTTP_USER_AGENT", "")) if request.user.is_authenticated: engine = request.profile.math_engine else: engine = settings.MATHOID_DEFAULT_TYPE - if engine == 'auto': - engine = 'mml' if bool(settings.MATHOID_URL) and caniuse.mathml == SUPPORT else 'jax' - return {'MATH_ENGINE': engine, 'REQUIRE_JAX': engine == 'jax', 'caniuse': caniuse} + if engine == "auto": + engine = ( + "mml" if bool(settings.MATHOID_URL) and caniuse.mathml == SUPPORT else "jax" + ) + return {"MATH_ENGINE": engine, "REQUIRE_JAX": engine == "jax", "caniuse": caniuse} diff --git a/judge/templatetags/dicts.py b/judge/templatetags/dicts.py index 3afc15b..efb321c 100644 --- a/judge/templatetags/dicts.py +++ b/judge/templatetags/dicts.py @@ -3,6 +3,6 @@ from django import template register = template.Library() -@register.filter(name='get_dict_item') +@register.filter(name="get_dict_item") def get_item(dictionary, key): return dictionary.get(key) diff --git a/judge/templatetags/list_processor.py b/judge/templatetags/list_processor.py index dc37105..15109cf 100644 --- a/judge/templatetags/list_processor.py +++ b/judge/templatetags/list_processor.py @@ -5,7 +5,7 @@ from django import template register = template.Library() -@register.filter(name='list_attr') +@register.filter(name="list_attr") def list_attr(iterable, prop): result = [] for item in iterable: @@ -15,43 +15,43 @@ def list_attr(iterable, prop): try: result.append(item[prop]) except KeyError: - result.append('') + result.append("") except TypeError: try: result.append(item[int(prop)]) except (IndexError, ValueError, TypeError): - result.append('') + result.append("") return result -@register.filter(name='list_getitem') +@register.filter(name="list_getitem") def list_getitem(iterable, prop): return list(map(itemgetter(prop), iterable)) -@register.filter(name='list_getindex') +@register.filter(name="list_getindex") def list_getindex(iterable, index): return list(map(itemgetter(int(index)), iterable)) -@register.filter(name='list_getattr') +@register.filter(name="list_getattr") def list_getattr(iterable, prop): return list(map(attrgetter(prop), iterable)) -@register.filter(name='sum_list') +@register.filter(name="sum_list") def sum_list(iterable): return sum(iterable) -@register.filter(name='max_list') +@register.filter(name="max_list") def max_list(iterable): if not iterable: return 0 return max(iterable) -@register.filter(name='min_list') +@register.filter(name="min_list") def min_list(iterable): if not iterable: return 0 diff --git a/judge/templatetags/strings.py b/judge/templatetags/strings.py index 6bd685a..475a615 100644 --- a/judge/templatetags/strings.py +++ b/judge/templatetags/strings.py @@ -3,16 +3,16 @@ from django import template register = template.Library() -@register.filter(name='split') +@register.filter(name="split") def split(value): - return value.split('\n') + return value.split("\n") -@register.filter(name='cutoff') +@register.filter(name="cutoff") def cutoff(value, length): - return value[:int(length)] + return value[: int(length)] -@register.filter(name='roundfloat') +@register.filter(name="roundfloat") def roundfloat(value, at): return str(round(value, int(at))) diff --git a/judge/user_log.py b/judge/user_log.py index d5304ff..91a4c67 100644 --- a/judge/user_log.py +++ b/judge/user_log.py @@ -10,12 +10,15 @@ class LogUserAccessMiddleware(object): def __call__(self, request): response = self.get_response(request) - if (hasattr(request, 'user') and request.user.is_authenticated and - not getattr(request, 'no_profile_update', False)): - updates = {'last_access': now()} + if ( + hasattr(request, "user") + and request.user.is_authenticated + and not getattr(request, "no_profile_update", False) + ): + updates = {"last_access": now()} # Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it. - if request.META.get('REMOTE_ADDR'): - updates['ip'] = request.META.get('REMOTE_ADDR') + if request.META.get("REMOTE_ADDR"): + updates["ip"] = request.META.get("REMOTE_ADDR") Profile.objects.filter(user_id=request.user.pk).update(**updates) return response diff --git a/judge/user_translations.py b/judge/user_translations.py index edc32ae..9932854 100644 --- a/judge/user_translations.py +++ b/judge/user_translations.py @@ -10,23 +10,25 @@ if settings.USE_I18N: def translation(language): global _translations if language not in _translations: - _translations[language] = DjangoTranslation(language, domain='dmoj-user') + _translations[language] = DjangoTranslation(language, domain="dmoj-user") return _translations[language] def do_translate(message, translation_function): """Copied from django.utils.translation.trans_real""" # str() is allowing a bytestring message to remain bytestring on Python 2 - eol_message = message.replace(str('\r\n'), str('\n')).replace(str('\r'), str('\n')) + eol_message = message.replace(str("\r\n"), str("\n")).replace( + str("\r"), str("\n") + ) if len(eol_message) == 0: # Returns an empty value of the corresponding type if an empty message # is given, instead of metadata, which is the default gettext behavior. - result = '' + result = "" else: translation_object = translation(get_language()) result = getattr(translation_object, translation_function)(eol_message) if not isinstance(result, six.text_type): - result = result.decode('utf-8') + result = result.decode("utf-8") if isinstance(message, SafeData): return mark_safe(result) @@ -34,7 +36,9 @@ if settings.USE_I18N: return result def gettext(message): - return do_translate(message, 'gettext') + return do_translate(message, "gettext") + else: + def gettext(message): return message diff --git a/judge/utils/camo.py b/judge/utils/camo.py index a3c90a5..025963f 100644 --- a/judge/utils/camo.py +++ b/judge/utils/camo.py @@ -10,39 +10,44 @@ class CamoClient(object): """Based on https://github.com/sionide21/camo-client""" def __init__(self, server, key, excluded=(), https=False): - self.server = server.rstrip('/') + self.server = server.rstrip("/") self.key = key self.https = https self.excluded = excluded def image_url(self, url): - return '%s/%s/%s' % (self.server, - hmac.new(utf8bytes(self.key), utf8bytes(url), sha1).hexdigest(), - utf8bytes(url).hex()) + return "%s/%s/%s" % ( + self.server, + hmac.new(utf8bytes(self.key), utf8bytes(url), sha1).hexdigest(), + utf8bytes(url).hex(), + ) def rewrite_url(self, url): if url.startswith(self.server) or url.startswith(self.excluded): return url - elif url.startswith(('http://', 'https://')): + elif url.startswith(("http://", "https://")): return self.image_url(url) - elif url.startswith('//'): - return self.rewrite_url(('https:' if self.https else 'http:') + url) + elif url.startswith("//"): + return self.rewrite_url(("https:" if self.https else "http:") + url) else: return url def update_tree(self, doc): - for img in doc.xpath('.//img'): - for attr in ('src', 'data-src'): + for img in doc.xpath(".//img"): + for attr in ("src", "data-src"): if img.get(attr): img.set(attr, self.rewrite_url(img.get(attr))) - for obj in doc.xpath('.//object'): - if obj.get('data'): - obj.set('data', self.rewrite_url(obj.get('data'))) + for obj in doc.xpath(".//object"): + if obj.get("data"): + obj.set("data", self.rewrite_url(obj.get("data"))) if settings.DMOJ_CAMO_URL and settings.DMOJ_CAMO_KEY: - client = CamoClient(settings.DMOJ_CAMO_URL, key=settings.DMOJ_CAMO_KEY, - excluded=settings.DMOJ_CAMO_EXCLUDE, - https=settings.DMOJ_CAMO_HTTPS) + client = CamoClient( + settings.DMOJ_CAMO_URL, + key=settings.DMOJ_CAMO_KEY, + excluded=settings.DMOJ_CAMO_EXCLUDE, + https=settings.DMOJ_CAMO_HTTPS, + ) else: client = None diff --git a/judge/utils/caniuse.py b/judge/utils/caniuse.py index bd4bb52..2414785 100644 --- a/judge/utils/caniuse.py +++ b/judge/utils/caniuse.py @@ -1,15 +1,17 @@ import requests from ua_parser import user_agent_parser -_SUPPORT_DATA = requests.get('https://raw.githubusercontent.com/Fyrd/caniuse/master/data.json').json()['data'] +_SUPPORT_DATA = requests.get( + "https://raw.githubusercontent.com/Fyrd/caniuse/master/data.json" +).json()["data"] -SUPPORT = 'y' -PARTIAL_SUPPORT = 'a' -UNSUPPORTED = 'n' -POLYFILL = 'p' -UNKNOWN = 'u' -PREFIX = 'x' -DISABLED = 'd' +SUPPORT = "y" +PARTIAL_SUPPORT = "a" +UNSUPPORTED = "n" +POLYFILL = "p" +UNKNOWN = "u" +PREFIX = "x" +DISABLED = "d" def safe_int(string): @@ -28,19 +30,19 @@ class BrowserFamily(object): max_support = UNKNOWN for version, support in data.items(): - if version == 'all': + if version == "all": self.max_support = support - elif '-' in version: - start, end = version.split('-') - start = tuple(map(int, start.split('.'))) - end = tuple(map(int, end.split('.'))) + (1e3000,) + elif "-" in version: + start, end = version.split("-") + start = tuple(map(int, start.split("."))) + end = tuple(map(int, end.split("."))) + (1e3000,) ranges.append((start, end, support)) if end > max_version: max_version = end max_support = support else: try: - version = tuple(map(int, version.split('.'))) + version = tuple(map(int, version.split("."))) except ValueError: pass else: @@ -59,7 +61,12 @@ class BrowserFamily(object): if version > self.max_version: return self.max_support - for key in ((int_major, int_minor, int_patch), (int_major, int_minor), (int_major,), major): + for key in ( + (int_major, int_minor, int_patch), + (int_major, int_minor), + (int_major,), + major, + ): try: return self._versions[key] except KeyError: @@ -75,7 +82,9 @@ class BrowserFamily(object): class Feat(object): def __init__(self, data): self._data = data - self._family = {name: BrowserFamily(data) for name, data in data['stats'].items()} + self._family = { + name: BrowserFamily(data) for name, data in data["stats"].items() + } def __getitem__(self, item): return self._family[item] @@ -97,31 +106,31 @@ class CanIUse(object): def __init__(self, ua): self._agent = user_agent_parser.Parse(ua) - os_family = self._agent['os']['family'] - browser_family = self._agent['user_agent']['family'] + os_family = self._agent["os"]["family"] + browser_family = self._agent["user_agent"]["family"] family = None - if os_family == 'Android': - if 'Firefox' in browser_family: - family = 'and_ff' - elif 'Chrome' in browser_family: - family = 'and_chr' - elif 'Android' in browser_family: - family = 'android' + if os_family == "Android": + if "Firefox" in browser_family: + family = "and_ff" + elif "Chrome" in browser_family: + family = "and_chr" + elif "Android" in browser_family: + family = "android" else: - if 'Edge' in browser_family: - family = 'edge' - elif 'Firefox' in browser_family: - family = 'firefox' - elif 'Chrome' in browser_family: - family = 'chrome' - elif 'IE' in browser_family: - family = 'ie' - elif 'Opera' in browser_family: - family = 'opera' - elif 'Safari' in browser_family: - family = 'safari' + if "Edge" in browser_family: + family = "edge" + elif "Firefox" in browser_family: + family = "firefox" + elif "Chrome" in browser_family: + family = "chrome" + elif "IE" in browser_family: + family = "ie" + elif "Opera" in browser_family: + family = "opera" + elif "Safari" in browser_family: + family = "safari" self._family = family @@ -134,12 +143,12 @@ class CanIUse(object): except KeyError: return UNKNOWN else: - ua = self._agent['user_agent'] - return stats.check(ua['major'], ua['minor'], ua['patch'])[0] + ua = self._agent["user_agent"] + return stats.check(ua["major"], ua["minor"], ua["patch"])[0] def __getattr__(self, attr): try: - feat = database[attr.replace('_', '-')] + feat = database[attr.replace("_", "-")] except KeyError: raise AttributeError(attr) else: diff --git a/judge/utils/celery.py b/judge/utils/celery.py index c905eae..a632ddb 100644 --- a/judge/utils/celery.py +++ b/judge/utils/celery.py @@ -12,11 +12,11 @@ class Progress: def _update_state(self): self.task.update_state( - state='PROGRESS', + state="PROGRESS", meta={ - 'done': self._done, - 'total': self._total, - 'stage': self._stage, + "done": self._done, + "total": self._total, + "stage": self._stage, }, ) @@ -54,12 +54,12 @@ class Progress: def task_status_url(result, message=None, redirect=None): args = {} if message: - args['message'] = message + args["message"] = message if redirect: - args['redirect'] = redirect - url = reverse('task_status', args=[result.id]) + args["redirect"] = redirect + url = reverse("task_status", args=[result.id]) if args: - url += '?' + urlencode(args) + url += "?" + urlencode(args) return url diff --git a/judge/utils/diggpaginator.py b/judge/utils/diggpaginator.py index 2832d2d..de9ee7d 100644 --- a/judge/utils/diggpaginator.py +++ b/judge/utils/diggpaginator.py @@ -4,10 +4,10 @@ from functools import reduce from django.core.paginator import InvalidPage, Page, Paginator __all__ = ( - 'InvalidPage', - 'ExPaginator', - 'DiggPaginator', - 'QuerySetDiggPaginator', + "InvalidPage", + "ExPaginator", + "DiggPaginator", + "QuerySetDiggPaginator", ) @@ -182,15 +182,17 @@ class DiggPaginator(ExPaginator): """ def __init__(self, *args, **kwargs): - self.body = kwargs.pop('body', 10) - self.tail = kwargs.pop('tail', 2) - self.align_left = kwargs.pop('align_left', False) - self.margin = kwargs.pop('margin', 4) # TODO: make the default relative to body? + self.body = kwargs.pop("body", 10) + self.tail = kwargs.pop("tail", 2) + self.align_left = kwargs.pop("align_left", False) + self.margin = kwargs.pop( + "margin", 4 + ) # TODO: make the default relative to body? # validate padding value max_padding = int(math.ceil(self.body / 2.0) - 1) - self.padding = kwargs.pop('padding', min(4, max_padding)) + self.padding = kwargs.pop("padding", min(4, max_padding)) if self.padding > max_padding: - raise ValueError('padding too large for body (max %d)' % max_padding) + raise ValueError("padding too large for body (max %d)" % max_padding) super(DiggPaginator, self).__init__(*args, **kwargs) def page(self, number, *args, **kwargs): @@ -202,13 +204,24 @@ class DiggPaginator(ExPaginator): number = int(number) # we know this will work # easier access - num_pages, body, tail, padding, margin = \ - self.num_pages, self.body, self.tail, self.padding, self.margin + num_pages, body, tail, padding, margin = ( + self.num_pages, + self.body, + self.tail, + self.padding, + self.margin, + ) # put active page in middle of main range - main_range = list(map(int, [ - math.floor(number - body / 2.0) + 1, # +1 = shift odd body to right - math.floor(number + body / 2.0)])) + main_range = list( + map( + int, + [ + math.floor(number - body / 2.0) + 1, # +1 = shift odd body to right + math.floor(number + body / 2.0), + ], + ) + ) # adjust bounds if main_range[0] < 1: main_range = list(map(abs(main_range[0] - 1).__add__, main_range)) @@ -249,7 +262,10 @@ class DiggPaginator(ExPaginator): # section, again. main_range = [1, num_pages] else: - main_range = [min(num_pages - body + 1, max(number - padding, main_range[0])), num_pages] + main_range = [ + min(num_pages - body + 1, max(number - padding, main_range[0])), + num_pages, + ] else: trailing = list(range(num_pages - tail + 1, num_pages + 1)) @@ -263,8 +279,10 @@ class DiggPaginator(ExPaginator): page.main_range = list(range(main_range[0], main_range[1] + 1)) page.leading_range = leading page.trailing_range = trailing - page.page_range = reduce(lambda x, y: x + ((x and y) and [False]) + y, - [page.leading_range, page.main_range, page.trailing_range]) + page.page_range = reduce( + lambda x, y: x + ((x and y) and [False]) + y, + [page.leading_range, page.main_range, page.trailing_range], + ) page.__class__ = DiggPage return page @@ -272,10 +290,16 @@ class DiggPaginator(ExPaginator): class DiggPage(Page): def __str__(self): - return " ... ".join(filter(None, [ - " ".join(map(str, self.leading_range)), - " ".join(map(str, self.main_range)), - " ".join(map(str, self.trailing_range))])) + return " ... ".join( + filter( + None, + [ + " ".join(map(str, self.leading_range)), + " ".join(map(str, self.main_range)), + " ".join(map(str, self.trailing_range)), + ], + ) + ) @property def num_pages(self): diff --git a/judge/utils/file_cache.py b/judge/utils/file_cache.py index 3864f59..3b19b4c 100644 --- a/judge/utils/file_cache.py +++ b/judge/utils/file_cache.py @@ -24,10 +24,10 @@ class HashFileCache(object): return os.path.join(self.root, hash, file) def get_url(self, hash, file): - return urljoin(self.url, '%s/%s' % (hash, file)) + return urljoin(self.url, "%s/%s" % (hash, file)) def read_file(self, hash, file): - return open(self.get_path(hash, file), 'rb') + return open(self.get_path(hash, file), "rb") def read_data(self, hash, file): with self.read_file(hash, file) as f: @@ -35,10 +35,10 @@ class HashFileCache(object): def cache_data(self, hash, file, data, url=True, gzip=True): if gzip and self.gzip: - with gzip_open(self.get_path(hash, file + '.gz'), 'wb') as f: + with gzip_open(self.get_path(hash, file + ".gz"), "wb") as f: f.write(data) - with open(self.get_path(hash, file), 'wb') as f: + with open(self.get_path(hash, file), "wb") as f: f.write(data) if url: diff --git a/judge/utils/fine_uploader.py b/judge/utils/fine_uploader.py index b38fcac..a9909a7 100644 --- a/judge/utils/fine_uploader.py +++ b/judge/utils/fine_uploader.py @@ -8,27 +8,25 @@ import os, os.path import tempfile import shutil -__all__ = ( - 'handle_upload', 'save_upload', 'FineUploadForm', 'FineUploadFileInput' -) +__all__ = ("handle_upload", "save_upload", "FineUploadForm", "FineUploadFileInput") def combine_chunks(total_parts, total_size, source_folder, dest): if not os.path.exists(os.path.dirname(dest)): os.makedirs(os.path.dirname(dest)) - with open(dest, 'wb+') as destination: + with open(dest, "wb+") as destination: for i in range(total_parts): part = os.path.join(source_folder, str(i)) - with open(part, 'rb') as source: + with open(part, "rb") as source: destination.write(source.read()) def save_upload(f, path): if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) - with open(path, 'wb+') as destination: - if hasattr(f, 'multiple_chunks') and f.multiple_chunks(): + with open(path, "wb+") as destination: + if hasattr(f, "multiple_chunks") and f.multiple_chunks(): for chunk in f.chunks(): destination.write(chunk) else: @@ -37,29 +35,35 @@ def save_upload(f, path): # pass callback function to post_upload def handle_upload(f, fileattrs, upload_dir, post_upload=None): - chunks_dir = os.path.join(tempfile.gettempdir(), 'chunk_upload_tmp') + chunks_dir = os.path.join(tempfile.gettempdir(), "chunk_upload_tmp") if not os.path.exists(os.path.dirname(chunks_dir)): os.makedirs(os.path.dirname(chunks_dir)) chunked = False dest_folder = upload_dir - dest = os.path.join(dest_folder, fileattrs['qqfilename']) + dest = os.path.join(dest_folder, fileattrs["qqfilename"]) # Chunked - if fileattrs.get('qqtotalparts') and int(fileattrs['qqtotalparts']) > 1: + if fileattrs.get("qqtotalparts") and int(fileattrs["qqtotalparts"]) > 1: chunked = True - dest_folder = os.path.join(chunks_dir, fileattrs['qquuid']) - dest = os.path.join(dest_folder, fileattrs['qqfilename'], str(fileattrs['qqpartindex'])) + dest_folder = os.path.join(chunks_dir, fileattrs["qquuid"]) + dest = os.path.join( + dest_folder, fileattrs["qqfilename"], str(fileattrs["qqpartindex"]) + ) save_upload(f, dest) # If the last chunk has been sent, combine the parts. - if chunked and (fileattrs['qqtotalparts'] - 1 == fileattrs['qqpartindex']): - combine_chunks(fileattrs['qqtotalparts'], - fileattrs['qqtotalfilesize'], + if chunked and (fileattrs["qqtotalparts"] - 1 == fileattrs["qqpartindex"]): + combine_chunks( + fileattrs["qqtotalparts"], + fileattrs["qqtotalfilesize"], source_folder=os.path.dirname(dest), - dest=os.path.join(upload_dir, fileattrs['qqfilename'])) + dest=os.path.join(upload_dir, fileattrs["qqfilename"]), + ) shutil.rmtree(os.path.dirname(os.path.dirname(dest))) - - if post_upload and (not chunked or fileattrs['qqtotalparts'] - 1 == fileattrs['qqpartindex']): + + if post_upload and ( + not chunked or fileattrs["qqtotalparts"] - 1 == fileattrs["qqpartindex"] + ): post_upload() @@ -75,13 +79,16 @@ class FineUploadForm(forms.Form): class FineUploadFileInput(ClearableFileInput): - template_name = 'widgets/fine_uploader.html' + template_name = "widgets/fine_uploader.html" + def fine_uploader_id(self, name): - return name + '_' + 'fine_uploader' + return name + "_" + "fine_uploader" def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context['widget'].update({ - 'fine_uploader_id': self.fine_uploader_id(name), - }) - return context \ No newline at end of file + context["widget"].update( + { + "fine_uploader_id": self.fine_uploader_id(name), + } + ) + return context diff --git a/judge/utils/mathoid.py b/judge/utils/mathoid.py index a28357d..a7ceb24 100644 --- a/judge/utils/mathoid.py +++ b/judge/utils/mathoid.py @@ -12,25 +12,25 @@ from mistune import escape from judge.utils.file_cache import HashFileCache from judge.utils.unicode import utf8bytes, utf8text -logger = logging.getLogger('judge.mathoid') -reescape = re.compile(r'(?'), - ('&', '&'), - ('−', '-'), - ('≤', r'\le'), - ('≥', r'\ge'), - ('…', '...'), - (r'\lt', '<'), - (r'\gt', '>'), + ("\u2264", r"\le"), + ("\u2265", r"\ge"), + ("\u2026", "..."), + ("\u2212", "-"), + ("≤", r"\le"), + ("≥", r"\ge"), + ("<", "<"), + (">", ">"), + ("&", "&"), + ("−", "-"), + ("≤", r"\le"), + ("≥", r"\ge"), + ("…", "..."), + (r"\lt", "<"), + (r"\gt", ">"), ] @@ -41,15 +41,17 @@ def format_math(math): class MathoidMathParser(object): - types = ('svg', 'mml', 'tex', 'jax') + types = ("svg", "mml", "tex", "jax") def __init__(self, type): self.type = type self.mathoid_url = settings.MATHOID_URL - self.cache = HashFileCache(settings.MATHOID_CACHE_ROOT, - settings.MATHOID_CACHE_URL, - settings.MATHOID_GZIP) + self.cache = HashFileCache( + settings.MATHOID_CACHE_ROOT, + settings.MATHOID_CACHE_URL, + settings.MATHOID_GZIP, + ) mml_cache = settings.MATHOID_MML_CACHE self.mml_cache = mml_cache and caches[mml_cache] @@ -61,69 +63,80 @@ class MathoidMathParser(object): self.cache.create(hash) try: - response = requests.post(self.mathoid_url, data={ - 'q': reescape.sub(lambda m: '\\' + m.group(0), formula).encode('utf-8'), - 'type': 'tex' if formula.startswith(r'\displaystyle') else 'inline-tex', - }) + response = requests.post( + self.mathoid_url, + data={ + "q": reescape.sub(lambda m: "\\" + m.group(0), formula).encode( + "utf-8" + ), + "type": "tex" + if formula.startswith(r"\displaystyle") + else "inline-tex", + }, + ) response.raise_for_status() data = response.json() except requests.ConnectionError: - logger.exception('Failed to connect to mathoid for: %s', formula) + logger.exception("Failed to connect to mathoid for: %s", formula) return except requests.HTTPError as e: - logger.error('Mathoid failed to render: %s\n%s', formula, e.response.text) + logger.error("Mathoid failed to render: %s\n%s", formula, e.response.text) return except Exception: - logger.exception('Failed to connect to mathoid for: %s', formula) + logger.exception("Failed to connect to mathoid for: %s", formula) return - if not data['success']: - logger.error('Mathoid failure for: %s\n%s', formula, data) + if not data["success"]: + logger.error("Mathoid failure for: %s\n%s", formula, data) return - if any(i not in data for i in ('mml', 'png', 'svg', 'mathoidStyle')): - logger.error('Mathoid did not return required information (mml, png, svg, mathoidStyle needed):\n%s', data) + if any(i not in data for i in ("mml", "png", "svg", "mathoidStyle")): + logger.error( + "Mathoid did not return required information (mml, png, svg, mathoidStyle needed):\n%s", + data, + ) return - css = data['mathoidStyle'] - mml = data['mml'] + css = data["mathoidStyle"] + mml = data["mml"] result = { - 'css': css, 'mml': mml, - 'png': self.cache.cache_data(hash, 'png', bytearray(data['png']['data'])), - 'svg': self.cache.cache_data(hash, 'svg', data['svg'].encode('utf-8')), + "css": css, + "mml": mml, + "png": self.cache.cache_data(hash, "png", bytearray(data["png"]["data"])), + "svg": self.cache.cache_data(hash, "svg", data["svg"].encode("utf-8")), } - self.cache.cache_data(hash, 'mml', mml.encode('utf-8'), url=False, gzip=False) - self.cache.cache_data(hash, 'css', css.encode('utf-8'), url=False, gzip=False) + self.cache.cache_data(hash, "mml", mml.encode("utf-8"), url=False, gzip=False) + self.cache.cache_data(hash, "css", css.encode("utf-8"), url=False, gzip=False) return result def query_cache(self, hash): result = { - 'svg': self.cache.get_url(hash, 'svg'), - 'png': self.cache.get_url(hash, 'png'), + "svg": self.cache.get_url(hash, "svg"), + "png": self.cache.get_url(hash, "png"), } - key = 'mathoid:css:' + hash - css = result['css'] = self.css_cache.get(key) + key = "mathoid:css:" + hash + css = result["css"] = self.css_cache.get(key) if css is None: - css = result['css'] = self.cache.read_data(hash, 'css').decode('utf-8') + css = result["css"] = self.cache.read_data(hash, "css").decode("utf-8") self.css_cache.set(key, css, self.mml_cache_ttl) mml = None if self.mml_cache: - mml = result['mml'] = self.mml_cache.get('mathoid:mml:' + hash) + mml = result["mml"] = self.mml_cache.get("mathoid:mml:" + hash) if mml is None: - mml = result['mml'] = self.cache.read_data(hash, 'mml').decode('utf-8') + mml = result["mml"] = self.cache.read_data(hash, "mml").decode("utf-8") if self.mml_cache: - self.mml_cache.set('mathoid:mml:' + hash, mml, self.mml_cache_ttl) + self.mml_cache.set("mathoid:mml:" + hash, mml, self.mml_cache_ttl) return result def get_result(self, formula): - if self.type == 'tex': + if self.type == "tex": return hash = hashlib.sha1(utf8bytes(formula)).hexdigest() formula = utf8text(formula) - if self.cache.has_file(hash, 'css'): + if self.cache.has_file(hash, "css"): result = self.query_cache(hash) else: result = self.query_mathoid(formula, hash) @@ -131,55 +144,76 @@ class MathoidMathParser(object): if not result: return None - result['tex'] = formula - result['display'] = formula.startswith(r'\displaystyle') + result["tex"] = formula + result["display"] = formula.startswith(r"\displaystyle") return { - 'mml': self.output_mml, - 'msp': self.output_msp, - 'svg': self.output_svg, - 'jax': self.output_jax, - 'png': self.output_png, - 'raw': lambda x: x, + "mml": self.output_mml, + "msp": self.output_msp, + "svg": self.output_svg, + "jax": self.output_jax, + "png": self.output_png, + "raw": lambda x: x, }[self.type](result) def output_mml(self, result): - return result['mml'] + return result["mml"] def output_msp(self, result): # 100% MediaWiki compatibility. - return format_html('' - '' - '', - mark_safe(result['mml']), result['svg'], result['png'], result['css'], result['tex'], - ['inline', 'display'][result['display']]) + return format_html( + '' + '' + '', + mark_safe(result["mml"]), + result["svg"], + result["png"], + result["css"], + result["tex"], + ["inline", "display"][result["display"]], + ) def output_jax(self, result): - return format_html('' - '''{3}''' - '''''' - '', - result['svg'], result['png'], result['css'], result['tex'], - ['inline-math', 'display-math'][result['display']], ['~', '$$'][result['display']]) + return format_html( + '' + '''{3}""" + """""" + "", + result["svg"], + result["png"], + result["css"], + result["tex"], + ["inline-math", "display-math"][result["display"]], + ["~", "$$"][result["display"]], + ) def output_svg(self, result): - return format_html('{3}''', - result['svg'], result['png'], result['css'], result['tex'], - ['inline-math', 'display-math'][result['display']]) + return format_html( + '{3}""", + result["svg"], + result["png"], + result["css"], + result["tex"], + ["inline-math", "display-math"][result["display"]], + ) def output_png(self, result): - return format_html('{2}', - result['png'], result['css'], result['tex'], - ['inline-math', 'display-math'][result['display']]) + return format_html( + '{2}', + result["png"], + result["css"], + result["tex"], + ["inline-math", "display-math"][result["display"]], + ) def display_math(self, math): math = format_math(math) - return self.get_result(r'\displaystyle ' + math) or r'\[%s\]' % escape(math) + return self.get_result(r"\displaystyle " + math) or r"\[%s\]" % escape(math) def inline_math(self, math): math = format_math(math) - return self.get_result(math) or r'\(%s\)' % escape(math) + return self.get_result(math) or r"\(%s\)" % escape(math) diff --git a/judge/utils/opengraph.py b/judge/utils/opengraph.py index df016c2..a323c90 100644 --- a/judge/utils/opengraph.py +++ b/judge/utils/opengraph.py @@ -10,15 +10,15 @@ def generate_opengraph(cache_key, data, style): if metadata is None: description = None tree = reference(markdown(data, style)).tree - for p in tree.iterfind('.//p'): + for p in tree.iterfind(".//p"): text = p.text_content().strip() if text: description = text break if description: - for remove in (r'\[', r'\]', r'\(', r'\)'): - description = description.replace(remove, '') - img = tree.xpath('.//img') - metadata = truncatewords(description, 60), img[0].get('src') if img else None + for remove in (r"\[", r"\]", r"\(", r"\)"): + description = description.replace(remove, "") + img = tree.xpath(".//img") + metadata = truncatewords(description, 60), img[0].get("src") if img else None cache.set(cache_key, metadata, 86400) return metadata diff --git a/judge/utils/problem_data.py b/judge/utils/problem_data.py index 6dc471c..aeb64cb 100644 --- a/judge/utils/problem_data.py +++ b/judge/utils/problem_data.py @@ -13,13 +13,18 @@ from django.urls import reverse from django.utils.translation import gettext as _ from django.core.cache import cache -VALIDATOR_TEMPLATE_PATH = 'validator_template/template.py' +VALIDATOR_TEMPLATE_PATH = "validator_template/template.py" if os.altsep: - def split_path_first(path, repath=re.compile('[%s]' % re.escape(os.sep + os.altsep))): + + def split_path_first( + path, repath=re.compile("[%s]" % re.escape(os.sep + os.altsep)) + ): return repath.split(path, 1) + else: + def split_path_first(path): return path.split(os.sep, 1) @@ -31,8 +36,8 @@ class ProblemDataStorage(FileSystemStorage): def url(self, name): path = split_path_first(name) if len(path) != 2: - raise ValueError('This file is not accessible via a URL.') - return reverse('problem_data_file', args=path) + raise ValueError("This file is not accessible via a URL.") + return reverse("problem_data_file", args=path) def _save(self, name, content): if self.exists(name): @@ -66,8 +71,8 @@ class ProblemDataCompiler(object): batch = None def end_batch(): - if not batch['batched']: - raise ProblemDataError(_('Empty batches not allowed.')) + if not batch["batched"]: + raise ProblemDataError(_("Empty batches not allowed.")) cases.append(batch) def make_checker_for_validator(case): @@ -75,109 +80,123 @@ class ProblemDataCompiler(object): validator_path = split_path_first(case.custom_validator.name) if len(validator_path) != 2: - raise ProblemDataError(_('How did you corrupt the custom checker path?')) + raise ProblemDataError( + _("How did you corrupt the custom checker path?") + ) - checker = os.path.join(settings.DMOJ_PROBLEM_DATA_ROOT, - validator_path[0], - checker_name) + checker = os.path.join( + settings.DMOJ_PROBLEM_DATA_ROOT, validator_path[0], checker_name + ) validator_name = validator_path[1] shutil.copy(VALIDATOR_TEMPLATE_PATH, checker) # replace {{filecpp}} and {{problemid}} in checker file - filedata = open(checker, 'r').read() - filedata = filedata.replace('{{filecpp}}', "\'%s\'" % validator_name) - filedata = filedata.replace('{{problemid}}', "\'%s\'" % validator_path[0]) - open(checker, 'w').write(filedata) + filedata = open(checker, "r").read() + filedata = filedata.replace("{{filecpp}}", "'%s'" % validator_name) + filedata = filedata.replace("{{problemid}}", "'%s'" % validator_path[0]) + open(checker, "w").write(filedata) return checker_name def make_checker(case): - if (case.checker == 'custom'): + if case.checker == "custom": custom_checker_path = split_path_first(case.custom_checker.name) if len(custom_checker_path) != 2: - raise ProblemDataError(_('How did you corrupt the custom checker path?')) - return(custom_checker_path[1]) + raise ProblemDataError( + _("How did you corrupt the custom checker path?") + ) + return custom_checker_path[1] - if (case.checker == 'customval'): + if case.checker == "customval": return make_checker_for_validator(case) if case.checker_args: return { - 'name': case.checker, - 'args': json.loads(case.checker_args), + "name": case.checker, + "args": json.loads(case.checker_args), } return case.checker for i, case in enumerate(self.cases, 1): - if case.type == 'C': + if case.type == "C": data = {} if batch: case.points = None - case.is_pretest = batch['is_pretest'] + case.is_pretest = batch["is_pretest"] else: if case.points is None: - raise ProblemDataError(_('Points must be defined for non-batch case #%d.') % i) - data['is_pretest'] = case.is_pretest + raise ProblemDataError( + _("Points must be defined for non-batch case #%d.") % i + ) + data["is_pretest"] = case.is_pretest if not self.generator: if case.input_file not in self.files: - raise ProblemDataError(_('Input file for case %d does not exist: %s') % - (i, case.input_file)) + raise ProblemDataError( + _("Input file for case %d does not exist: %s") + % (i, case.input_file) + ) if case.output_file not in self.files: - raise ProblemDataError(_('Output file for case %d does not exist: %s') % - (i, case.output_file)) + raise ProblemDataError( + _("Output file for case %d does not exist: %s") + % (i, case.output_file) + ) if case.input_file: - data['in'] = case.input_file + data["in"] = case.input_file if case.output_file: - data['out'] = case.output_file + data["out"] = case.output_file if case.points is not None: - data['points'] = case.points + data["points"] = case.points if case.generator_args: - data['generator_args'] = case.generator_args.splitlines() + data["generator_args"] = case.generator_args.splitlines() if case.output_limit is not None: - data['output_limit_length'] = case.output_limit + data["output_limit_length"] = case.output_limit if case.output_prefix is not None: - data['output_prefix_length'] = case.output_prefix + data["output_prefix_length"] = case.output_prefix if case.checker: - data['checker'] = make_checker(case) + data["checker"] = make_checker(case) else: - case.checker_args = '' - case.save(update_fields=('checker_args', 'is_pretest')) - (batch['batched'] if batch else cases).append(data) - elif case.type == 'S': + case.checker_args = "" + case.save(update_fields=("checker_args", "is_pretest")) + (batch["batched"] if batch else cases).append(data) + elif case.type == "S": if batch: end_batch() if case.points is None: - raise ProblemDataError(_('Batch start case #%d requires points.') % i) + raise ProblemDataError( + _("Batch start case #%d requires points.") % i + ) batch = { - 'points': case.points, - 'batched': [], - 'is_pretest': case.is_pretest, + "points": case.points, + "batched": [], + "is_pretest": case.is_pretest, } if case.generator_args: - batch['generator_args'] = case.generator_args.splitlines() + batch["generator_args"] = case.generator_args.splitlines() if case.output_limit is not None: - batch['output_limit_length'] = case.output_limit + batch["output_limit_length"] = case.output_limit if case.output_prefix is not None: - batch['output_prefix_length'] = case.output_prefix + batch["output_prefix_length"] = case.output_prefix if case.checker: - batch['checker'] = make_checker(case) + batch["checker"] = make_checker(case) else: - case.checker_args = '' - case.input_file = '' - case.output_file = '' - case.save(update_fields=('checker_args', 'input_file', 'output_file')) - elif case.type == 'E': + case.checker_args = "" + case.input_file = "" + case.output_file = "" + case.save(update_fields=("checker_args", "input_file", "output_file")) + elif case.type == "E": if not batch: - raise ProblemDataError(_('Attempt to end batch outside of one in case #%d') % i) - case.is_pretest = batch['is_pretest'] - case.input_file = '' - case.output_file = '' - case.generator_args = '' - case.checker = '' - case.checker_args = '' + raise ProblemDataError( + _("Attempt to end batch outside of one in case #%d") % i + ) + case.is_pretest = batch["is_pretest"] + case.input_file = "" + case.output_file = "" + case.generator_args = "" + case.checker = "" + case.checker_args = "" case.save() end_batch() batch = None @@ -189,44 +208,44 @@ class ProblemDataCompiler(object): if self.data.zipfile: zippath = split_path_first(self.data.zipfile.name) if len(zippath) != 2: - raise ProblemDataError(_('How did you corrupt the zip path?')) - init['archive'] = zippath[1] + raise ProblemDataError(_("How did you corrupt the zip path?")) + init["archive"] = zippath[1] if self.generator: generator_path = split_path_first(self.generator.name) if len(generator_path) != 2: - raise ProblemDataError(_('How did you corrupt the generator path?')) - init['generator'] = generator_path[1] + raise ProblemDataError(_("How did you corrupt the generator path?")) + init["generator"] = generator_path[1] - pretests = [case for case in cases if case['is_pretest']] + pretests = [case for case in cases if case["is_pretest"]] for case in cases: - del case['is_pretest'] + del case["is_pretest"] if pretests: - init['pretest_test_cases'] = pretests + init["pretest_test_cases"] = pretests if cases: - init['test_cases'] = cases + init["test_cases"] = cases if self.data.output_limit is not None: - init['output_limit_length'] = self.data.output_limit + init["output_limit_length"] = self.data.output_limit if self.data.output_prefix is not None: - init['output_prefix_length'] = self.data.output_prefix + init["output_prefix_length"] = self.data.output_prefix if self.data.checker: - if self.data.checker == 'interact': - init['interactive'] = { - 'files': split_path_first(self.data.interactive_judge.name)[1], - 'feedback': True + if self.data.checker == "interact": + init["interactive"] = { + "files": split_path_first(self.data.interactive_judge.name)[1], + "feedback": True, } - init['unbuffered'] = True + init["unbuffered"] = True else: - init['checker'] = make_checker(self.data) + init["checker"] = make_checker(self.data) else: - self.data.checker_args = '' + self.data.checker_args = "" return init def compile(self): from judge.models import problem_data_storage - yml_file = '%s/init.yml' % self.problem.code + yml_file = "%s/init.yml" % self.problem.code try: init = yaml.safe_dump(self.make_init()) except ProblemDataError as e: @@ -234,7 +253,7 @@ class ProblemDataCompiler(object): self.data.save() problem_data_storage.delete(yml_file) else: - self.data.feedback = '' + self.data.feedback = "" self.data.save() problem_data_storage.save(yml_file, ContentFile(init)) @@ -245,26 +264,27 @@ class ProblemDataCompiler(object): def get_visible_content(data): - data = data or b'' - data = data.replace(b'\r\n', b'\r').replace(b'\r', b'\n') + data = data or b"" + data = data.replace(b"\r\n", b"\r").replace(b"\r", b"\n") - data = data.decode('utf-8') + data = data.decode("utf-8") - if (len(data) > settings.TESTCASE_VISIBLE_LENGTH): - data = data[:settings.TESTCASE_VISIBLE_LENGTH] - data += '.' * 3 + if len(data) > settings.TESTCASE_VISIBLE_LENGTH: + data = data[: settings.TESTCASE_VISIBLE_LENGTH] + data += "." * 3 return data def get_file_cachekey(file): return hashlib.sha1(file.encode()).hexdigest() + def get_problem_case(problem, files): result = {} uncached_files = [] for file in files: - cache_key = 'problem_archive:%s:%s' % (problem.code, get_file_cachekey(file)) + cache_key = "problem_archive:%s:%s" % (problem.code, get_file_cachekey(file)) qs = cache.get(cache_key) if qs is None: uncached_files.append(file) @@ -274,33 +294,33 @@ def get_problem_case(problem, files): if not uncached_files: return result - archive_path = os.path.join(settings.DMOJ_PROBLEM_DATA_ROOT, - str(problem.data_files.zipfile)) + archive_path = os.path.join( + settings.DMOJ_PROBLEM_DATA_ROOT, str(problem.data_files.zipfile) + ) if not os.path.exists(archive_path): - raise Exception( - 'archive file "%s" does not exist' % archive_path) + raise Exception('archive file "%s" does not exist' % archive_path) try: - archive = zipfile.ZipFile(archive_path, 'r') + archive = zipfile.ZipFile(archive_path, "r") except zipfile.BadZipfile: raise Exception('bad archive: "%s"' % archive_path) for file in uncached_files: - cache_key = 'problem_archive:%s:%s' % (problem.code, get_file_cachekey(file)) + cache_key = "problem_archive:%s:%s" % (problem.code, get_file_cachekey(file)) with archive.open(file) as f: s = f.read(settings.TESTCASE_VISIBLE_LENGTH + 3) # add this so there are no characters left behind (ex, 'á' = 2 utf-8 chars) while True: try: - s.decode('utf-8') + s.decode("utf-8") break except UnicodeDecodeError: next_char = f.read(1) if next_char: s += next_char else: - raise Exception('File %s is not able to decode in utf-8' % file) + raise Exception("File %s is not able to decode in utf-8" % file) qs = get_visible_content(s) cache.set(cache_key, qs, 86400) result[file] = qs - return result \ No newline at end of file + return result diff --git a/judge/utils/problems.py b/judge/utils/problems.py index aacb916..73d53d0 100644 --- a/judge/utils/problems.py +++ b/judge/utils/problems.py @@ -11,79 +11,120 @@ from django.utils.translation import gettext as _, gettext_noop from judge.models import Problem, Submission -__all__ = ['contest_completed_ids', 'get_result_data', 'user_completed_ids', 'user_authored_ids', 'user_editable_ids'] +__all__ = [ + "contest_completed_ids", + "get_result_data", + "user_completed_ids", + "user_authored_ids", + "user_editable_ids", +] def user_authored_ids(profile): - result = set(Problem.objects.filter(authors=profile).values_list('id', flat=True)) + result = set(Problem.objects.filter(authors=profile).values_list("id", flat=True)) return result def user_editable_ids(profile): - result = set((Problem.objects.filter(authors=profile) | Problem.objects.filter(curators=profile)) - .values_list('id', flat=True)) + result = set( + ( + Problem.objects.filter(authors=profile) + | Problem.objects.filter(curators=profile) + ).values_list("id", flat=True) + ) return result def contest_completed_ids(participation): - key = 'contest_complete:%d' % participation.id + key = "contest_complete:%d" % participation.id result = cache.get(key) if result is None: - result = set(participation.submissions.filter(submission__result='AC', points=F('problem__points')) - .values_list('problem__problem__id', flat=True).distinct()) + result = set( + participation.submissions.filter( + submission__result="AC", points=F("problem__points") + ) + .values_list("problem__problem__id", flat=True) + .distinct() + ) cache.set(key, result, 86400) return result def user_completed_ids(profile): - key = 'user_complete:%d' % profile.id + key = "user_complete:%d" % profile.id result = cache.get(key) if result is None: - result = set(Submission.objects.filter(user=profile, result='AC', points=F('problem__points')) - .values_list('problem_id', flat=True).distinct()) + result = set( + Submission.objects.filter( + user=profile, result="AC", points=F("problem__points") + ) + .values_list("problem_id", flat=True) + .distinct() + ) cache.set(key, result, 86400) return result def contest_attempted_ids(participation): - key = 'contest_attempted:%s' % participation.id + key = "contest_attempted:%s" % participation.id result = cache.get(key) if result is None: - result = {id: {'achieved_points': points, 'max_points': max_points} - for id, max_points, points in (participation.submissions - .values_list('problem__problem__id', 'problem__points') - .annotate(points=Max('points')) - .filter(points__lt=F('problem__points')))} + result = { + id: {"achieved_points": points, "max_points": max_points} + for id, max_points, points in ( + participation.submissions.values_list( + "problem__problem__id", "problem__points" + ) + .annotate(points=Max("points")) + .filter(points__lt=F("problem__points")) + ) + } cache.set(key, result, 86400) return result def user_attempted_ids(profile): - key = 'user_attempted:%s' % profile.id + key = "user_attempted:%s" % profile.id result = cache.get(key) if result is None: - result = {id: {'achieved_points': points, 'max_points': max_points} - for id, max_points, points in (Submission.objects.filter(user=profile) - .values_list('problem__id', 'problem__points') - .annotate(points=Max('points')) - .filter(points__lt=F('problem__points')))} + result = { + id: {"achieved_points": points, "max_points": max_points} + for id, max_points, points in ( + Submission.objects.filter(user=profile) + .values_list("problem__id", "problem__points") + .annotate(points=Max("points")) + .filter(points__lt=F("problem__points")) + ) + } cache.set(key, result, 86400) return result def _get_result_data(results): return { - 'categories': [ + "categories": [ # Using gettext_noop here since this will be tacked into the cache, so it must be language neutral. # The caller, SubmissionList.get_result_data will run ugettext on the name. - {'code': 'AC', 'name': gettext_noop('Accepted'), 'count': results['AC']}, - {'code': 'WA', 'name': gettext_noop('Wrong'), 'count': results['WA']}, - {'code': 'CE', 'name': gettext_noop('Compile Error'), 'count': results['CE']}, - {'code': 'TLE', 'name': gettext_noop('Timeout'), 'count': results['TLE']}, - {'code': 'ERR', 'name': gettext_noop('Error'), - 'count': results['MLE'] + results['OLE'] + results['IR'] + results['RTE'] + results['AB'] + results['IE']}, + {"code": "AC", "name": gettext_noop("Accepted"), "count": results["AC"]}, + {"code": "WA", "name": gettext_noop("Wrong"), "count": results["WA"]}, + { + "code": "CE", + "name": gettext_noop("Compile Error"), + "count": results["CE"], + }, + {"code": "TLE", "name": gettext_noop("Timeout"), "count": results["TLE"]}, + { + "code": "ERR", + "name": gettext_noop("Error"), + "count": results["MLE"] + + results["OLE"] + + results["IR"] + + results["RTE"] + + results["AB"] + + results["IE"], + }, ], - 'total': sum(results.values()), + "total": sum(results.values()), } @@ -93,8 +134,16 @@ def get_result_data(*args, **kwargs): if kwargs: raise ValueError(_("Can't pass both queryset and keyword filters")) else: - submissions = Submission.objects.filter(**kwargs) if kwargs is not None else Submission.objects - raw = submissions.values('result').annotate(count=Count('result')).values_list('result', 'count') + submissions = ( + Submission.objects.filter(**kwargs) + if kwargs is not None + else Submission.objects + ) + raw = ( + submissions.values("result") + .annotate(count=Count("result")) + .values_list("result", "count") + ) return _get_result_data(defaultdict(int, raw)) @@ -102,48 +151,73 @@ def editable_problems(user, profile=None): subquery = Problem.objects.all() if profile is None: profile = user.profile - if not user.has_perm('judge.edit_all_problem'): + if not user.has_perm("judge.edit_all_problem"): subfilter = Q(authors__id=profile.id) | Q(curators__id=profile.id) - if user.has_perm('judge.edit_public_problem'): + if user.has_perm("judge.edit_public_problem"): subfilter |= Q(is_public=True) subquery = subquery.filter(subfilter) return subquery def hot_problems(duration, limit): - cache_key = 'hot_problems:%d:%d' % (duration.total_seconds(), limit) + cache_key = "hot_problems:%d:%d" % (duration.total_seconds(), limit) qs = cache.get(cache_key) if qs is None: - qs = Problem.get_public_problems() \ - .filter(submission__date__gt=timezone.now() - duration) - qs0 = qs.annotate(k=Count('submission__user', distinct=True)).order_by('-k').values_list('k', flat=True) + qs = Problem.get_public_problems().filter( + submission__date__gt=timezone.now() - duration + ) + qs0 = ( + qs.annotate(k=Count("submission__user", distinct=True)) + .order_by("-k") + .values_list("k", flat=True) + ) if not qs0: return [] # make this an aggregate mx = float(qs0[0]) - qs = qs.annotate(unique_user_count=Count('submission__user', distinct=True)) + qs = qs.annotate(unique_user_count=Count("submission__user", distinct=True)) # fix braindamage in excluding CE - qs = qs.annotate(submission_volume=Count(Case( - When(submission__result='AC', then=1), - When(submission__result='WA', then=1), - When(submission__result='IR', then=1), - When(submission__result='RTE', then=1), - When(submission__result='TLE', then=1), - When(submission__result='OLE', then=1), - output_field=FloatField(), - ))) - qs = qs.annotate(ac_volume=Count(Case( - When(submission__result='AC', then=1), - output_field=FloatField(), - ))) + qs = qs.annotate( + submission_volume=Count( + Case( + When(submission__result="AC", then=1), + When(submission__result="WA", then=1), + When(submission__result="IR", then=1), + When(submission__result="RTE", then=1), + When(submission__result="TLE", then=1), + When(submission__result="OLE", then=1), + output_field=FloatField(), + ) + ) + ) + qs = qs.annotate( + ac_volume=Count( + Case( + When(submission__result="AC", then=1), + output_field=FloatField(), + ) + ) + ) qs = qs.filter(unique_user_count__gt=max(mx / 3.0, 1)) - qs = qs.annotate(ordering=ExpressionWrapper( - 0.02 * F('points') * (0.4 * F('ac_volume') / F('submission_volume') + 0.6 * F('ac_rate')) + - 100 * e ** (F('unique_user_count') / mx), output_field=FloatField(), - )).order_by('-ordering').defer('description')[:limit] + qs = ( + qs.annotate( + ordering=ExpressionWrapper( + 0.02 + * F("points") + * ( + 0.4 * F("ac_volume") / F("submission_volume") + + 0.6 * F("ac_rate") + ) + + 100 * e ** (F("unique_user_count") / mx), + output_field=FloatField(), + ) + ) + .order_by("-ordering") + .defer("description")[:limit] + ) cache.set(cache_key, qs, 900) - return qs \ No newline at end of file + return qs diff --git a/judge/utils/pwned.py b/judge/utils/pwned.py index fa3df63..0070003 100644 --- a/judge/utils/pwned.py +++ b/judge/utils/pwned.py @@ -47,7 +47,7 @@ from judge.utils.unicode import utf8bytes log = logging.getLogger(__name__) -API_ENDPOINT = 'https://api.pwnedpasswords.com/range/{}' +API_ENDPOINT = "https://api.pwnedpasswords.com/range/{}" REQUEST_TIMEOUT = 2.0 # 2 seconds @@ -61,19 +61,19 @@ def _get_pwned(prefix): url=API_ENDPOINT.format(prefix), timeout=getattr( settings, - 'PWNED_PASSWORDS_API_TIMEOUT', + "PWNED_PASSWORDS_API_TIMEOUT", REQUEST_TIMEOUT, ), ) response.raise_for_status() except requests.RequestException: # Gracefully handle timeouts and HTTP error response codes. - log.warning('Skipped Pwned Passwords check due to error', exc_info=True) + log.warning("Skipped Pwned Passwords check due to error", exc_info=True) return None results = {} for line in response.text.splitlines(): - line_suffix, _, times = line.partition(':') + line_suffix, _, times = line.partition(":") results[line_suffix] = int(times) return results @@ -84,7 +84,7 @@ def pwned_password(password): Checks a password against the Pwned Passwords database. """ if not isinstance(password, string_types): - raise TypeError('Password values to check must be strings.') + raise TypeError("Password values to check must be strings.") password_hash = hashlib.sha1(utf8bytes(password)).hexdigest().upper() prefix, suffix = password_hash[:5], password_hash[5:] results = _get_pwned(prefix) @@ -98,8 +98,9 @@ class PwnedPasswordsValidator(object): """ Password validator which checks the Pwned Passwords database. """ + DEFAULT_HELP_MESSAGE = _("Your password can't be a commonly used password.") - DEFAULT_PWNED_MESSAGE = _('This password is too common.') + DEFAULT_PWNED_MESSAGE = _("This password is too common.") def __init__(self, error_message=None, help_message=None): self.help_message = help_message or self.DEFAULT_HELP_MESSAGE @@ -111,8 +112,8 @@ class PwnedPasswordsValidator(object): else: singular, plural = error_message self.error_message = { - 'singular': singular, - 'plural': plural, + "singular": singular, + "plural": plural, } def validate(self, password, user=None): @@ -125,12 +126,12 @@ class PwnedPasswordsValidator(object): elif amount: raise ValidationError( ungettext( - self.error_message['singular'], - self.error_message['plural'], + self.error_message["singular"], + self.error_message["plural"], amount, ), - params={'amount': amount}, - code='pwned_password', + params={"amount": amount}, + code="pwned_password", ) def get_help_text(self): diff --git a/judge/utils/ranker.py b/judge/utils/ranker.py index 52f0e4c..3f15e62 100644 --- a/judge/utils/ranker.py +++ b/judge/utils/ranker.py @@ -1,7 +1,7 @@ from operator import attrgetter -def ranker(iterable, key=attrgetter('points'), rank=0): +def ranker(iterable, key=attrgetter("points"), rank=0): delta = 1 last = None for item in iterable: @@ -12,4 +12,3 @@ def ranker(iterable, key=attrgetter('points'), rank=0): delta += 1 yield rank, item last = key(item) - diff --git a/judge/utils/raw_sql.py b/judge/utils/raw_sql.py index edbca4c..3111e3b 100644 --- a/judge/utils/raw_sql.py +++ b/judge/utils/raw_sql.py @@ -10,13 +10,18 @@ from django.utils import six from judge.utils.cachedict import CacheDict -def unique_together_left_join(queryset, model, link_field_name, filter_field_name, filter_value, parent_model=None): +def unique_together_left_join( + queryset, model, link_field_name, filter_field_name, filter_value, parent_model=None +): link_field = copy(model._meta.get_field(link_field_name).remote_field) filter_field = model._meta.get_field(filter_field_name) def restrictions(where_class, alias, related_alias): cond = where_class() - cond.add(filter_field.get_lookup('exact')(filter_field.get_col(alias), filter_value), 'AND') + cond.add( + filter_field.get_lookup("exact")(filter_field.get_col(alias), filter_value), + "AND", + ) return cond link_field.get_extra_restriction = restrictions @@ -25,17 +30,36 @@ def unique_together_left_join(queryset, model, link_field_name, filter_field_nam parent_alias = parent_model._meta.db_table else: parent_alias = queryset.query.get_initial_alias() - return queryset.query.join(Join(model._meta.db_table, parent_alias, None, LOUTER, link_field, True)) + return queryset.query.join( + Join(model._meta.db_table, parent_alias, None, LOUTER, link_field, True) + ) class RawSQLJoin(Join): - def __init__(self, subquery, subquery_params, parent_alias, table_alias, join_type, join_field, nullable, - filtered_relation=None): + def __init__( + self, + subquery, + subquery_params, + parent_alias, + table_alias, + join_type, + join_field, + nullable, + filtered_relation=None, + ): self.subquery_params = subquery_params - super().__init__(subquery, parent_alias, table_alias, join_type, join_field, nullable, filtered_relation) + super().__init__( + subquery, + parent_alias, + table_alias, + join_type, + join_field, + nullable, + filtered_relation, + ) def as_sql(self, compiler, connection): - compiler.quote_cache[self.table_name] = '(%s)' % self.table_name + compiler.quote_cache[self.table_name] = "(%s)" % self.table_name sql, params = super().as_sql(compiler, connection) return sql, self.subquery_params + params @@ -51,13 +75,23 @@ class FakeJoinField: pass -def join_sql_subquery(queryset, subquery, params, join_fields, alias, join_type=INNER, parent_model=None): +def join_sql_subquery( + queryset, subquery, params, join_fields, alias, join_type=INNER, parent_model=None +): if parent_model is not None: parent_alias = parent_model._meta.db_table else: parent_alias = queryset.query.get_initial_alias() queryset.query.external_aliases.add(alias) - join = RawSQLJoin(subquery, params, parent_alias, alias, join_type, FakeJoinField(join_fields), join_type == LOUTER) + join = RawSQLJoin( + subquery, + params, + parent_alias, + alias, + join_type, + FakeJoinField(join_fields), + join_type == LOUTER, + ) queryset.query.join(join) join.table_alias = alias @@ -68,7 +102,7 @@ def RawSQLColumn(model, field=None): model = field.model if isinstance(field, six.string_types): field = model._meta.get_field(field) - return RawSQL('%s.%s' % (model._meta.db_table, field.get_attname_column()[1]), ()) + return RawSQL("%s.%s" % (model._meta.db_table, field.get_attname_column()[1]), ()) def make_straight_join_query(QueryType): @@ -77,7 +111,7 @@ def make_straight_join_query(QueryType): alias = super().join(join, *args, **kwargs) join = self.alias_map[alias] if join.join_type == INNER: - join.join_type = 'STRAIGHT_JOIN' + join.join_type = "STRAIGHT_JOIN" return alias return Query @@ -87,7 +121,7 @@ straight_join_cache = CacheDict(make_straight_join_query) def use_straight_join(queryset): - if connections[queryset.db].vendor != 'mysql': + if connections[queryset.db].vendor != "mysql": return try: cloner = queryset.query.chain diff --git a/judge/utils/recaptcha.py b/judge/utils/recaptcha.py index 331a12d..9be7e5f 100644 --- a/judge/utils/recaptcha.py +++ b/judge/utils/recaptcha.py @@ -6,6 +6,7 @@ except ImportError: ReCaptchaWidget = None else: from django.conf import settings - if not hasattr(settings, 'RECAPTCHA_PRIVATE_KEY'): + + if not hasattr(settings, "RECAPTCHA_PRIVATE_KEY"): ReCaptchaField = None ReCaptchaWidget = None diff --git a/judge/utils/stats.py b/judge/utils/stats.py index ba923aa..6c79d8c 100644 --- a/judge/utils/stats.py +++ b/judge/utils/stats.py @@ -1,10 +1,30 @@ from operator import itemgetter -__all__ = ('chart_colors', 'highlight_colors', 'get_pie_chart', 'get_bar_chart') +__all__ = ("chart_colors", "highlight_colors", "get_pie_chart", "get_bar_chart") -chart_colors = [0x3366CC, 0xDC3912, 0xFF9900, 0x109618, 0x990099, 0x3B3EAC, 0x0099C6, 0xDD4477, 0x66AA00, 0xB82E2E, - 0x316395, 0x994499, 0x22AA99, 0xAAAA11, 0x6633CC, 0xE67300, 0x8B0707, 0x329262, 0x5574A6, 0x3B3EAC] +chart_colors = [ + 0x3366CC, + 0xDC3912, + 0xFF9900, + 0x109618, + 0x990099, + 0x3B3EAC, + 0x0099C6, + 0xDD4477, + 0x66AA00, + 0xB82E2E, + 0x316395, + 0x994499, + 0x22AA99, + 0xAAAA11, + 0x6633CC, + 0xE67300, + 0x8B0707, + 0x329262, + 0x5574A6, + 0x3B3EAC, +] highlight_colors = [] @@ -13,25 +33,26 @@ highlight_colors = [] def _highlight_colors(): for color in chart_colors: r, g, b = color >> 16, (color >> 8) & 0xFF, color & 0xFF - highlight_colors.append('#%02X%02X%02X' % (min(int(r * 1.2), 255), - min(int(g * 1.2), 255), - min(int(b * 1.2), 255))) + highlight_colors.append( + "#%02X%02X%02X" + % (min(int(r * 1.2), 255), min(int(g * 1.2), 255), min(int(b * 1.2), 255)) + ) _highlight_colors() -chart_colors = list(map('#%06X'.__mod__, chart_colors)) +chart_colors = list(map("#%06X".__mod__, chart_colors)) def get_pie_chart(data): return { - 'labels': list(map(itemgetter(0), data)), - 'datasets': [ + "labels": list(map(itemgetter(0), data)), + "datasets": [ { - 'backgroundColor': chart_colors, - 'highlightBackgroundColor': highlight_colors, - 'data': list(map(itemgetter(1), data)), + "backgroundColor": chart_colors, + "highlightBackgroundColor": highlight_colors, + "data": list(map(itemgetter(1), data)), }, ], } @@ -39,30 +60,39 @@ def get_pie_chart(data): def get_bar_chart(data, **kwargs): return { - 'labels': list(map(itemgetter(0), data)), - 'datasets': [ + "labels": list(map(itemgetter(0), data)), + "datasets": [ { - 'backgroundColor': kwargs.get('fillColor', 'rgba(151,187,205,0.5)'), - 'borderColor': kwargs.get('strokeColor', 'rgba(151,187,205,0.8)'), - 'borderWidth': 1, - 'hoverBackgroundColor': kwargs.get('highlightFill', 'rgba(151,187,205,0.75)'), - 'hoverBorderColor': kwargs.get('highlightStroke', 'rgba(151,187,205,1)'), - 'data': list(map(itemgetter(1), data)), + "backgroundColor": kwargs.get("fillColor", "rgba(151,187,205,0.5)"), + "borderColor": kwargs.get("strokeColor", "rgba(151,187,205,0.8)"), + "borderWidth": 1, + "hoverBackgroundColor": kwargs.get( + "highlightFill", "rgba(151,187,205,0.75)" + ), + "hoverBorderColor": kwargs.get( + "highlightStroke", "rgba(151,187,205,1)" + ), + "data": list(map(itemgetter(1), data)), }, ], } + def get_histogram(data, **kwargs): return { - 'labels': [round(i, 1) for i in list(map(itemgetter(0), data))], - 'datasets': [ + "labels": [round(i, 1) for i in list(map(itemgetter(0), data))], + "datasets": [ { - 'backgroundColor': kwargs.get('fillColor', 'rgba(151,187,205,0.5)'), - 'borderColor': kwargs.get('strokeColor', 'rgba(151,187,205,0.8)'), - 'borderWidth': 1, - 'hoverBackgroundColor': kwargs.get('highlightFill', 'rgba(151,187,205,0.75)'), - 'hoverBorderColor': kwargs.get('highlightStroke', 'rgba(151,187,205,1)'), - 'data': list(map(itemgetter(1), data)), + "backgroundColor": kwargs.get("fillColor", "rgba(151,187,205,0.5)"), + "borderColor": kwargs.get("strokeColor", "rgba(151,187,205,0.8)"), + "borderWidth": 1, + "hoverBackgroundColor": kwargs.get( + "highlightFill", "rgba(151,187,205,0.75)" + ), + "hoverBorderColor": kwargs.get( + "highlightStroke", "rgba(151,187,205,1)" + ), + "data": list(map(itemgetter(1), data)), }, ], - } \ No newline at end of file + } diff --git a/judge/utils/subscription.py b/judge/utils/subscription.py index 1129b0d..883d905 100644 --- a/judge/utils/subscription.py +++ b/judge/utils/subscription.py @@ -1,8 +1,10 @@ from django.conf import settings -if 'newsletter' in settings.INSTALLED_APPS: +if "newsletter" in settings.INSTALLED_APPS: from newsletter.models import Subscription else: Subscription = None -newsletter_id = None if Subscription is None else settings.DMOJ_NEWSLETTER_ID_ON_REGISTER +newsletter_id = ( + None if Subscription is None else settings.DMOJ_NEWSLETTER_ID_ON_REGISTER +) diff --git a/judge/utils/texoid.py b/judge/utils/texoid.py index d668066..cb51169 100644 --- a/judge/utils/texoid.py +++ b/judge/utils/texoid.py @@ -10,16 +10,16 @@ from django.core.cache import caches from judge.utils.file_cache import HashFileCache from judge.utils.unicode import utf8bytes -logger = logging.getLogger('judge.texoid') +logger = logging.getLogger("judge.texoid") -TEXOID_ENABLED = hasattr(settings, 'TEXOID_URL') +TEXOID_ENABLED = hasattr(settings, "TEXOID_URL") class TexoidRenderer(object): def __init__(self): - self.cache = HashFileCache(settings.TEXOID_CACHE_ROOT, - settings.TEXOID_CACHE_URL, - settings.TEXOID_GZIP) + self.cache = HashFileCache( + settings.TEXOID_CACHE_ROOT, settings.TEXOID_CACHE_URL, settings.TEXOID_GZIP + ) self.meta_cache = caches[settings.TEXOID_META_CACHE] self.meta_cache_ttl = settings.TEXOID_META_CACHE_TTL @@ -27,59 +27,69 @@ class TexoidRenderer(object): self.cache.create(hash) try: - response = requests.post(settings.TEXOID_URL, data=utf8bytes(document), headers={ - 'Content-Type': 'application/x-tex', - }) + response = requests.post( + settings.TEXOID_URL, + data=utf8bytes(document), + headers={ + "Content-Type": "application/x-tex", + }, + ) response.raise_for_status() except requests.HTTPError as e: if e.response.status == 400: - logger.error('Texoid failed to render: %s\n%s', document, e.response.text) + logger.error( + "Texoid failed to render: %s\n%s", document, e.response.text + ) else: - logger.exception('Failed to connect to texoid for: %s', document) + logger.exception("Failed to connect to texoid for: %s", document) return except Exception: - logger.exception('Failed to connect to texoid for: %s', document) + logger.exception("Failed to connect to texoid for: %s", document) return try: data = response.json() except ValueError: - logger.exception('Invalid texoid response for: %s\n%s', document, response.text) + logger.exception( + "Invalid texoid response for: %s\n%s", document, response.text + ) return - if not data['success']: - logger.error('Texoid failure for: %s\n%s', document, data) - return {'error': data['error']} + if not data["success"]: + logger.error("Texoid failure for: %s\n%s", document, data) + return {"error": data["error"]} - meta = data['meta'] - self.cache.cache_data(hash, 'meta', utf8bytes(json.dumps(meta)), url=False, gzip=False) + meta = data["meta"] + self.cache.cache_data( + hash, "meta", utf8bytes(json.dumps(meta)), url=False, gzip=False + ) result = { - 'png': self.cache.cache_data(hash, 'png', b64decode(data['png'])), - 'svg': self.cache.cache_data(hash, 'svg', data['svg'].encode('utf-8')), - 'meta': meta, + "png": self.cache.cache_data(hash, "png", b64decode(data["png"])), + "svg": self.cache.cache_data(hash, "svg", data["svg"].encode("utf-8")), + "meta": meta, } return result def query_cache(self, hash): result = { - 'svg': self.cache.get_url(hash, 'svg'), - 'png': self.cache.get_url(hash, 'png'), + "svg": self.cache.get_url(hash, "svg"), + "png": self.cache.get_url(hash, "png"), } - key = 'texoid:meta:' + hash + key = "texoid:meta:" + hash cached_meta = self.meta_cache.get(key) if cached_meta is None: - cached_meta = json.loads(self.cache.read_data(hash, 'meta').decode('utf-8')) + cached_meta = json.loads(self.cache.read_data(hash, "meta").decode("utf-8")) self.meta_cache.set(key, cached_meta, self.meta_cache_ttl) - result['meta'] = cached_meta + result["meta"] = cached_meta return result def get_result(self, formula): hash = hashlib.sha1(utf8bytes(formula)).hexdigest() - if self.cache.has_file(hash, 'svg'): + if self.cache.has_file(hash, "svg"): return self.query_cache(hash) else: return self.query_texoid(formula, hash) diff --git a/judge/utils/tickets.py b/judge/utils/tickets.py index 1743db4..b630e60 100644 --- a/judge/utils/tickets.py +++ b/judge/utils/tickets.py @@ -12,6 +12,10 @@ def own_ticket_filter(profile_id): def filter_visible_tickets(queryset, user, profile=None): if profile is None: profile = user.profile - return queryset.filter(own_ticket_filter(profile.id) | - Q(content_type=ContentType.objects.get_for_model(Problem), - object_id__in=editable_problems(user, profile))).distinct() + return queryset.filter( + own_ticket_filter(profile.id) + | Q( + content_type=ContentType.objects.get_for_model(Problem), + object_id__in=editable_problems(user, profile), + ) + ).distinct() diff --git a/judge/utils/timedelta.py b/judge/utils/timedelta.py index 292caeb..adf3330 100644 --- a/judge/utils/timedelta.py +++ b/judge/utils/timedelta.py @@ -3,7 +3,7 @@ import datetime from django.utils.translation import npgettext, pgettext, ungettext -def nice_repr(timedelta, display='long', sep=', '): +def nice_repr(timedelta, display="long", sep=", "): """ Turns a datetime.timedelta object into a nice string repr. @@ -16,7 +16,9 @@ def nice_repr(timedelta, display='long', sep=', '): '1d, 1s' """ - assert isinstance(timedelta, datetime.timedelta), 'First argument must be a timedelta.' + assert isinstance( + timedelta, datetime.timedelta + ), "First argument must be a timedelta." result = [] @@ -26,65 +28,94 @@ def nice_repr(timedelta, display='long', sep=', '): minutes = (timedelta.seconds % 3600) // 60 seconds = timedelta.seconds % 60 - if display == 'simple-no-seconds': + if display == "simple-no-seconds": days += weeks * 7 if days: if hours or minutes: - return '%d day%s %d:%02d' % (days, 's'[days == 1:], hours, minutes) - return '%d day%s' % (days, 's'[days == 1:]) + return "%d day%s %d:%02d" % (days, "s"[days == 1 :], hours, minutes) + return "%d day%s" % (days, "s"[days == 1 :]) else: - return '%d:%02d' % (hours, minutes) - elif display == 'sql': + return "%d:%02d" % (hours, minutes) + elif display == "sql": days += weeks * 7 - return '%d %02d:%02d:%02d' % (days, hours, minutes, seconds) - elif display == 'simple': + return "%d %02d:%02d:%02d" % (days, hours, minutes, seconds) + elif display == "simple": days += weeks * 7 if days: - return '%d day%s %02d:%02d:%02d' % (days, 's'[days == 1:], hours, minutes, seconds) + return "%d day%s %02d:%02d:%02d" % ( + days, + "s"[days == 1 :], + hours, + minutes, + seconds, + ) else: - return '%02d:%02d:%02d' % (hours, minutes, seconds) - elif display == 'localized': + return "%02d:%02d:%02d" % (hours, minutes, seconds) + elif display == "localized": days += weeks * 7 if days: - return npgettext('time format with day', '%d day %h:%m:%s', '%d days %h:%m:%s', days) \ - .replace('%d', str(days)).replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes) \ - .replace('%s', '%02d' % seconds) + return ( + npgettext( + "time format with day", "%d day %h:%m:%s", "%d days %h:%m:%s", days + ) + .replace("%d", str(days)) + .replace("%h", "%02d" % hours) + .replace("%m", "%02d" % minutes) + .replace("%s", "%02d" % seconds) + ) else: - return pgettext('time format without day', '%h:%m:%s') \ - .replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes).replace('%s', '%02d' % seconds) - elif display == 'localized-no-seconds': + return ( + pgettext("time format without day", "%h:%m:%s") + .replace("%h", "%02d" % hours) + .replace("%m", "%02d" % minutes) + .replace("%s", "%02d" % seconds) + ) + elif display == "localized-no-seconds": days += weeks * 7 if days: if hours or minutes: - return npgettext('time format no seconds with day', '%d day %h:%m', '%d days %h:%m', days) \ - .replace('%d', str(days)).replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes) - return ungettext('%d day', '%d days', days) % days + return ( + npgettext( + "time format no seconds with day", + "%d day %h:%m", + "%d days %h:%m", + days, + ) + .replace("%d", str(days)) + .replace("%h", "%02d" % hours) + .replace("%m", "%02d" % minutes) + ) + return ungettext("%d day", "%d days", days) % days else: - return pgettext('hours and minutes', '%h:%m').replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes) - elif display == 'concise': + return ( + pgettext("hours and minutes", "%h:%m") + .replace("%h", "%02d" % hours) + .replace("%m", "%02d" % minutes) + ) + elif display == "concise": days += weeks * 7 if days: - return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds) + return "%dd %02d:%02d:%02d" % (days, hours, minutes, seconds) else: - return '%02d:%02d:%02d' % (hours, minutes, seconds) - elif display == 'noday': + return "%02d:%02d:%02d" % (hours, minutes, seconds) + elif display == "noday": days += weeks * 7 hours += days * 24 - return '%02d:%02d:%02d' % (hours, minutes, seconds) - elif display == 'minimal': - words = ['w', 'd', 'h', 'm', 's'] - elif display == 'short': - words = [' wks', ' days', ' hrs', ' min', ' sec'] + return "%02d:%02d:%02d" % (hours, minutes, seconds) + elif display == "minimal": + words = ["w", "d", "h", "m", "s"] + elif display == "short": + words = [" wks", " days", " hrs", " min", " sec"] else: - words = [' weeks', ' days', ' hours', ' minutes', ' seconds'] + words = [" weeks", " days", " hours", " minutes", " seconds"] values = [weeks, days, hours, minutes, seconds] for i in range(len(values)): if values[i]: if values[i] == 1 and len(words[i]) > 1: - result.append('%i%s' % (values[i], words[i].rstrip('s'))) + result.append("%i%s" % (values[i], words[i].rstrip("s"))) else: - result.append('%i%s' % (values[i], words[i])) + result.append("%i%s" % (values[i], words[i])) return sep.join(result) diff --git a/judge/utils/unicode.py b/judge/utils/unicode.py index 78cc176..b55a4b5 100644 --- a/judge/utils/unicode.py +++ b/judge/utils/unicode.py @@ -6,12 +6,12 @@ def utf8bytes(maybe_text): return if isinstance(maybe_text, six.binary_type): return maybe_text - return maybe_text.encode('utf-8') + return maybe_text.encode("utf-8") -def utf8text(maybe_bytes, errors='strict'): +def utf8text(maybe_bytes, errors="strict"): if maybe_bytes is None: return if isinstance(maybe_bytes, six.text_type): return maybe_bytes - return maybe_bytes.decode('utf-8', errors) + return maybe_bytes.decode("utf-8", errors) diff --git a/judge/utils/views.py b/judge/utils/views.py index 622f0e6..530caba 100644 --- a/judge/utils/views.py +++ b/judge/utils/views.py @@ -6,6 +6,7 @@ from django.views.generic.detail import SingleObjectMixin from judge.utils.diggpaginator import DiggPaginator from django.utils.html import mark_safe + def class_view_decorator(function_decorator): """Convert a function based decorator into a class based decorator usable on class based Views. @@ -22,34 +23,43 @@ def class_view_decorator(function_decorator): def generic_message(request, title, message, status=None): - return render(request, 'generic-message.html', { - 'message': message, - 'title': title, - }, status=status) + return render( + request, + "generic-message.html", + { + "message": message, + "title": title, + }, + status=status, + ) def paginate_query_context(request): query = request.GET.copy() - query.setlist('page', []) + query.setlist("page", []) query = query.urlencode() if query: - return {'page_prefix': '%s?%s&page=' % (request.path, query), - 'first_page_href': '%s?%s' % (request.path, query)} + return { + "page_prefix": "%s?%s&page=" % (request.path, query), + "first_page_href": "%s?%s" % (request.path, query), + } else: - return {'page_prefix': '%s?page=' % request.path, - 'first_page_href': request.path} + return { + "page_prefix": "%s?page=" % request.path, + "first_page_href": request.path, + } class TitleMixin(object): - title = '(untitled)' + title = "(untitled)" content_title = None def get_context_data(self, **kwargs): context = super(TitleMixin, self).get_context_data(**kwargs) - context['title'] = self.get_title() + context["title"] = self.get_title() content_title = self.get_content_title() if content_title is not None: - context['content_title'] = content_title + context["content_title"] = content_title return context def get_content_title(self): @@ -60,10 +70,18 @@ class TitleMixin(object): class DiggPaginatorMixin(object): - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - return DiggPaginator(queryset, per_page, body=6, padding=2, - orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs) + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + return DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) class QueryStringSortMixin(object): @@ -75,8 +93,11 @@ class QueryStringSortMixin(object): return self.default_sort def get(self, request, *args, **kwargs): - order = request.GET.get('order', '') - if not ((not order.startswith('-') or order.count('-') == 1) and (order.lstrip('-') in self.all_sorts)): + order = request.GET.get("order", "") + if not ( + (not order.startswith("-") or order.count("-") == 1) + and (order.lstrip("-") in self.all_sorts) + ): order = self.get_default_sort_order(request) self.order = order @@ -84,17 +105,26 @@ class QueryStringSortMixin(object): def get_sort_context(self): query = self.request.GET.copy() - query.setlist('order', []) + query.setlist("order", []) query = query.urlencode() - sort_prefix = '%s?%s&order=' % (self.request.path, query) if query else '%s?order=' % self.request.path - current = self.order.lstrip('-') + sort_prefix = ( + "%s?%s&order=" % (self.request.path, query) + if query + else "%s?order=" % self.request.path + ) + current = self.order.lstrip("-") - links = {key: sort_prefix + ('-' if key in self.default_desc else '') + key for key in self.all_sorts} - links[current] = sort_prefix + ('' if self.order.startswith('-') else '-') + current + links = { + key: sort_prefix + ("-" if key in self.default_desc else "") + key + for key in self.all_sorts + } + links[current] = ( + sort_prefix + ("" if self.order.startswith("-") else "-") + current + ) - order = {key: '' for key in self.all_sorts} - order[current] = ' \u25BE' if self.order.startswith('-') else u' \u25B4' - return {'sort_links': links, 'sort_order': order} + order = {key: "" for key in self.all_sorts} + order[current] = " \u25BE" if self.order.startswith("-") else " \u25B4" + return {"sort_links": links, "sort_order": order} def get_sort_paginate_context(self): return paginate_query_context(self.request) diff --git a/judge/views/__init__.py b/judge/views/__init__.py index 37d173d..94a01c5 100644 --- a/judge/views/__init__.py +++ b/judge/views/__init__.py @@ -5,6 +5,6 @@ class TitledTemplateView(TemplateView): title = None def get_context_data(self, **kwargs): - if 'title' not in kwargs and self.title is not None: - kwargs['title'] = self.title + if "title" not in kwargs and self.title is not None: + kwargs["title"] = self.title return super(TitledTemplateView, self).get_context_data(**kwargs) diff --git a/judge/views/about.py b/judge/views/about.py index d328066..e503724 100644 --- a/judge/views/about.py +++ b/judge/views/about.py @@ -3,12 +3,20 @@ from django.utils.translation import gettext as _ def about(request): - return render(request, 'about/about.html', { - 'title': _('About'), - }) + return render( + request, + "about/about.html", + { + "title": _("About"), + }, + ) def custom_checker_sample(request): - return render(request, 'about/custom-checker-sample.html', { - 'title': _('Custom Checker Sample'), - }) + return render( + request, + "about/custom-checker-sample.html", + { + "title": _("Custom Checker Sample"), + }, + ) diff --git a/judge/views/api/api_v1.py b/judge/views/api/api_v1.py index bad7486..80f18f0 100644 --- a/judge/views/api/api_v1.py +++ b/judge/views/api/api_v1.py @@ -5,27 +5,40 @@ from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404 from dmoj import settings -from judge.models import Contest, ContestParticipation, ContestTag, Problem, Profile, Submission +from judge.models import ( + Contest, + ContestParticipation, + ContestTag, + Problem, + Profile, + Submission, +) def sane_time_repr(delta): days = delta.days hours = delta.seconds / 3600 minutes = (delta.seconds % 3600) / 60 - return '%02d:%02d:%02d' % (days, hours, minutes) + return "%02d:%02d:%02d" % (days, hours, minutes) def api_v1_contest_list(request): queryset = Contest.get_visible_contests(request.user).prefetch_related( - Prefetch('tags', queryset=ContestTag.objects.only('name'), to_attr='tag_list')) - - return JsonResponse({c.key: { - 'name': c.name, - 'start_time': c.start_time.isoformat(), - 'end_time': c.end_time.isoformat(), - 'time_limit': c.time_limit and sane_time_repr(c.time_limit), - 'labels': list(map(attrgetter('name'), c.tag_list)), - } for c in queryset}) + Prefetch("tags", queryset=ContestTag.objects.only("name"), to_attr="tag_list") + ) + + return JsonResponse( + { + c.key: { + "name": c.name, + "start_time": c.start_time.isoformat(), + "end_time": c.end_time.isoformat(), + "time_limit": c.time_limit and sane_time_repr(c.time_limit), + "labels": list(map(attrgetter("name"), c.tag_list)), + } + for c in queryset + } + ) def api_v1_contest_detail(request, contest): @@ -33,61 +46,87 @@ def api_v1_contest_detail(request, contest): in_contest = contest.is_in_contest(request.user) can_see_rankings = contest.can_see_full_scoreboard(request.user) - problems = list(contest.contest_problems.select_related('problem') - .defer('problem__description').order_by('order')) - participations = (contest.users.filter(virtual=0) - .prefetch_related('user__organizations') - .annotate(username=F('user__user__username')) - .order_by('-score', 'cumtime') if can_see_rankings else []) + problems = list( + contest.contest_problems.select_related("problem") + .defer("problem__description") + .order_by("order") + ) + participations = ( + contest.users.filter(virtual=0) + .prefetch_related("user__organizations") + .annotate(username=F("user__user__username")) + .order_by("-score", "cumtime") + if can_see_rankings + else [] + ) - can_see_problems = (in_contest or contest.ended or contest.is_editable_by(request.user)) + can_see_problems = ( + in_contest or contest.ended or contest.is_editable_by(request.user) + ) - return JsonResponse({ - 'time_limit': contest.time_limit and contest.time_limit.total_seconds(), - 'start_time': contest.start_time.isoformat(), - 'end_time': contest.end_time.isoformat(), - 'tags': list(contest.tags.values_list('name', flat=True)), - 'is_rated': contest.is_rated, - 'rate_all': contest.is_rated and contest.rate_all, - 'has_rating': contest.ratings.exists(), - 'rating_floor': contest.rating_floor, - 'rating_ceiling': contest.rating_ceiling, - 'format': { - 'name': contest.format_name, - 'config': contest.format_config, - }, - 'problems': [ - { - 'points': int(problem.points), - 'partial': problem.partial, - 'name': problem.problem.name, - 'code': problem.problem.code, - } for problem in problems] if can_see_problems else [], - 'rankings': [ - { - 'user': participation.username, - 'points': participation.score, - 'cumtime': participation.cumtime, - 'is_disqualified': participation.is_disqualified, - 'solutions': contest.format.get_problem_breakdown(participation, problems), - } for participation in participations], - }) + return JsonResponse( + { + "time_limit": contest.time_limit and contest.time_limit.total_seconds(), + "start_time": contest.start_time.isoformat(), + "end_time": contest.end_time.isoformat(), + "tags": list(contest.tags.values_list("name", flat=True)), + "is_rated": contest.is_rated, + "rate_all": contest.is_rated and contest.rate_all, + "has_rating": contest.ratings.exists(), + "rating_floor": contest.rating_floor, + "rating_ceiling": contest.rating_ceiling, + "format": { + "name": contest.format_name, + "config": contest.format_config, + }, + "problems": [ + { + "points": int(problem.points), + "partial": problem.partial, + "name": problem.problem.name, + "code": problem.problem.code, + } + for problem in problems + ] + if can_see_problems + else [], + "rankings": [ + { + "user": participation.username, + "points": participation.score, + "cumtime": participation.cumtime, + "is_disqualified": participation.is_disqualified, + "solutions": contest.format.get_problem_breakdown( + participation, problems + ), + } + for participation in participations + ], + } + ) def api_v1_problem_list(request): queryset = Problem.objects.filter(is_public=True, is_organization_private=False) - if settings.ENABLE_FTS and 'search' in request.GET: - query = ' '.join(request.GET.getlist('search')).strip() + if settings.ENABLE_FTS and "search" in request.GET: + query = " ".join(request.GET.getlist("search")).strip() if query: queryset = queryset.search(query) - queryset = queryset.values_list('code', 'points', 'partial', 'name', 'group__full_name') + queryset = queryset.values_list( + "code", "points", "partial", "name", "group__full_name" + ) - return JsonResponse({code: { - 'points': points, - 'partial': partial, - 'name': name, - 'group': group, - } for code, points, partial, name, group in queryset}) + return JsonResponse( + { + code: { + "points": points, + "partial": partial, + "name": name, + "group": group, + } + for code, points, partial, name, group in queryset + } + ) def api_v1_problem_info(request, problem): @@ -95,60 +134,83 @@ def api_v1_problem_info(request, problem): if not p.is_accessible_by(request.user): raise Http404() - return JsonResponse({ - 'name': p.name, - 'authors': list(p.authors.values_list('user__username', flat=True)), - 'types': list(p.types.values_list('full_name', flat=True)), - 'group': p.group.full_name, - 'time_limit': p.time_limit, - 'memory_limit': p.memory_limit, - 'points': p.points, - 'partial': p.partial, - 'languages': list(p.allowed_languages.values_list('key', flat=True)), - }) + return JsonResponse( + { + "name": p.name, + "authors": list(p.authors.values_list("user__username", flat=True)), + "types": list(p.types.values_list("full_name", flat=True)), + "group": p.group.full_name, + "time_limit": p.time_limit, + "memory_limit": p.memory_limit, + "points": p.points, + "partial": p.partial, + "languages": list(p.allowed_languages.values_list("key", flat=True)), + } + ) def api_v1_user_list(request): - queryset = Profile.objects.filter(is_unlisted=False).values_list('user__username', 'points', 'performance_points', - 'display_rank') - return JsonResponse({username: { - 'points': points, - 'performance_points': performance_points, - 'rank': rank, - } for username, points, performance_points, rank in queryset}) + queryset = Profile.objects.filter(is_unlisted=False).values_list( + "user__username", "points", "performance_points", "display_rank" + ) + return JsonResponse( + { + username: { + "points": points, + "performance_points": performance_points, + "rank": rank, + } + for username, points, performance_points, rank in queryset + } + ) def api_v1_user_info(request, user): profile = get_object_or_404(Profile, user__username=user) - submissions = list(Submission.objects.filter(case_points=F('case_total'), user=profile, problem__is_public=True, - problem__is_organization_private=False) - .values('problem').distinct().values_list('problem__code', flat=True)) + submissions = list( + Submission.objects.filter( + case_points=F("case_total"), + user=profile, + problem__is_public=True, + problem__is_organization_private=False, + ) + .values("problem") + .distinct() + .values_list("problem__code", flat=True) + ) resp = { - 'points': profile.points, - 'performance_points': profile.performance_points, - 'rank': profile.display_rank, - 'solved_problems': submissions, - 'organizations': list(profile.organizations.values_list('id', flat=True)), + "points": profile.points, + "performance_points": profile.performance_points, + "rank": profile.display_rank, + "solved_problems": submissions, + "organizations": list(profile.organizations.values_list("id", flat=True)), } last_rating = profile.ratings.last() contest_history = {} - participations = ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True, - contest__is_private=False, - contest__is_organization_private=False) + participations = ContestParticipation.objects.filter( + user=profile, + virtual=0, + contest__is_visible=True, + contest__is_private=False, + contest__is_organization_private=False, + ) for contest_key, rating, mean, performance in participations.values_list( - 'contest__key', 'rating__rating', 'rating__mean', 'rating__performance', + "contest__key", + "rating__rating", + "rating__mean", + "rating__performance", ): contest_history[contest_key] = { - 'rating': rating, - 'raw_rating': mean, - 'performance': performance, + "rating": rating, + "raw_rating": mean, + "performance": performance, } - resp['contests'] = { - 'current_rating': last_rating.rating if last_rating else None, - 'history': contest_history, + resp["contests"] = { + "current_rating": last_rating.rating if last_rating else None, + "history": contest_history, } return JsonResponse(resp) @@ -156,14 +218,30 @@ def api_v1_user_info(request, user): def api_v1_user_submissions(request, user): profile = get_object_or_404(Profile, user__username=user) - subs = Submission.objects.filter(user=profile, problem__is_public=True, problem__is_organization_private=False) + subs = Submission.objects.filter( + user=profile, problem__is_public=True, problem__is_organization_private=False + ) - return JsonResponse({sub['id']: { - 'problem': sub['problem__code'], - 'time': sub['time'], - 'memory': sub['memory'], - 'points': sub['points'], - 'language': sub['language__key'], - 'status': sub['status'], - 'result': sub['result'], - } for sub in subs.values('id', 'problem__code', 'time', 'memory', 'points', 'language__key', 'status', 'result')}) + return JsonResponse( + { + sub["id"]: { + "problem": sub["problem__code"], + "time": sub["time"], + "memory": sub["memory"], + "points": sub["points"], + "language": sub["language__key"], + "status": sub["status"], + "result": sub["result"], + } + for sub in subs.values( + "id", + "problem__code", + "time", + "memory", + "points", + "language__key", + "status", + "result", + ) + } + ) diff --git a/judge/views/api/api_v2.py b/judge/views/api/api_v2.py index 8e512b3..a750d98 100644 --- a/judge/views/api/api_v2.py +++ b/judge/views/api/api_v2.py @@ -9,7 +9,7 @@ from judge.views.contests import contest_ranking_list def error(message): - return JsonResponse({'error': message}, status=422) + return JsonResponse({"error": message}, status=422) def api_v2_user_info(request): @@ -44,9 +44,9 @@ def api_v2_user_info(request): // ... ] } - """ + """ try: - username = request.GET['username'] + username = request.GET["username"] except KeyError: return error("no username passed") if not username: @@ -56,66 +56,90 @@ def api_v2_user_info(request): except Profile.DoesNotExist: return error("no such user") - last_rating = list(profile.ratings.order_by('-contest__end_time')) + last_rating = list(profile.ratings.order_by("-contest__end_time")) resp = { "rank": profile.display_rank, - "organizations": list(profile.organizations.values_list('key', flat=True)), + "organizations": list(profile.organizations.values_list("key", flat=True)), } contest_history = [] - for participation in (ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True) - .order_by('-contest__end_time')): + for participation in ContestParticipation.objects.filter( + user=profile, virtual=0, contest__is_visible=True + ).order_by("-contest__end_time"): contest = participation.contest - problems = list(contest.contest_problems.select_related('problem').defer('problem__description') - .order_by('order')) - rank, result = next(filter(lambda data: data[1].user == profile.user, - ranker(contest_ranking_list(contest, problems), - key=attrgetter('points', 'cumtime')))) + problems = list( + contest.contest_problems.select_related("problem") + .defer("problem__description") + .order_by("order") + ) + rank, result = next( + filter( + lambda data: data[1].user == profile.user, + ranker( + contest_ranking_list(contest, problems), + key=attrgetter("points", "cumtime"), + ), + ) + ) - contest_history.append({ - 'contest': { - 'code': contest.key, - 'name': contest.name, - 'tags': list(contest.tags.values_list('name', flat=True)), - 'time_limit': contest.time_limit and contest.time_limit.total_seconds(), - 'start_time': contest.start_time.isoformat(), - 'end_time': contest.end_time.isoformat(), - }, - 'rank': rank, - 'rating': result.participation_rating, - }) + contest_history.append( + { + "contest": { + "code": contest.key, + "name": contest.name, + "tags": list(contest.tags.values_list("name", flat=True)), + "time_limit": contest.time_limit + and contest.time_limit.total_seconds(), + "start_time": contest.start_time.isoformat(), + "end_time": contest.end_time.isoformat(), + }, + "rank": rank, + "rating": result.participation_rating, + } + ) - resp['contests'] = { + resp["contests"] = { "current_rating": last_rating[0].rating if last_rating else None, - 'history': contest_history, + "history": contest_history, } solved_problems = [] attempted_problems = [] - problem_data = (Submission.objects.filter(points__gt=0, user=profile, problem__is_public=True, - problem__is_organization_private=False) - .annotate(max_pts=Max('points')) - .values_list('max_pts', 'problem__points', 'problem__code') - .distinct()) + problem_data = ( + Submission.objects.filter( + points__gt=0, + user=profile, + problem__is_public=True, + problem__is_organization_private=False, + ) + .annotate(max_pts=Max("points")) + .values_list("max_pts", "problem__points", "problem__code") + .distinct() + ) for awarded_pts, max_pts, problem in problem_data: if awarded_pts == max_pts: solved_problems.append(problem) else: - attempted_problems.append({ - 'awarded': awarded_pts, - 'max': max_pts, - 'problem': problem, - }) + attempted_problems.append( + { + "awarded": awarded_pts, + "max": max_pts, + "problem": problem, + } + ) - resp['problems'] = { - 'points': profile.points, - 'solved': solved_problems, - 'attempted': attempted_problems, - 'authored': list(Problem.objects.filter(is_public=True, is_organization_private=False, authors=profile) - .values_list('code', flat=True)), + resp["problems"] = { + "points": profile.points, + "solved": solved_problems, + "attempted": attempted_problems, + "authored": list( + Problem.objects.filter( + is_public=True, is_organization_private=False, authors=profile + ).values_list("code", flat=True) + ), } return JsonResponse(resp) diff --git a/judge/views/blog.py b/judge/views/blog.py index 828f54f..9ce0fdc 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -8,8 +8,17 @@ from django.utils.translation import ugettext as _ from django.views.generic import ListView from judge.comments import CommentedDetailView -from judge.models import BlogPost, Comment, Contest, Language, Problem, ProblemClarification, Profile, Submission, \ - Ticket +from judge.models import ( + BlogPost, + Comment, + Contest, + Language, + Problem, + ProblemClarification, + Profile, + Submission, + Ticket, +) from judge.utils.cachedict import CacheDict from judge.utils.diggpaginator import DiggPaginator from judge.utils.problems import user_completed_ids @@ -19,32 +28,44 @@ from judge.utils.views import TitleMixin # General view for all content list on home feed class FeedView(ListView): - template_name = 'blog/list.html' + template_name = "blog/list.html" title = None - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - return DiggPaginator(queryset, per_page, body=6, padding=2, - orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs) + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + return DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) def get_context_data(self, **kwargs): context = super(FeedView, self).get_context_data(**kwargs) - context['has_clarifications'] = False + context["has_clarifications"] = False if self.request.user.is_authenticated: participation = self.request.profile.current_contest if participation: - clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all()) - context['has_clarifications'] = clarifications.count() > 0 - context['clarifications'] = clarifications.order_by('-date') + clarifications = ProblemClarification.objects.filter( + problem__in=participation.contest.problems.all() + ) + context["has_clarifications"] = clarifications.count() > 0 + context["clarifications"] = clarifications.order_by("-date") if participation.contest.is_editable_by(self.request.user): - context['can_edit_contest'] = True + context["can_edit_contest"] = True - context['page_titles'] = CacheDict(lambda page: Comment.get_page_title(page)) + context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page)) - context['user_count'] = lazy(Profile.objects.count, int, int) - context['problem_count'] = lazy(Problem.objects.filter(is_public=True).count, int, int) - context['submission_count'] = lazy(Submission.objects.count, int, int) - context['language_count'] = lazy(Language.objects.count, int, int) + context["user_count"] = lazy(Profile.objects.count, int, int) + context["problem_count"] = lazy( + Problem.objects.filter(is_public=True).count, int, int + ) + context["submission_count"] = lazy(Submission.objects.count, int, int) + context["language_count"] = lazy(Language.objects.count, int, int) now = timezone.now() @@ -57,31 +78,44 @@ class FeedView(ListView): # .annotate(points=Max('points'), latest=Max('date')) # .order_by('-latest') # [:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT]) - - visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) \ - .order_by('start_time') - context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now) - context['future_contests'] = visible_contests.filter(start_time__gt=now) + visible_contests = ( + Contest.get_visible_contests(self.request.user) + .filter(is_visible=True) + .order_by("start_time") + ) - visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) + context["current_contests"] = visible_contests.filter( + start_time__lte=now, end_time__gt=now + ) + context["future_contests"] = visible_contests.filter(start_time__gt=now) - context['top_rated'] = Profile.objects.filter(is_unlisted=False).order_by('-rating')[:10] - context['top_scorer'] = Profile.objects.filter(is_unlisted=False).order_by('-performance_points')[:10] + visible_contests = Contest.get_visible_contests(self.request.user).filter( + is_visible=True + ) + + context["top_rated"] = Profile.objects.filter(is_unlisted=False).order_by( + "-rating" + )[:10] + context["top_scorer"] = Profile.objects.filter(is_unlisted=False).order_by( + "-performance_points" + )[:10] return context - + class PostList(FeedView): model = BlogPost paginate_by = 10 - context_object_name = 'posts' + context_object_name = "posts" def get_queryset(self): - queryset = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) \ - .order_by('-sticky', '-publish_on') \ - .prefetch_related('authors__user', 'organizations') - if not self.request.user.has_perm('judge.edit_all_post'): + queryset = ( + BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) + .order_by("-sticky", "-publish_on") + .prefetch_related("authors__user", "organizations") + ) + if not self.request.user.has_perm("judge.edit_all_post"): filter = Q(is_organization_private=False) if self.request.user.is_authenticated: filter |= Q(organizations__in=self.request.profile.organizations.all()) @@ -90,15 +124,20 @@ class PostList(FeedView): def get_context_data(self, **kwargs): context = super(PostList, self).get_context_data(**kwargs) - context['title'] = self.title or _('Page %d of Posts') % context['page_obj'].number - context['first_page_href'] = reverse('home') - context['page_prefix'] = reverse('blog_post_list') - context['page_type'] = 'blog' - context['post_comment_counts'] = { - int(page[2:]): count for page, count in - Comment.objects - .filter(page__in=['b:%d' % post.id for post in context['posts']], hidden=False) - .values_list('page').annotate(count=Count('page')).order_by() + context["title"] = ( + self.title or _("Page %d of Posts") % context["page_obj"].number + ) + context["first_page_href"] = reverse("home") + context["page_prefix"] = reverse("blog_post_list") + context["page_type"] = "blog" + context["post_comment_counts"] = { + int(page[2:]): count + for page, count in Comment.objects.filter( + page__in=["b:%d" % post.id for post in context["posts"]], hidden=False + ) + .values_list("page") + .annotate(count=Count("page")) + .order_by() } return context @@ -106,39 +145,49 @@ class PostList(FeedView): class TicketFeed(FeedView): model = Ticket - context_object_name = 'tickets' + context_object_name = "tickets" paginate_by = 30 def get_queryset(self, is_own=True): profile = self.request.profile if is_own: if self.request.user.is_authenticated: - return (Ticket.objects.filter(Q(user=profile) | Q(assignees__in=[profile]), is_open=True).order_by('-id') - .prefetch_related('linked_item').select_related('user__user')) + return ( + Ticket.objects.filter( + Q(user=profile) | Q(assignees__in=[profile]), is_open=True + ) + .order_by("-id") + .prefetch_related("linked_item") + .select_related("user__user") + ) else: return [] else: # Superusers better be staffs, not the spell-casting kind either. if self.request.user.is_staff: - tickets = (Ticket.objects.order_by('-id').filter(is_open=True).prefetch_related('linked_item') - .select_related('user__user')) + tickets = ( + Ticket.objects.order_by("-id") + .filter(is_open=True) + .prefetch_related("linked_item") + .select_related("user__user") + ) return filter_visible_tickets(tickets, self.request.user, profile) else: return [] def get_context_data(self, **kwargs): context = super(TicketFeed, self).get_context_data(**kwargs) - context['page_type'] = 'ticket' - context['first_page_href'] = self.request.path - context['page_prefix'] = '?page=' - context['title'] = _('Ticket feed') + context["page_type"] = "ticket" + context["first_page_href"] = self.request.path + context["page_prefix"] = "?page=" + context["title"] = _("Ticket feed") return context class CommentFeed(FeedView): model = Comment - context_object_name = 'comments' + context_object_name = "comments" paginate_by = 50 def get_queryset(self): @@ -146,29 +195,29 @@ class CommentFeed(FeedView): def get_context_data(self, **kwargs): context = super(CommentFeed, self).get_context_data(**kwargs) - context['page_type'] = 'comment' - context['first_page_href'] = self.request.path - context['page_prefix'] = '?page=' - context['title'] = _('Comment feed') + context["page_type"] = "comment" + context["first_page_href"] = self.request.path + context["page_prefix"] = "?page=" + context["title"] = _("Comment feed") return context class PostView(TitleMixin, CommentedDetailView): model = BlogPost - pk_url_kwarg = 'id' - context_object_name = 'post' - template_name = 'blog/blog.html' + pk_url_kwarg = "id" + context_object_name = "post" + template_name = "blog/blog.html" def get_title(self): return self.object.title def get_comment_page(self): - return 'b:%s' % self.object.id + return "b:%s" % self.object.id def get_context_data(self, **kwargs): context = super(PostView, self).get_context_data(**kwargs) - context['og_image'] = self.object.og_image + context["og_image"] = self.object.og_image return context def get_object(self, queryset=None): diff --git a/judge/views/comment.py b/judge/views/comment.py index 7550d97..edba7f3 100644 --- a/judge/views/comment.py +++ b/judge/views/comment.py @@ -4,7 +4,12 @@ from django.core.exceptions import PermissionDenied from django.db import IntegrityError, transaction from django.db.models import F from django.forms.models import ModelForm -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseForbidden, +) from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST @@ -18,27 +23,41 @@ from judge.utils.views import TitleMixin from judge.widgets import MathJaxPagedownWidget from judge.comments import add_mention_notifications, del_mention_notifications -__all__ = ['upvote_comment', 'downvote_comment', 'CommentEditAjax', 'CommentContent', - 'CommentEdit'] +__all__ = [ + "upvote_comment", + "downvote_comment", + "CommentEditAjax", + "CommentContent", + "CommentEdit", +] @login_required def vote_comment(request, delta): if abs(delta) != 1: - return HttpResponseBadRequest(_('Messing around, are we?'), content_type='text/plain') + return HttpResponseBadRequest( + _("Messing around, are we?"), content_type="text/plain" + ) - if request.method != 'POST': + if request.method != "POST": return HttpResponseForbidden() - if 'id' not in request.POST: + if "id" not in request.POST: return HttpResponseBadRequest() - if not request.user.is_staff and not request.profile.submission_set.filter(points=F('problem__points')).exists(): - return HttpResponseBadRequest(_('You must solve at least one problem before you can vote.'), - content_type='text/plain') + if ( + not request.user.is_staff + and not request.profile.submission_set.filter( + points=F("problem__points") + ).exists() + ): + return HttpResponseBadRequest( + _("You must solve at least one problem before you can vote."), + content_type="text/plain", + ) try: - comment_id = int(request.POST['id']) + comment_id = int(request.POST["id"]) except ValueError: return HttpResponseBadRequest() else: @@ -56,18 +75,22 @@ def vote_comment(request, delta): except IntegrityError: with LockModel(write=(CommentVote,)): try: - vote = CommentVote.objects.get(comment_id=comment_id, voter=request.profile) + vote = CommentVote.objects.get( + comment_id=comment_id, voter=request.profile + ) except CommentVote.DoesNotExist: # We must continue racing in case this is exploited to manipulate votes. continue if -vote.score != delta: - return HttpResponseBadRequest(_('You already voted.'), content_type='text/plain') + return HttpResponseBadRequest( + _("You already voted."), content_type="text/plain" + ) vote.delete() - Comment.objects.filter(id=comment_id).update(score=F('score') - vote.score) + Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score) else: - Comment.objects.filter(id=comment_id).update(score=F('score') + delta) + Comment.objects.filter(id=comment_id).update(score=F("score") + delta) break - return HttpResponse('success', content_type='text/plain') + return HttpResponse("success", content_type="text/plain") def upvote_comment(request): @@ -80,26 +103,28 @@ def downvote_comment(request): class CommentMixin(object): model = Comment - pk_url_kwarg = 'id' - context_object_name = 'comment' + pk_url_kwarg = "id" + context_object_name = "comment" class CommentRevisionAjax(CommentMixin, DetailView): - template_name = 'comments/revision-ajax.html' + template_name = "comments/revision-ajax.html" def get_context_data(self, **kwargs): context = super(CommentRevisionAjax, self).get_context_data(**kwargs) - revisions = Version.objects.get_for_object(self.object).order_by('-revision') + revisions = Version.objects.get_for_object(self.object).order_by("-revision") try: - wanted = min(max(int(self.request.GET.get('revision', 0)), 0), len(revisions) - 1) + wanted = min( + max(int(self.request.GET.get("revision", 0)), 0), len(revisions) - 1 + ) except ValueError: raise Http404 - context['revision'] = revisions[wanted] + context["revision"] = revisions[wanted] return context def get_object(self, queryset=None): comment = super(CommentRevisionAjax, self).get_object(queryset) - if comment.hidden and not self.request.user.has_perm('judge.change_comment'): + if comment.hidden and not self.request.user.has_perm("judge.change_comment"): raise Http404() return comment @@ -107,13 +132,15 @@ class CommentRevisionAjax(CommentMixin, DetailView): class CommentEditForm(ModelForm): class Meta: model = Comment - fields = ['body'] + fields = ["body"] if MathJaxPagedownWidget is not None: - widgets = {'body': MathJaxPagedownWidget(attrs={'id': 'id-edit-comment-body'})} + widgets = { + "body": MathJaxPagedownWidget(attrs={"id": "id-edit-comment-body"}) + } class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): - template_name = 'comments/edit-ajax.html' + template_name = "comments/edit-ajax.html" form_class = CommentEditForm def form_valid(self, form): @@ -123,7 +150,7 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): add_mention_notifications(comment) with transaction.atomic(), revisions.create_revision(): - revisions.set_comment(_('Edited from site')) + revisions.set_comment(_("Edited from site")) revisions.set_user(self.request.user) return super(CommentEditAjax, self).form_valid(form) @@ -132,7 +159,7 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): def get_object(self, queryset=None): comment = super(CommentEditAjax, self).get_object(queryset) - if self.request.user.has_perm('judge.change_comment'): + if self.request.user.has_perm("judge.change_comment"): return comment profile = self.request.profile if profile != comment.author or profile.mute or comment.hidden: @@ -141,36 +168,37 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): class CommentEdit(TitleMixin, CommentEditAjax): - template_name = 'comments/edit.html' + template_name = "comments/edit.html" def get_title(self): - return _('Editing comment') + return _("Editing comment") class CommentContent(CommentMixin, DetailView): - template_name = 'comments/content.html' + template_name = "comments/content.html" class CommentVotesAjax(PermissionRequiredMixin, CommentMixin, DetailView): - template_name = 'comments/votes.html' - permission_required = 'judge.change_commentvote' + template_name = "comments/votes.html" + permission_required = "judge.change_commentvote" def get_context_data(self, **kwargs): context = super(CommentVotesAjax, self).get_context_data(**kwargs) - context['votes'] = (self.object.votes.select_related('voter__user') - .only('id', 'voter__display_rank', 'voter__user__username', 'score')) + context["votes"] = self.object.votes.select_related("voter__user").only( + "id", "voter__display_rank", "voter__user__username", "score" + ) return context @require_POST def comment_hide(request): - if not request.user.has_perm('judge.change_comment'): + if not request.user.has_perm("judge.change_comment"): raise PermissionDenied() try: - comment_id = int(request.POST['id']) + comment_id = int(request.POST["id"]) except ValueError: return HttpResponseBadRequest() comment = get_object_or_404(Comment, id=comment_id) comment.get_descendants(include_self=True).update(hidden=True) - return HttpResponse('ok') + return HttpResponse("ok") diff --git a/judge/views/contests.py b/judge/views/contests.py index 8dcd43b..f16fdae 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -13,9 +13,28 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.db import IntegrityError -from django.db.models import Case, Count, F, FloatField, IntegerField, Max, Min, Q, Sum, Value, When +from django.db.models import ( + Case, + Count, + F, + FloatField, + IntegerField, + Max, + Min, + Q, + Sum, + Value, + When, +) from django.db.models.expressions import CombinedExpression -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse, HttpResponseNotAllowed +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseRedirect, + JsonResponse, + HttpResponseNotAllowed, +) from django.shortcuts import get_object_or_404, render from django.template.defaultfilters import date as date_filter from django.urls import reverse, reverse_lazy @@ -26,27 +45,62 @@ from django.utils.safestring import mark_safe from django.utils.timezone import make_aware from django.utils.translation import gettext as _, gettext_lazy from django.views.generic import ListView, TemplateView -from django.views.generic.detail import BaseDetailView, DetailView, SingleObjectMixin, View +from django.views.generic.detail import ( + BaseDetailView, + DetailView, + SingleObjectMixin, + View, +) from judge import event_poster as event from judge.comments import CommentedDetailView from judge.forms import ContestCloneForm -from judge.models import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestTag, \ - Organization, Problem, Profile, Submission, ProblemClarification +from judge.models import ( + Contest, + ContestMoss, + ContestParticipation, + ContestProblem, + ContestTag, + Organization, + Problem, + Profile, + Submission, + ProblemClarification, +) from judge.tasks import run_moss from judge.utils.celery import redirect_to_task_status from judge.utils.opengraph import generate_opengraph from judge.utils.problems import _get_result_data from judge.utils.ranker import ranker from judge.utils.stats import get_bar_chart, get_pie_chart, get_histogram -from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, SingleObjectFormView, TitleMixin, \ - generic_message +from judge.utils.views import ( + DiggPaginatorMixin, + QueryStringSortMixin, + SingleObjectFormView, + TitleMixin, + generic_message, +) from judge.widgets import HeavyPreviewPageDownWidget -__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar', - 'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax', - 'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list', - 'base_contest_ranking_list', 'ContestClarificationView', 'update_contest_mode'] +__all__ = [ + "ContestList", + "ContestDetail", + "ContestRanking", + "ContestJoin", + "ContestLeave", + "ContestCalendar", + "ContestClone", + "ContestStats", + "ContestMossView", + "ContestMossDelete", + "contest_ranking_ajax", + "ContestParticipationList", + "ContestParticipationDisqualify", + "get_contest_ranking_list", + "base_contest_ranking_list", + "ContestClarificationView", + "update_contest_mode", +] def _find_contest(request, key, private_check=True): @@ -55,8 +109,15 @@ def _find_contest(request, key, private_check=True): if private_check and not contest.is_accessible_by(request.user): raise ObjectDoesNotExist() except ObjectDoesNotExist: - return generic_message(request, _('No such contest'), - _('Could not find a contest with the key "%s".') % key, status=404), False + return ( + generic_message( + request, + _("No such contest"), + _('Could not find a contest with the key "%s".') % key, + status=404, + ), + False, + ) return contest, True @@ -65,16 +126,18 @@ class ContestListMixin(object): return Contest.get_visible_contests(self.request.user) -class ContestList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView): +class ContestList( + QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView +): model = Contest paginate_by = 20 - template_name = 'contest/list.html' - title = gettext_lazy('Contests') - context_object_name = 'past_contests' - all_sorts = frozenset(('name', 'user_count', 'start_time')) - default_desc = frozenset(('name', 'user_count')) - default_sort = '-start_time' - + template_name = "contest/list.html" + title = gettext_lazy("Contests") + context_object_name = "past_contests" + all_sorts = frozenset(("name", "user_count", "start_time")) + default_desc = frozenset(("name", "user_count")) + default_sort = "-start_time" + @cached_property def _now(self): return timezone.now() @@ -83,30 +146,40 @@ class ContestList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestL self.contest_query = None self.org_query = [] - if 'orgs' in self.request.GET: + if "orgs" in self.request.GET: try: - self.org_query = list(map(int, request.GET.getlist('orgs'))) + self.org_query = list(map(int, request.GET.getlist("orgs"))) except ValueError: pass return super(ContestList, self).get(request, *args, **kwargs) def _get_queryset(self): - queryset = super(ContestList, self).get_queryset() \ - .prefetch_related('tags', 'organizations', 'authors', 'curators', 'testers') - - if 'contest' in self.request.GET: - self.contest_query = query = ' '.join(self.request.GET.getlist('contest')).strip() + queryset = ( + super(ContestList, self) + .get_queryset() + .prefetch_related("tags", "organizations", "authors", "curators", "testers") + ) + + if "contest" in self.request.GET: + self.contest_query = query = " ".join( + self.request.GET.getlist("contest") + ).strip() if query: queryset = queryset.filter( - Q(key__icontains=query) | Q(name__icontains=query)) + Q(key__icontains=query) | Q(name__icontains=query) + ) if self.org_query: queryset = queryset.filter(organizations__in=self.org_query) return queryset def get_queryset(self): - return self._get_queryset().order_by(self.order, 'key').filter(end_time__lt=self._now) + return ( + self._get_queryset() + .order_by(self.order, "key") + .filter(end_time__lt=self._now) + ) def get_context_data(self, **kwargs): context = super(ContestList, self).get_context_data(**kwargs) @@ -118,26 +191,31 @@ class ContestList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestL present.append(contest) if self.request.user.is_authenticated: - for participation in ContestParticipation.objects.filter(virtual=0, user=self.request.profile, - contest_id__in=present) \ - .select_related('contest') \ - .prefetch_related('contest__authors', 'contest__curators', 'contest__testers')\ - .annotate(key=F('contest__key')): + for participation in ( + ContestParticipation.objects.filter( + virtual=0, user=self.request.profile, contest_id__in=present + ) + .select_related("contest") + .prefetch_related( + "contest__authors", "contest__curators", "contest__testers" + ) + .annotate(key=F("contest__key")) + ): if not participation.ended: 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')) - context['active_participations'] = active - context['current_contests'] = present - context['future_contests'] = future - context['now'] = self._now - context['first_page_href'] = '.' - context['contest_query'] = self.contest_query - context['org_query'] = self.org_query - context['organizations'] = Organization.objects.all() + 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 + context["now"] = self._now + context["first_page_href"] = "." + context["contest_query"] = self.contest_query + context["org_query"] = self.org_query + context["organizations"] = Organization.objects.all() context.update(self.get_sort_context()) context.update(self.get_sort_paginate_context()) return context @@ -152,10 +230,10 @@ class PrivateContestError(Exception): class ContestMixin(object): - context_object_name = 'contest' + context_object_name = "contest" model = Contest - slug_field = 'key' - slug_url_kwarg = 'contest' + slug_field = "key" + slug_url_kwarg = "contest" @cached_property def is_editor(self): @@ -168,7 +246,7 @@ class ContestMixin(object): if not self.request.user.is_authenticated: return False return self.request.profile.id in self.object.tester_ids - + @cached_property def can_edit(self): return self.object.is_editable_by(self.request.user) @@ -177,35 +255,43 @@ class ContestMixin(object): context = super(ContestMixin, self).get_context_data(**kwargs) if self.request.user.is_authenticated: try: - context['live_participation'] = ( - self.request.profile.contest_history.get( - contest=self.object, - virtual=ContestParticipation.LIVE, - ) + context[ + "live_participation" + ] = self.request.profile.contest_history.get( + contest=self.object, + virtual=ContestParticipation.LIVE, ) except ContestParticipation.DoesNotExist: - context['live_participation'] = None - context['has_joined'] = False + context["live_participation"] = None + context["has_joined"] = False else: - context['has_joined'] = True + context["has_joined"] = True else: - context['live_participation'] = None - context['has_joined'] = False - - context['now'] = timezone.now() - context['is_editor'] = self.is_editor - context['is_tester'] = self.is_tester - context['can_edit'] = self.can_edit + context["live_participation"] = None + context["has_joined"] = False + + context["now"] = timezone.now() + context["is_editor"] = self.is_editor + context["is_tester"] = self.is_tester + context["can_edit"] = self.can_edit if not self.object.og_image or not self.object.summary: - metadata = generate_opengraph('generated-meta-contest:%d' % self.object.id, - self.object.description, 'contest') - context['meta_description'] = self.object.summary or metadata[0] - context['og_image'] = self.object.og_image or metadata[1] - context['has_moss_api_key'] = settings.MOSS_API_KEY is not None - context['logo_override_image'] = self.object.logo_override_image - if not context['logo_override_image'] and self.object.organizations.count() == 1: - context['logo_override_image'] = self.object.organizations.first().logo_override_image + metadata = generate_opengraph( + "generated-meta-contest:%d" % self.object.id, + self.object.description, + "contest", + ) + context["meta_description"] = self.object.summary or metadata[0] + context["og_image"] = self.object.og_image or metadata[1] + context["has_moss_api_key"] = settings.MOSS_API_KEY is not None + context["logo_override_image"] = self.object.logo_override_image + if ( + not context["logo_override_image"] + and self.object.organizations.count() == 1 + ): + context[ + "logo_override_image" + ] = self.object.organizations.first().logo_override_image return context @@ -213,30 +299,48 @@ class ContestMixin(object): contest = super(ContestMixin, self).get_object(queryset) profile = self.request.profile - if (profile is not None and - ContestParticipation.objects.filter(id=profile.current_contest_id, contest_id=contest.id).exists()): + if ( + profile is not None + and ContestParticipation.objects.filter( + id=profile.current_contest_id, contest_id=contest.id + ).exists() + ): return contest try: contest.access_check(self.request.user) except Contest.PrivateContest: - raise PrivateContestError(contest.name, contest.is_private, contest.is_organization_private, - contest.organizations.all()) + raise PrivateContestError( + contest.name, + contest.is_private, + contest.is_organization_private, + contest.organizations.all(), + ) except Contest.Inaccessible: raise Http404() else: return contest - + if contest.is_private or contest.is_organization_private: - private_contest_error = PrivateContestError(contest.name, contest.is_private, - contest.is_organization_private, contest.organizations.all()) + private_contest_error = PrivateContestError( + contest.name, + contest.is_private, + contest.is_organization_private, + contest.organizations.all(), + ) if profile is None: raise private_contest_error - if user.has_perm('judge.edit_all_contest'): + if user.has_perm("judge.edit_all_contest"): return contest - if not (contest.is_organization_private and - contest.organizations.filter(id__in=profile.organizations.all()).exists()) and \ - not (contest.is_private and contest.private_contestants.filter(id=profile.id).exists()): + if not ( + contest.is_organization_private + and contest.organizations.filter( + id__in=profile.organizations.all() + ).exists() + ) and not ( + contest.is_private + and contest.private_contestants.filter(id=profile.id).exists() + ): raise private_contest_error return contest @@ -247,41 +351,63 @@ class ContestMixin(object): except Http404: key = kwargs.get(self.slug_url_kwarg, None) if key: - return generic_message(request, _('No such contest'), - _('Could not find a contest with the key "%s".') % key) + return generic_message( + request, + _("No such contest"), + _('Could not find a contest with the key "%s".') % key, + ) else: - return generic_message(request, _('No such contest'), - _('Could not find such contest.')) + return generic_message( + request, _("No such contest"), _("Could not find such contest.") + ) except PrivateContestError as e: - return render(request, 'contest/private.html', { - 'error': e, 'title': _('Access to contest "%s" denied') % e.name, - }, status=403) + return render( + request, + "contest/private.html", + { + "error": e, + "title": _('Access to contest "%s" denied') % e.name, + }, + status=403, + ) class ContestDetail(ContestMixin, TitleMixin, CommentedDetailView): - template_name = 'contest/contest.html' + template_name = "contest/contest.html" def get_comment_page(self): - return 'c:%s' % self.object.key + return "c:%s" % self.object.key def get_title(self): return self.object.name def get_context_data(self, **kwargs): context = super(ContestDetail, self).get_context_data(**kwargs) - context['contest_problems'] = Problem.objects.filter(contests__contest=self.object) \ - .order_by('contests__order').defer('description') \ - .annotate(has_public_editorial=Sum(Case(When(solution__is_public=True, then=1), - default=0, output_field=IntegerField()))) \ + context["contest_problems"] = ( + Problem.objects.filter(contests__contest=self.object) + .order_by("contests__order") + .defer("description") + .annotate( + has_public_editorial=Sum( + Case( + When(solution__is_public=True, then=1), + default=0, + output_field=IntegerField(), + ) + ) + ) .add_i18n_name(self.request.LANGUAGE_CODE) + ) return context -class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView): - title = _('Clone Contest') - template_name = 'contest/clone.html' +class ContestClone( + ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView +): + title = _("Clone Contest") + template_name = "contest/clone.html" form_class = ContestCloneForm - permission_required = 'judge.clone_contest' + permission_required = "judge.clone_contest" def form_valid(self, form): contest = self.object @@ -295,7 +421,7 @@ class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObje contest.pk = None contest.is_visible = False contest.user_count = 0 - contest.key = form.cleaned_data['key'] + contest.key = form.cleaned_data["key"] contest.save() contest.tags.set(tags) @@ -309,7 +435,9 @@ class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObje problem.pk = None ContestProblem.objects.bulk_create(contest_problems) - return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest.id,))) + return HttpResponseRedirect( + reverse("admin:judge_contest_change", args=(contest.id,)) + ) class ContestAccessDenied(Exception): @@ -321,7 +449,7 @@ class ContestAccessCodeForm(forms.Form): def __init__(self, *args, **kwargs): super(ContestAccessCodeForm, self).__init__(*args, **kwargs) - self.fields['access_code'].widget.attrs.update({'autocomplete': 'off'}) + self.fields["access_code"].widget.attrs.update({"autocomplete": "off"}) class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView): @@ -334,7 +462,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView): try: return self.join_contest(request) except ContestAccessDenied: - if request.POST.get('access_code'): + if request.POST.get("access_code"): return self.ask_for_access_code(ContestAccessCodeForm(request.POST)) else: return HttpResponseRedirect(request.path) @@ -343,30 +471,59 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView): contest = self.object if not contest.can_join and not (self.is_editor or self.is_tester): - return generic_message(request, _('Contest not ongoing'), - _('"%s" is not currently ongoing.') % contest.name) + return generic_message( + request, + _("Contest not ongoing"), + _('"%s" is not currently ongoing.') % contest.name, + ) profile = request.profile if profile.current_contest is not None: - return generic_message(request, _('Already in contest'), - _('You are already in a contest: "%s".') % profile.current_contest.contest.name) + return generic_message( + request, + _("Already in contest"), + _('You are already in a contest: "%s".') + % profile.current_contest.contest.name, + ) - if not request.user.is_superuser and contest.banned_users.filter(id=profile.id).exists(): - return generic_message(request, _('Banned from joining'), - _('You have been declared persona non grata for this contest. ' - 'You are permanently barred from joining this contest.')) + if ( + not request.user.is_superuser + and contest.banned_users.filter(id=profile.id).exists() + ): + return generic_message( + request, + _("Banned from joining"), + _( + "You have been declared persona non grata for this contest. " + "You are permanently barred from joining this contest." + ), + ) - requires_access_code = (not self.can_edit and contest.access_code and access_code != contest.access_code) + requires_access_code = ( + not self.can_edit + and contest.access_code + and access_code != contest.access_code + ) if contest.ended: if requires_access_code: raise ContestAccessDenied() while True: - virtual_id = max((ContestParticipation.objects.filter(contest=contest, user=profile) - .aggregate(virtual_id=Max('virtual'))['virtual_id'] or 0) + 1, 1) + virtual_id = max( + ( + ContestParticipation.objects.filter( + contest=contest, user=profile + ).aggregate(virtual_id=Max("virtual"))["virtual_id"] + or 0 + ) + + 1, + 1, + ) try: participation = ContestParticipation.objects.create( - contest=contest, user=profile, virtual=virtual_id, + contest=contest, + user=profile, + virtual=virtual_id, real_start=timezone.now(), ) # There is obviously a race condition here, so we keep trying until we win the race. @@ -379,43 +536,56 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView): LIVE = ContestParticipation.LIVE try: participation = ContestParticipation.objects.get( - contest=contest, user=profile, virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE), + contest=contest, + user=profile, + virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE), ) except ContestParticipation.DoesNotExist: if requires_access_code: raise ContestAccessDenied() participation = ContestParticipation.objects.create( - contest=contest, user=profile, virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE), + contest=contest, + user=profile, + virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE), real_start=timezone.now(), ) else: if participation.ended: participation = ContestParticipation.objects.get_or_create( - contest=contest, user=profile, virtual=SPECTATE, - defaults={'real_start': timezone.now()}, + contest=contest, + user=profile, + virtual=SPECTATE, + defaults={"real_start": timezone.now()}, )[0] profile.current_contest = participation profile.save() contest._updating_stats_only = True contest.update_user_count() - return HttpResponseRedirect(reverse('problem_list')) + return HttpResponseRedirect(reverse("problem_list")) def ask_for_access_code(self, form=None): contest = self.object wrong_code = False if form: if form.is_valid(): - if form.cleaned_data['access_code'] == contest.access_code: - return self.join_contest(self.request, form.cleaned_data['access_code']) + if form.cleaned_data["access_code"] == contest.access_code: + return self.join_contest( + self.request, form.cleaned_data["access_code"] + ) wrong_code = True else: form = ContestAccessCodeForm() - return render(self.request, 'contest/access_code.html', { - 'form': form, 'wrong_code': wrong_code, - 'title': _('Enter access code for "%s"') % contest.name, - }) + return render( + self.request, + "contest/access_code.html", + { + "form": form, + "wrong_code": wrong_code, + "title": _('Enter access code for "%s"') % contest.name, + }, + ) class ContestLeave(LoginRequiredMixin, ContestMixin, BaseDetailView): @@ -423,29 +593,38 @@ class ContestLeave(LoginRequiredMixin, ContestMixin, BaseDetailView): contest = self.get_object() profile = request.profile - if profile.current_contest is None or profile.current_contest.contest_id != contest.id: - return generic_message(request, _('No such contest'), - _('You are not in contest "%s".') % contest.key, 404) + if ( + profile.current_contest is None + or profile.current_contest.contest_id != contest.id + ): + return generic_message( + request, + _("No such contest"), + _('You are not in contest "%s".') % contest.key, + 404, + ) profile.remove_contest() - request.session['contest_mode'] = True # reset contest_mode - return HttpResponseRedirect(reverse('contest_view', args=(contest.key,))) + request.session["contest_mode"] = True # reset contest_mode + return HttpResponseRedirect(reverse("contest_view", args=(contest.key,))) -ContestDay = namedtuple('ContestDay', 'date weekday is_pad is_today starts ends oneday') +ContestDay = namedtuple("ContestDay", "date weekday is_pad is_today starts ends oneday") class ContestCalendar(TitleMixin, ContestListMixin, TemplateView): firstweekday = SUNDAY - weekday_classes = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] - template_name = 'contest/calendar.html' + weekday_classes = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] + template_name = "contest/calendar.html" def get(self, request, *args, **kwargs): try: - self.year = int(kwargs['year']) - self.month = int(kwargs['month']) + self.year = int(kwargs["year"]) + self.month = int(kwargs["month"]) except (KeyError, ValueError): - raise ImproperlyConfigured(_('ContestCalendar requires integer year and month')) + raise ImproperlyConfigured( + _("ContestCalendar requires integer year and month") + ) self.today = timezone.now().date() return self.render() @@ -455,12 +634,16 @@ class ContestCalendar(TitleMixin, ContestListMixin, TemplateView): def get_contest_data(self, start, end): end += timedelta(days=1) - contests = self.get_queryset().filter(Q(start_time__gte=start, start_time__lt=end) | - Q(end_time__gte=start, end_time__lt=end)) + contests = self.get_queryset().filter( + Q(start_time__gte=start, start_time__lt=end) + | Q(end_time__gte=start, end_time__lt=end) + ) starts, ends, oneday = (defaultdict(list) for i in range(3)) for contest in contests: start_date = timezone.localtime(contest.start_time).date() - end_date = timezone.localtime(contest.end_time - timedelta(seconds=1)).date() + end_date = timezone.localtime( + contest.end_time - timedelta(seconds=1) + ).date() if start_date == end_date: oneday[start_date].append(contest) else: @@ -470,12 +653,25 @@ class ContestCalendar(TitleMixin, ContestListMixin, TemplateView): def get_table(self): calendar = Calendar(self.firstweekday).monthdatescalendar(self.year, self.month) - starts, ends, oneday = self.get_contest_data(make_aware(datetime.combine(calendar[0][0], time.min)), - make_aware(datetime.combine(calendar[-1][-1], time.min))) - return [[ContestDay( - date=date, weekday=self.weekday_classes[weekday], is_pad=date.month != self.month, - is_today=date == self.today, starts=starts[date], ends=ends[date], oneday=oneday[date], - ) for weekday, date in enumerate(week)] for week in calendar] + starts, ends, oneday = self.get_contest_data( + make_aware(datetime.combine(calendar[0][0], time.min)), + make_aware(datetime.combine(calendar[-1][-1], time.min)), + ) + return [ + [ + ContestDay( + date=date, + weekday=self.weekday_classes[weekday], + is_pad=date.month != self.month, + is_today=date == self.today, + starts=starts[date], + ends=ends[date], + oneday=oneday[date], + ) + for weekday, date in enumerate(week) + ] + for week in calendar + ] def get_context_data(self, **kwargs): context = super(ContestCalendar, self).get_context_data(**kwargs) @@ -485,40 +681,53 @@ class ContestCalendar(TitleMixin, ContestListMixin, TemplateView): except ValueError: raise Http404() else: - context['title'] = _('Contests in %(month)s') % {'month': date_filter(month, _("F Y"))} + context["title"] = _("Contests in %(month)s") % { + "month": date_filter(month, _("F Y")) + } - dates = Contest.objects.aggregate(min=Min('start_time'), max=Max('end_time')) + dates = Contest.objects.aggregate(min=Min("start_time"), max=Max("end_time")) min_month = (self.today.year, self.today.month) - if dates['min'] is not None: - min_month = dates['min'].year, dates['min'].month + if dates["min"] is not None: + min_month = dates["min"].year, dates["min"].month max_month = (self.today.year, self.today.month) - if dates['max'] is not None: - max_month = max((dates['max'].year, dates['max'].month), (self.today.year, self.today.month)) + if dates["max"] is not None: + max_month = max( + (dates["max"].year, dates["max"].month), + (self.today.year, self.today.month), + ) month = (self.year, self.month) if month < min_month or month > max_month: # 404 is valid because it merely declares the lack of existence, without any reason raise Http404() - context['now'] = timezone.now() - context['calendar'] = self.get_table() - context['curr_month'] = date(self.year, self.month, 1) + context["now"] = timezone.now() + context["calendar"] = self.get_table() + context["curr_month"] = date(self.year, self.month, 1) if month > min_month: - context['prev_month'] = date(self.year - (self.month == 1), 12 if self.month == 1 else self.month - 1, 1) + context["prev_month"] = date( + self.year - (self.month == 1), + 12 if self.month == 1 else self.month - 1, + 1, + ) else: - context['prev_month'] = None + context["prev_month"] = None if month < max_month: - context['next_month'] = date(self.year + (self.month == 12), 1 if self.month == 12 else self.month + 1, 1) + context["next_month"] = date( + self.year + (self.month == 12), + 1 if self.month == 12 else self.month + 1, + 1, + ) else: - context['next_month'] = None + context["next_month"] = None return context class CachedContestCalendar(ContestCalendar): def render(self): - key = 'contest_cal:%d:%d' % (self.year, self.month) + key = "contest_cal:%d:%d" % (self.year, self.month) cached = cache.get(key) if cached is not None: return HttpResponse(cached) @@ -529,11 +738,11 @@ class CachedContestCalendar(ContestCalendar): class ContestStats(TitleMixin, ContestMixin, DetailView): - template_name = 'contest/stats.html' - POINT_BIN = 10 # in point distribution + template_name = "contest/stats.html" + POINT_BIN = 10 # in point distribution def get_title(self): - return _('%s Statistics') % self.object.name + return _("%s Statistics") % self.object.name def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -543,15 +752,22 @@ class ContestStats(TitleMixin, ContestMixin, DetailView): queryset = Submission.objects.filter(contest_object=self.object) - ac_count = Count(Case(When(result='AC', then=Value(1)), output_field=IntegerField())) - ac_rate = CombinedExpression(ac_count / Count('problem'), '*', Value(100.0), output_field=FloatField()) + ac_count = Count( + Case(When(result="AC", then=Value(1)), output_field=IntegerField()) + ) + ac_rate = CombinedExpression( + ac_count / Count("problem"), "*", Value(100.0), output_field=FloatField() + ) status_count_queryset = list( - queryset.values('problem__code', 'result').annotate(count=Count('result')) - .values_list('problem__code', 'result', 'count'), + queryset.values("problem__code", "result") + .annotate(count=Count("result")) + .values_list("problem__code", "result", "count"), ) labels, codes = [], [] - contest_problems = self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code') + contest_problems = self.object.contest_problems.order_by("order").values_list( + "problem__name", "problem__code" + ) if contest_problems: labels, codes = zip(*contest_problems) num_problems = len(labels) @@ -562,65 +778,85 @@ class ContestStats(TitleMixin, ContestMixin, DetailView): result_data = defaultdict(partial(list, [0] * num_problems)) for i in range(num_problems): - for category in _get_result_data(defaultdict(int, status_counts[i]))['categories']: - result_data[category['code']][i] = category['count'] + for category in _get_result_data(defaultdict(int, status_counts[i]))[ + "categories" + ]: + result_data[category["code"]][i] = category["count"] problem_points = [[] for _ in range(num_problems)] - point_count_queryset = list(queryset.values('problem__code', 'contest__points', 'contest__problem__points') - .annotate(count=Count('contest__points')) - .order_by('problem__code', 'contest__points') - .values_list('problem__code', 'contest__points', 'contest__problem__points', 'count')) + point_count_queryset = list( + queryset.values( + "problem__code", "contest__points", "contest__problem__points" + ) + .annotate(count=Count("contest__points")) + .order_by("problem__code", "contest__points") + .values_list( + "problem__code", "contest__points", "contest__problem__points", "count" + ) + ) counter = [[0 for _ in range(self.POINT_BIN + 1)] for _ in range(num_problems)] for problem_code, point, max_point, count in point_count_queryset: - if (point == None) or (problem_code not in codes): continue + if (point == None) or (problem_code not in codes): + continue problem_idx = codes.index(problem_code) - bin_idx = math.floor(point * self.POINT_BIN / max_point) + bin_idx = math.floor(point * self.POINT_BIN / max_point) counter[problem_idx][bin_idx] += count for i in range(num_problems): - problem_points[i] = [(j * 100 / self.POINT_BIN, counter[i][j]) - for j in range(len(counter[i]))] - + problem_points[i] = [ + (j * 100 / self.POINT_BIN, counter[i][j]) + for j in range(len(counter[i])) + ] + stats = { - 'problem_status_count': { - 'labels': labels, - 'datasets': [ + "problem_status_count": { + "labels": labels, + "datasets": [ { - 'label': name, - 'backgroundColor': settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS[name], - 'data': data, + "label": name, + "backgroundColor": settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS[ + name + ], + "data": data, } for name, data in result_data.items() ], }, - 'problem_ac_rate': get_bar_chart( - queryset.values('contest__problem__order', 'problem__name').annotate(ac_rate=ac_rate) - .order_by('contest__problem__order').values_list('problem__name', 'ac_rate'), + "problem_ac_rate": get_bar_chart( + queryset.values("contest__problem__order", "problem__name") + .annotate(ac_rate=ac_rate) + .order_by("contest__problem__order") + .values_list("problem__name", "ac_rate"), ), - 'problem_point': [get_histogram(problem_points[i]) - for i in range(num_problems) + "problem_point": [ + get_histogram(problem_points[i]) for i in range(num_problems) ], - 'language_count': get_pie_chart( - queryset.values('language__name').annotate(count=Count('language__name')) - .filter(count__gt=0).order_by('-count').values_list('language__name', 'count'), + "language_count": get_pie_chart( + queryset.values("language__name") + .annotate(count=Count("language__name")) + .filter(count__gt=0) + .order_by("-count") + .values_list("language__name", "count"), ), - 'language_ac_rate': get_bar_chart( - queryset.values('language__name').annotate(ac_rate=ac_rate) - .filter(ac_rate__gt=0).values_list('language__name', 'ac_rate'), + "language_ac_rate": get_bar_chart( + queryset.values("language__name") + .annotate(ac_rate=ac_rate) + .filter(ac_rate__gt=0) + .values_list("language__name", "ac_rate"), ), } - context['stats'] = mark_safe(json.dumps(stats)) - context['problems'] = labels + context["stats"] = mark_safe(json.dumps(stats)) + context["problems"] = labels return context ContestRankingProfile = namedtuple( - 'ContestRankingProfile', - 'id user css_class username points cumtime tiebreaker organization participation ' - 'participation_rating problem_cells result_cell', + "ContestRankingProfile", + "id user css_class username points cumtime tiebreaker organization participation " + "participation_rating problem_cells result_cell", ) -BestSolutionData = namedtuple('BestSolutionData', 'code points time state is_pretested') +BestSolutionData = namedtuple("BestSolutionData", "code points time state is_pretested") def make_contest_ranking_profile(contest, participation, contest_problems): @@ -634,33 +870,57 @@ def make_contest_ranking_profile(contest, participation, contest_problems): cumtime=participation.cumtime, tiebreaker=participation.tiebreaker, organization=user.organization, - participation_rating=participation.rating.rating if hasattr(participation, 'rating') else None, - problem_cells=[contest.format.display_user_problem(participation, contest_problem) - for contest_problem in contest_problems], + participation_rating=participation.rating.rating + if hasattr(participation, "rating") + else None, + problem_cells=[ + contest.format.display_user_problem(participation, contest_problem) + for contest_problem in contest_problems + ], result_cell=contest.format.display_participation_result(participation), participation=participation, ) def base_contest_ranking_list(contest, problems, queryset): - return [make_contest_ranking_profile(contest, participation, problems) for participation in - queryset.select_related('user__user', 'rating').defer('user__about', 'user__organizations__about')] + return [ + make_contest_ranking_profile(contest, participation, problems) + for participation in queryset.select_related("user__user", "rating").defer( + "user__about", "user__organizations__about" + ) + ] def contest_ranking_list(contest, problems, queryset=None): if not queryset: queryset = contest.users.filter(virtual=0) - return base_contest_ranking_list(contest, problems, queryset - .prefetch_related('user__organizations') - .extra(select={'round_score': 'round(score, 6)'}) - .order_by('is_disqualified', '-round_score', 'cumtime', 'tiebreaker')) + return base_contest_ranking_list( + contest, + problems, + queryset.prefetch_related("user__organizations") + .extra(select={"round_score": "round(score, 6)"}) + .order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"), + ) -def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list, - show_current_virtual=False, ranker=ranker): - problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order')) +def get_contest_ranking_list( + request, + contest, + participation=None, + ranking_list=contest_ranking_list, + show_current_virtual=False, + ranker=ranker, +): + problems = list( + contest.contest_problems.select_related("problem") + .defer("problem__description") + .order_by("order") + ) - users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime', 'tiebreaker')) + users = ranker( + ranking_list(contest, problems), + key=attrgetter("points", "cumtime", "tiebreaker"), + ) if show_current_virtual: if participation is None and request.user.is_authenticated: @@ -668,38 +928,49 @@ def get_contest_ranking_list(request, contest, participation=None, ranking_list= if participation is None or participation.contest_id != contest.id: participation = None if participation is not None and participation.virtual: - users = chain([('-', make_contest_ranking_profile(contest, participation, problems))], users) + users = chain( + [("-", make_contest_ranking_profile(contest, participation, problems))], + users, + ) return users, problems def contest_ranking_ajax(request, contest, participation=None): contest, exists = _find_contest(request, contest) if not exists: - return HttpResponseBadRequest('Invalid contest', content_type='text/plain') + return HttpResponseBadRequest("Invalid contest", content_type="text/plain") if not contest.can_see_full_scoreboard(request.user): raise Http404() queryset = contest.users.filter(virtual__gte=0) - if request.GET.get('friend') == 'true' and request.profile: + if request.GET.get("friend") == "true" and request.profile: friends = list(request.profile.get_friends()) queryset = queryset.filter(user__user__username__in=friends) - if request.GET.get('virtual') != 'true': + if request.GET.get("virtual") != "true": queryset = queryset.filter(virtual=0) - users, problems = get_contest_ranking_list(request, contest, participation, - ranking_list=partial(contest_ranking_list, queryset=queryset)) - return render(request, 'contest/ranking-table.html', { - 'users': users, - 'problems': problems, - 'contest': contest, - 'has_rating': contest.ratings.exists(), - 'can_edit': contest.is_editable_by(request.user) - }) + users, problems = get_contest_ranking_list( + request, + contest, + participation, + ranking_list=partial(contest_ranking_list, queryset=queryset), + ) + return render( + request, + "contest/ranking-table.html", + { + "users": users, + "problems": problems, + "contest": contest, + "has_rating": contest.ratings.exists(), + "can_edit": contest.is_editable_by(request.user), + }, + ) class ContestRankingBase(ContestMixin, TitleMixin, DetailView): - template_name = 'contest/ranking.html' + template_name = "contest/ranking.html" tab = None def get_title(self): @@ -718,67 +989,84 @@ class ContestRankingBase(ContestMixin, TitleMixin, DetailView): raise Http404() users, problems = self.get_ranking_list() - context['users'] = users - context['problems'] = problems - context['tab'] = self.tab + context["users"] = users + context["problems"] = problems + context["tab"] = self.tab return context class ContestRanking(ContestRankingBase): - tab = 'ranking' + tab = "ranking" def get_title(self): - return _('%s Rankings') % self.object.name + return _("%s Rankings") % self.object.name def get_ranking_list(self): if not self.object.can_see_full_scoreboard(self.request.user): - queryset = self.object.users.filter(user=self.request.profile, virtual=ContestParticipation.LIVE) + queryset = self.object.users.filter( + user=self.request.profile, virtual=ContestParticipation.LIVE + ) return get_contest_ranking_list( - self.request, self.object, + self.request, + self.object, ranking_list=partial(base_contest_ranking_list, queryset=queryset), - ranker=lambda users, key: ((_('???'), user) for user in users), + ranker=lambda users, key: ((_("???"), user) for user in users), ) return get_contest_ranking_list(self.request, self.object) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['has_rating'] = self.object.ratings.exists() + context["has_rating"] = self.object.ratings.exists() return context class ContestParticipationList(LoginRequiredMixin, ContestRankingBase): - tab = 'participation' + tab = "participation" def get_title(self): if self.profile == self.request.profile: - return _('Your participation in %s') % self.object.name + return _("Your participation in %s") % self.object.name return _("%s's participation in %s") % (self.profile.username, self.object.name) def get_ranking_list(self): - if not self.object.can_see_full_scoreboard(self.request.user) and self.profile != self.request.profile: + if ( + not self.object.can_see_full_scoreboard(self.request.user) + and self.profile != self.request.profile + ): raise Http404() - - queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by('-virtual') - live_link = format_html('{0}', _('Live'), self.profile.username, - reverse('contest_ranking', args=[self.object.key])) + + queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by( + "-virtual" + ) + live_link = format_html( + '{0}', + _("Live"), + self.profile.username, + reverse("contest_ranking", args=[self.object.key]), + ) return get_contest_ranking_list( - self.request, self.object, show_current_virtual=False, + self.request, + self.object, + show_current_virtual=False, ranking_list=partial(base_contest_ranking_list, queryset=queryset), - ranker=lambda users, key: ((user.participation.virtual or live_link, user) for user in users)) + ranker=lambda users, key: ( + (user.participation.virtual or live_link, user) for user in users + ), + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['has_rating'] = False - context['now'] = timezone.now() - context['rank_header'] = _('Participation') - context['participation_tab'] = True + context["has_rating"] = False + context["now"] = timezone.now() + context["rank_header"] = _("Participation") + context["participation_tab"] = True return context def get(self, request, *args, **kwargs): - if 'user' in kwargs: - self.profile = get_object_or_404(Profile, user__username=kwargs['user']) + if "user" in kwargs: + self.profile = get_object_or_404(Profile, user__username=kwargs["user"]) else: self.profile = self.request.profile return super().get(request, *args, **kwargs) @@ -795,20 +1083,22 @@ class ContestParticipationDisqualify(ContestMixin, SingleObjectMixin, View): self.object = self.get_object() try: - participation = self.object.users.get(pk=request.POST.get('participation')) + participation = self.object.users.get(pk=request.POST.get("participation")) except ObjectDoesNotExist: pass else: participation.set_disqualified(not participation.is_disqualified) - return HttpResponseRedirect(reverse('contest_ranking', args=(self.object.key,))) + return HttpResponseRedirect(reverse("contest_ranking", args=(self.object.key,))) class ContestMossMixin(ContestMixin, PermissionRequiredMixin): - permission_required = 'judge.moss_contest' + permission_required = "judge.moss_contest" def get_object(self, queryset=None): contest = super().get_object(queryset) - if settings.MOSS_API_KEY is None or not contest.is_editable_by(self.request.user): + if settings.MOSS_API_KEY is None or not contest.is_editable_by( + self.request.user + ): raise Http404() if not contest.is_editable_by(self.request.user): raise Http404() @@ -816,16 +1106,22 @@ class ContestMossMixin(ContestMixin, PermissionRequiredMixin): class ContestMossView(ContestMossMixin, TitleMixin, DetailView): - template_name = 'contest/moss.html' + template_name = "contest/moss.html" def get_title(self): - return _('%s MOSS Results') % self.object.name + return _("%s MOSS Results") % self.object.name def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - problems = list(map(attrgetter('problem'), self.object.contest_problems.order_by('order') - .select_related('problem'))) + problems = list( + map( + attrgetter("problem"), + self.object.contest_problems.order_by("order").select_related( + "problem" + ), + ) + ) languages = list(map(itemgetter(0), ContestMoss.LANG_MAPPING)) results = ContestMoss.objects.filter(contest=self.object) @@ -836,9 +1132,11 @@ class ContestMossView(ContestMossMixin, TitleMixin, DetailView): for result_list in moss_results.values(): result_list.sort(key=lambda x: languages.index(x.language)) - context['languages'] = languages - context['has_results'] = results.exists() - context['moss_results'] = [(problem, moss_results[problem]) for problem in problems] + context["languages"] = languages + context["has_results"] = results.exists() + context["moss_results"] = [ + (problem, moss_results[problem]) for problem in problems + ] return context @@ -846,8 +1144,9 @@ class ContestMossView(ContestMossMixin, TitleMixin, DetailView): self.object = self.get_object() status = run_moss.delay(self.object.key) return redirect_to_task_status( - status, message=_('Running MOSS for %s...') % (self.object.name,), - redirect=reverse('contest_moss', args=(self.object.key,)), + status, + message=_("Running MOSS for %s...") % (self.object.name,), + redirect=reverse("contest_moss", args=(self.object.key,)), ) @@ -855,40 +1154,45 @@ class ContestMossDelete(ContestMossMixin, SingleObjectMixin, View): def post(self, request, *args, **kwargs): self.object = self.get_object() ContestMoss.objects.filter(contest=self.object).delete() - return HttpResponseRedirect(reverse('contest_moss', args=(self.object.key,))) + return HttpResponseRedirect(reverse("contest_moss", args=(self.object.key,))) class ContestTagDetailAjax(DetailView): model = ContestTag - slug_field = slug_url_kwarg = 'name' - context_object_name = 'tag' - template_name = 'contest/tag-ajax.html' + slug_field = slug_url_kwarg = "name" + context_object_name = "tag" + template_name = "contest/tag-ajax.html" class ContestTagDetail(TitleMixin, ContestTagDetailAjax): - template_name = 'contest/tag.html' + template_name = "contest/tag.html" def get_title(self): - return _('Contest tag: %s') % self.object.name + return _("Contest tag: %s") % self.object.name class ProblemClarificationForm(forms.Form): - body = forms.CharField(widget=HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'), - preview_timeout=1000, hide_preview_button=True)) + body = forms.CharField( + widget=HeavyPreviewPageDownWidget( + preview=reverse_lazy("comment_preview"), + preview_timeout=1000, + hide_preview_button=True, + ) + ) def __init__(self, request, *args, **kwargs): self.request = request super(ProblemClarificationForm, self).__init__(*args, **kwargs) - self.fields['body'].widget.attrs.update({'placeholder': _('Issue description')}) + self.fields["body"].widget.attrs.update({"placeholder": _("Issue description")}) class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView): form_class = ProblemClarificationForm - template_name = 'contest/clarification.html' + template_name = "contest/clarification.html" def get_form_kwargs(self): kwargs = super(NewContestClarificationView, self).get_form_kwargs() - kwargs['request'] = self.request + kwargs["request"] = self.request return kwargs def is_accessible(self): @@ -898,9 +1202,11 @@ class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView return False if not self.request.participation.contest == self.get_object(): return False - return self.request.user.is_superuser or \ - self.request.profile in self.request.participation.contest.authors.all() or \ - self.request.profile in self.request.participation.contest.curators.all() + return ( + self.request.user.is_superuser + or self.request.profile in self.request.participation.contest.authors.all() + or self.request.profile in self.request.participation.contest.curators.all() + ) def get(self, request, *args, **kwargs): if not self.is_accessible(): @@ -908,28 +1214,34 @@ class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView return super().get(self, request, *args, **kwargs) def form_valid(self, form): - problem_code = self.request.POST['problem'] - description = form.cleaned_data['body'] + problem_code = self.request.POST["problem"] + description = form.cleaned_data["body"] clarification = ProblemClarification(description=description) clarification.problem = Problem.objects.get(code=problem_code) clarification.save() - - link = reverse('home') + + link = reverse("home") return HttpResponseRedirect(link) def get_title(self): return "New clarification for %s" % self.object.name def get_content_title(self): - return mark_safe(escape(_('New clarification for %s')) % - format_html('{1}', reverse('problem_detail', args=[self.object.key]), - self.object.name)) + return mark_safe( + escape(_("New clarification for %s")) + % format_html( + '{1}', + reverse("problem_detail", args=[self.object.key]), + self.object.name, + ) + ) def get_context_data(self, **kwargs): context = super(NewContestClarificationView, self).get_context_data(**kwargs) - context['problems'] = ContestProblem.objects.filter(contest=self.object)\ - .order_by('order') + context["problems"] = ContestProblem.objects.filter( + contest=self.object + ).order_by("order") return context @@ -939,27 +1251,37 @@ class ContestClarificationAjax(ContestMixin, DetailView): if not self.object.is_accessible_by(request.user): raise Http404() - polling_time = 1 # minute - last_one_minute = last_five_minutes = timezone.now()-timezone.timedelta(minutes=polling_time) - - queryset = list(ProblemClarification.objects.filter( - problem__in=self.object.problems.all(), - date__gte=last_one_minute - ).values('problem', 'problem__name', 'description')) - - problems = list(ContestProblem.objects.filter(contest=self.object)\ - .order_by('order').values('problem')) - problems = [i['problem'] for i in problems] - for cla in queryset: - cla['order'] = self.object.get_label_for_problem(problems.index(cla['problem'])) + polling_time = 1 # minute + last_one_minute = last_five_minutes = timezone.now() - timezone.timedelta( + minutes=polling_time + ) - return JsonResponse(queryset, safe=False, json_dumps_params={'ensure_ascii': False}) + queryset = list( + ProblemClarification.objects.filter( + problem__in=self.object.problems.all(), date__gte=last_one_minute + ).values("problem", "problem__name", "description") + ) + + problems = list( + ContestProblem.objects.filter(contest=self.object) + .order_by("order") + .values("problem") + ) + problems = [i["problem"] for i in problems] + for cla in queryset: + cla["order"] = self.object.get_label_for_problem( + problems.index(cla["problem"]) + ) + + return JsonResponse( + queryset, safe=False, json_dumps_params={"ensure_ascii": False} + ) def update_contest_mode(request): - if not request.is_ajax() or not request.method=='POST': - return HttpResponseNotAllowed(['POST']) + if not request.is_ajax() or not request.method == "POST": + return HttpResponseNotAllowed(["POST"]) - old_mode = request.session.get('contest_mode', True) - request.session['contest_mode'] = not old_mode - return HttpResponse() \ No newline at end of file + old_mode = request.session.get("contest_mode", True) + request.session["contest_mode"] = not old_mode + return HttpResponse() diff --git a/judge/views/error.py b/judge/views/error.py index 113422d..cffed7e 100644 --- a/judge/views/error.py +++ b/judge/views/error.py @@ -5,29 +5,42 @@ from django.utils.translation import gettext as _ def error(request, context, status): - return render(request, 'error.html', context=context, status=status) + return render(request, "error.html", context=context, status=status) def error404(request, exception=None): # TODO: "panic: go back" - return render(request, 'generic-message.html', { - 'title': _('404 error'), - 'message': _('Could not find page "%s"') % request.path, - }, status=404) + return render( + request, + "generic-message.html", + { + "title": _("404 error"), + "message": _('Could not find page "%s"') % request.path, + }, + status=404, + ) def error403(request, exception=None): - return error(request, { - 'id': 'unauthorized_access', - 'description': _('no permission for %s') % request.path, - 'code': 403, - }, 403) + return error( + request, + { + "id": "unauthorized_access", + "description": _("no permission for %s") % request.path, + "code": 403, + }, + 403, + ) def error500(request): - return error(request, { - 'id': 'invalid_state', - 'description': _('corrupt page %s') % request.path, - 'traceback': traceback.format_exc(), - 'code': 500, - }, 500) + return error( + request, + { + "id": "invalid_state", + "description": _("corrupt page %s") % request.path, + "traceback": traceback.format_exc(), + "code": 500, + }, + 500, + ) diff --git a/judge/views/internal.py b/judge/views/internal.py index dd19e88..07fa75b 100644 --- a/judge/views/internal.py +++ b/judge/views/internal.py @@ -6,30 +6,42 @@ from django.http import HttpResponseForbidden from judge.utils.diggpaginator import DiggPaginator from judge.models import VolunteerProblemVote, Problem + class InternalProblem(ListView): model = Problem - title = _('Internal problems') - template_name = 'internal/base.html' + title = _("Internal problems") + template_name = "internal/base.html" paginate_by = 100 - context_object_name = 'problems' + context_object_name = "problems" + + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + return DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - return DiggPaginator(queryset, per_page, body=6, padding=2, orphans=orphans, - allow_empty_first_page=allow_empty_first_page, **kwargs) - def get_queryset(self): - queryset = Problem.objects.annotate(vote_count=Count('volunteer_user_votes')) \ - .filter(vote_count__gte=1).order_by('-vote_count') + queryset = ( + Problem.objects.annotate(vote_count=Count("volunteer_user_votes")) + .filter(vote_count__gte=1) + .order_by("-vote_count") + ) return queryset def get_context_data(self, **kwargs): context = super(InternalProblem, self).get_context_data(**kwargs) - context['page_type'] = 'problem' - context['title'] = self.title + context["page_type"] = "problem" + context["title"] = self.title return context def get(self, request, *args, **kwargs): if request.user.is_superuser: return super(InternalProblem, self).get(request, *args, **kwargs) - return HttpResponseForbidden() \ No newline at end of file + return HttpResponseForbidden() diff --git a/judge/views/language.py b/judge/views/language.py index 87ab8cb..bbdc4bd 100644 --- a/judge/views/language.py +++ b/judge/views/language.py @@ -7,12 +7,12 @@ from judge.utils.views import TitleMixin class LanguageList(TitleMixin, ListView): model = Language - context_object_name = 'languages' - template_name = 'status/language-list.html' - title = gettext_lazy('Runtimes') + context_object_name = "languages" + template_name = "status/language-list.html" + title = gettext_lazy("Runtimes") def get_queryset(self): - queryset = super().get_queryset().prefetch_related('runtimeversion_set') + queryset = super().get_queryset().prefetch_related("runtimeversion_set") if not self.request.user.is_superuser and not self.request.user.is_staff: queryset = queryset.filter(judges__online=True).distinct() return queryset diff --git a/judge/views/license.py b/judge/views/license.py index 77208e6..0e208b7 100644 --- a/judge/views/license.py +++ b/judge/views/license.py @@ -6,9 +6,9 @@ from judge.utils.views import TitleMixin class LicenseDetail(TitleMixin, DetailView): model = License - slug_field = slug_url_kwarg = 'key' - context_object_name = 'license' - template_name = 'license.html' + slug_field = slug_url_kwarg = "key" + context_object_name = "license" + template_name = "license.html" def get_title(self): return self.object.name diff --git a/judge/views/mailgun.py b/judge/views/mailgun.py index 3bd8321..f28589a 100644 --- a/judge/views/mailgun.py +++ b/judge/views/mailgun.py @@ -15,49 +15,77 @@ from registration.models import RegistrationProfile from judge.utils.unicode import utf8bytes -logger = logging.getLogger('judge.mail.activate') +logger = logging.getLogger("judge.mail.activate") class MailgunActivationView(View): - if hasattr(settings, 'MAILGUN_ACCESS_KEY'): + if hasattr(settings, "MAILGUN_ACCESS_KEY"): + def post(self, request, *args, **kwargs): params = request.POST - timestamp = params.get('timestamp', '') - token = params.get('token', '') - signature = params.get('signature', '') + timestamp = params.get("timestamp", "") + token = params.get("token", "") + signature = params.get("signature", "") - logger.debug('Received request: %s', params) + logger.debug("Received request: %s", params) - if signature != hmac.new(key=utf8bytes(settings.MAILGUN_ACCESS_KEY), - msg=utf8bytes('%s%s' % (timestamp, token)), digestmod=hashlib.sha256).hexdigest(): - logger.info('Rejected request: signature: %s, timestamp: %s, token: %s', signature, timestamp, token) + if ( + signature + != hmac.new( + key=utf8bytes(settings.MAILGUN_ACCESS_KEY), + msg=utf8bytes("%s%s" % (timestamp, token)), + digestmod=hashlib.sha256, + ).hexdigest() + ): + logger.info( + "Rejected request: signature: %s, timestamp: %s, token: %s", + signature, + timestamp, + token, + ) raise PermissionDenied() - _, sender = parseaddr(params.get('from')) + _, sender = parseaddr(params.get("from")) if not sender: - logger.info('Rejected invalid sender: %s', params.get('from')) + logger.info("Rejected invalid sender: %s", params.get("from")) return HttpResponse(status=406) try: user = User.objects.get(email__iexact=sender) except (User.DoesNotExist, User.MultipleObjectsReturned): - logger.info('Rejected unknown sender: %s: %s', sender, params.get('from')) + logger.info( + "Rejected unknown sender: %s: %s", sender, params.get("from") + ) return HttpResponse(status=406) try: registration = RegistrationProfile.objects.get(user=user) except RegistrationProfile.DoesNotExist: - logger.info('Rejected sender without RegistrationProfile: %s: %s', sender, params.get('from')) + logger.info( + "Rejected sender without RegistrationProfile: %s: %s", + sender, + params.get("from"), + ) return HttpResponse(status=406) if registration.activated: - logger.info('Rejected activated sender: %s: %s', sender, params.get('from')) + logger.info( + "Rejected activated sender: %s: %s", sender, params.get("from") + ) return HttpResponse(status=406) key = registration.activation_key - if key in params.get('body-plain', '') or key in params.get('body-html', ''): - if RegistrationProfile.objects.activate_user(key, get_current_site(request)): - logger.info('Activated sender: %s: %s', sender, params.get('from')) - return HttpResponse('Activated', status=200) - logger.info('Failed to activate sender: %s: %s', sender, params.get('from')) + if key in params.get("body-plain", "") or key in params.get( + "body-html", "" + ): + if RegistrationProfile.objects.activate_user( + key, get_current_site(request) + ): + logger.info("Activated sender: %s: %s", sender, params.get("from")) + return HttpResponse("Activated", status=200) + logger.info( + "Failed to activate sender: %s: %s", sender, params.get("from") + ) else: - logger.info('Activation key not found: %s: %s', sender, params.get('from')) + logger.info( + "Activation key not found: %s: %s", sender, params.get("from") + ) return HttpResponse(status=406) @method_decorator(csrf_exempt) diff --git a/judge/views/notification.py b/judge/views/notification.py index b0d350f..ea8d55f 100644 --- a/judge/views/notification.py +++ b/judge/views/notification.py @@ -7,46 +7,48 @@ from django.db.models import BooleanField, Value from judge.utils.cachedict import CacheDict from judge.models import Profile, Comment, Notification -__all__ = ['NotificationList'] +__all__ = ["NotificationList"] + class NotificationList(ListView): model = Notification - context_object_name = 'notifications' - template_name = 'notification/list.html' - + context_object_name = "notifications" + template_name = "notification/list.html" + def get_queryset(self): self.unseen_cnt = self.request.profile.count_unseen_notifications - + query = { - 'owner': self.request.profile, + "owner": self.request.profile, } - self.queryset = Notification.objects.filter(**query)\ - .order_by('-time')[:100] \ - .annotate(seen=Value(True, output_field=BooleanField())) - + self.queryset = ( + Notification.objects.filter(**query) + .order_by("-time")[:100] + .annotate(seen=Value(True, output_field=BooleanField())) + ) + # Mark the several first unseen for cnt, q in enumerate(self.queryset): - if (cnt < self.unseen_cnt): + if cnt < self.unseen_cnt: q.seen = False else: break - + return self.queryset - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['unseen_count'] = self.unseen_cnt - context['title'] = _('Notifications (%d unseen)' % context['unseen_count']) - context['has_notifications'] = self.queryset.exists() - context['page_titles'] = CacheDict(lambda page: Comment.get_page_title(page)) + context["unseen_count"] = self.unseen_cnt + context["title"] = _("Notifications (%d unseen)" % context["unseen_count"]) + context["has_notifications"] = self.queryset.exists() + context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page)) return context def get(self, request, *args, **kwargs): ret = super().get(request, *args, **kwargs) - + # update after rendering Notification.objects.filter(owner=self.request.profile).update(read=True) - - return ret + return ret diff --git a/judge/views/organization.py b/judge/views/organization.py index 057e617..2883fc0 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -14,18 +14,45 @@ from django.utils import timezone from django.utils.safestring import mark_safe from django.utils.translation import gettext as _, gettext_lazy, ungettext from django.views.generic import DetailView, FormView, ListView, UpdateView, View -from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin +from django.views.generic.detail import ( + SingleObjectMixin, + SingleObjectTemplateResponseMixin, +) from reversion import revisions from judge.forms import EditOrganizationForm -from judge.models import BlogPost, Comment, Organization, OrganizationRequest, Problem, Profile, Contest +from judge.models import ( + BlogPost, + Comment, + Organization, + OrganizationRequest, + Problem, + Profile, + Contest, +) from judge.utils.ranker import ranker -from judge.utils.views import TitleMixin, generic_message, QueryStringSortMixin, DiggPaginatorMixin +from judge.utils.views import ( + TitleMixin, + generic_message, + QueryStringSortMixin, + DiggPaginatorMixin, +) + +__all__ = [ + "OrganizationList", + "OrganizationHome", + "OrganizationUsers", + "OrganizationMembershipChange", + "JoinOrganization", + "LeaveOrganization", + "EditOrganization", + "RequestJoinOrganization", + "OrganizationRequestDetail", + "OrganizationRequestView", + "OrganizationRequestLog", + "KickUserWidgetView", +] -__all__ = ['OrganizationList', 'OrganizationHome', 'OrganizationUsers', 'OrganizationMembershipChange', - 'JoinOrganization', 'LeaveOrganization', 'EditOrganization', 'RequestJoinOrganization', - 'OrganizationRequestDetail', 'OrganizationRequestView', 'OrganizationRequestLog', - 'KickUserWidgetView'] class OrganizationBase(object): def can_edit_organization(self, org=None): @@ -34,20 +61,25 @@ class OrganizationBase(object): if not self.request.user.is_authenticated: return False profile_id = self.request.profile.id - return org.admins.filter(id=profile_id).exists() or org.registrant_id == profile_id + return ( + org.admins.filter(id=profile_id).exists() or org.registrant_id == profile_id + ) def is_member(self, org=None): if org is None: org = self.object - return self.request.profile in org if self.request.user.is_authenticated else False + return ( + self.request.profile in org if self.request.user.is_authenticated else False + ) + class OrganizationMixin(OrganizationBase): - context_object_name = 'organization' + context_object_name = "organization" model = Organization def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['logo_override_image'] = self.object.logo_override_image + context["logo_override_image"] = self.object.logo_override_image return context def dispatch(self, request, *args, **kwargs): @@ -56,92 +88,131 @@ class OrganizationMixin(OrganizationBase): except Http404: key = kwargs.get(self.slug_url_kwarg, None) if key: - return generic_message(request, _('No such organization'), - _('Could not find an organization with the key "%s".') % key) + return generic_message( + request, + _("No such organization"), + _('Could not find an organization with the key "%s".') % key, + ) else: - return generic_message(request, _('No such organization'), - _('Could not find such organization.')) - + return generic_message( + request, + _("No such organization"), + _("Could not find such organization."), + ) + class OrganizationDetailView(OrganizationMixin, DetailView): def get(self, request, *args, **kwargs): self.object = self.get_object() - if self.object.slug != kwargs['slug']: - return HttpResponsePermanentRedirect(request.get_full_path().replace(kwargs['slug'], self.object.slug)) + if self.object.slug != kwargs["slug"]: + return HttpResponsePermanentRedirect( + request.get_full_path().replace(kwargs["slug"], self.object.slug) + ) context = self.get_context_data(object=self.object) return self.render_to_response(context) class OrganizationList(TitleMixin, ListView, OrganizationBase): model = Organization - context_object_name = 'organizations' - template_name = 'organization/list.html' - title = gettext_lazy('Organizations') + context_object_name = "organizations" + template_name = "organization/list.html" + title = gettext_lazy("Organizations") def get_queryset(self): - return super(OrganizationList, self).get_queryset().annotate(member_count=Count('member')) + return ( + super(OrganizationList, self) + .get_queryset() + .annotate(member_count=Count("member")) + ) def get_context_data(self, **kwargs): context = super(OrganizationList, self).get_context_data(**kwargs) - context['my_organizations'] = set() + context["my_organizations"] = set() - for organization in context['organizations']: + for organization in context["organizations"]: if self.can_edit_organization(organization) or self.is_member(organization): - context['my_organizations'].add(organization) + context["my_organizations"].add(organization) return context + class OrganizationHome(OrganizationDetailView): - template_name = 'organization/home.html' + template_name = "organization/home.html" def get_context_data(self, **kwargs): context = super(OrganizationHome, self).get_context_data(**kwargs) - context['title'] = self.object.name - context['can_edit'] = self.can_edit_organization() - context['is_member'] = self.is_member() - context['new_problems'] = Problem.objects.filter(is_public=True, is_organization_private=True, - organizations=self.object) \ - .order_by('-date', '-id')[:settings.DMOJ_BLOG_NEW_PROBLEM_COUNT] - context['new_contests'] = Contest.objects.filter(is_visible=True, is_organization_private=True, - organizations=self.object) \ - .order_by('-end_time', '-id')[:settings.DMOJ_BLOG_NEW_CONTEST_COUNT] + context["title"] = self.object.name + context["can_edit"] = self.can_edit_organization() + context["is_member"] = self.is_member() + context["new_problems"] = Problem.objects.filter( + is_public=True, is_organization_private=True, organizations=self.object + ).order_by("-date", "-id")[: settings.DMOJ_BLOG_NEW_PROBLEM_COUNT] + context["new_contests"] = Contest.objects.filter( + is_visible=True, is_organization_private=True, organizations=self.object + ).order_by("-end_time", "-id")[: settings.DMOJ_BLOG_NEW_CONTEST_COUNT] - context['posts'] = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now(), - is_organization_private=True, organizations=self.object) \ - .order_by('-sticky', '-publish_on') \ - .prefetch_related('authors__user', 'organizations') - context['post_comment_counts'] = { - int(page[2:]): count for page, count in - Comment.objects.filter(page__in=['b:%d' % post.id for post in context['posts']], hidden=False) - .values_list('page').annotate(count=Count('page')).order_by() + context["posts"] = ( + BlogPost.objects.filter( + visible=True, + publish_on__lte=timezone.now(), + is_organization_private=True, + organizations=self.object, + ) + .order_by("-sticky", "-publish_on") + .prefetch_related("authors__user", "organizations") + ) + context["post_comment_counts"] = { + int(page[2:]): count + for page, count in Comment.objects.filter( + page__in=["b:%d" % post.id for post in context["posts"]], hidden=False + ) + .values_list("page") + .annotate(count=Count("page")) + .order_by() } - context['pending_count'] = OrganizationRequest.objects.filter(state='P', organization=self.object).count() + context["pending_count"] = OrganizationRequest.objects.filter( + state="P", organization=self.object + ).count() return context class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView): - template_name = 'organization/users.html' - all_sorts = frozenset(('points', 'problem_count', 'rating', 'performance_points')) + template_name = "organization/users.html" + all_sorts = frozenset(("points", "problem_count", "rating", "performance_points")) default_desc = all_sorts - default_sort = '-performance_points' - + default_sort = "-performance_points" + def get_context_data(self, **kwargs): context = super(OrganizationUsers, self).get_context_data(**kwargs) - context['title'] = _('%s Members') % self.object.name - context['partial'] = True - context['is_admin'] = self.can_edit_organization() - context['kick_url'] = reverse('organization_user_kick', args=[self.object.id, self.object.slug]) + context["title"] = _("%s Members") % self.object.name + context["partial"] = True + context["is_admin"] = self.can_edit_organization() + context["kick_url"] = reverse( + "organization_user_kick", args=[self.object.id, self.object.slug] + ) - context['users'] = ranker( - self.get_object().members.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user') \ - .only('display_rank', 'user__username', 'points', 'rating', 'performance_points', 'problem_count') + context["users"] = ranker( + self.get_object() + .members.filter(is_unlisted=False) + .order_by(self.order, "id") + .select_related("user") + .only( + "display_rank", + "user__username", + "points", + "rating", + "performance_points", + "problem_count", ) - context['first_page_href'] = '.' + ) + context["first_page_href"] = "." context.update(self.get_sort_context()) return context -class OrganizationMembershipChange(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View): +class OrganizationMembershipChange( + LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View +): def post(self, request, *args, **kwargs): org = self.get_object() response = self.handle(request, org, request.profile) @@ -156,29 +227,42 @@ class OrganizationMembershipChange(LoginRequiredMixin, OrganizationMixin, Single class JoinOrganization(OrganizationMembershipChange): def handle(self, request, org, profile): if profile.organizations.filter(id=org.id).exists(): - return generic_message(request, _('Joining organization'), _('You are already in the organization.')) + return generic_message( + request, + _("Joining organization"), + _("You are already in the organization."), + ) if not org.is_open: - return generic_message(request, _('Joining organization'), _('This organization is not open.')) + return generic_message( + request, _("Joining organization"), _("This organization is not open.") + ) max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT if profile.organizations.filter(is_open=True).count() >= max_orgs: return generic_message( - request, _('Joining organization'), - _('You may not be part of more than {count} public organizations.').format(count=max_orgs), + request, + _("Joining organization"), + _( + "You may not be part of more than {count} public organizations." + ).format(count=max_orgs), ) profile.organizations.add(org) profile.save() - cache.delete(make_template_fragment_key('org_member_count', (org.id,))) + cache.delete(make_template_fragment_key("org_member_count", (org.id,))) class LeaveOrganization(OrganizationMembershipChange): def handle(self, request, org, profile): if not profile.organizations.filter(id=org.id).exists(): - return generic_message(request, _('Leaving organization'), _('You are not in "%s".') % org.short_name) + return generic_message( + request, + _("Leaving organization"), + _('You are not in "%s".') % org.short_name, + ) profile.organizations.remove(org) - cache.delete(make_template_fragment_key('org_member_count', (org.id,))) + cache.delete(make_template_fragment_key("org_member_count", (org.id,))) class OrganizationRequestForm(Form): @@ -187,9 +271,9 @@ class OrganizationRequestForm(Form): class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView): model = Organization - slug_field = 'key' - slug_url_kwarg = 'key' - template_name = 'organization/requests/request.html' + slug_field = "key" + slug_url_kwarg = "key" + template_name = "organization/requests/request.html" form_class = OrganizationRequestForm def dispatch(self, request, *args, **kwargs): @@ -200,75 +284,99 @@ class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView): context = super(RequestJoinOrganization, self).get_context_data(**kwargs) if self.object.is_open: raise Http404() - context['title'] = _('Request to join %s') % self.object.name + context["title"] = _("Request to join %s") % self.object.name return context def form_valid(self, form): request = OrganizationRequest() request.organization = self.get_object() request.user = self.request.profile - request.reason = form.cleaned_data['reason'] - request.state = 'P' + request.reason = form.cleaned_data["reason"] + request.state = "P" request.save() - return HttpResponseRedirect(reverse('request_organization_detail', args=( - request.organization.id, request.organization.slug, request.id, - ))) + return HttpResponseRedirect( + reverse( + "request_organization_detail", + args=( + request.organization.id, + request.organization.slug, + request.id, + ), + ) + ) class OrganizationRequestDetail(LoginRequiredMixin, TitleMixin, DetailView): model = OrganizationRequest - template_name = 'organization/requests/detail.html' - title = gettext_lazy('Join request detail') - pk_url_kwarg = 'rpk' + template_name = "organization/requests/detail.html" + title = gettext_lazy("Join request detail") + pk_url_kwarg = "rpk" def get_object(self, queryset=None): object = super(OrganizationRequestDetail, self).get_object(queryset) profile = self.request.profile - if object.user_id != profile.id and not object.organization.admins.filter(id=profile.id).exists(): + if ( + object.user_id != profile.id + and not object.organization.admins.filter(id=profile.id).exists() + ): raise PermissionDenied() return object -OrganizationRequestFormSet = modelformset_factory(OrganizationRequest, extra=0, fields=('state',), can_delete=True) +OrganizationRequestFormSet = modelformset_factory( + OrganizationRequest, extra=0, fields=("state",), can_delete=True +) -class OrganizationRequestBaseView(TitleMixin, LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, View): +class OrganizationRequestBaseView( + TitleMixin, + LoginRequiredMixin, + SingleObjectTemplateResponseMixin, + SingleObjectMixin, + View, +): model = Organization - slug_field = 'key' - slug_url_kwarg = 'key' + slug_field = "key" + slug_url_kwarg = "key" tab = None def get_object(self, queryset=None): organization = super(OrganizationRequestBaseView, self).get_object(queryset) - if not (organization.admins.filter(id=self.request.profile.id).exists() or - organization.registrant_id == self.request.profile.id): + if not ( + organization.admins.filter(id=self.request.profile.id).exists() + or organization.registrant_id == self.request.profile.id + ): raise PermissionDenied() return organization def get_content_title(self): - href = reverse('organization_home', args=[self.object.id, self.object.slug]) - return mark_safe(f'Manage join requests for {self.object.name}') - + href = reverse("organization_home", args=[self.object.id, self.object.slug]) + return mark_safe( + f'Manage join requests for {self.object.name}' + ) + def get_context_data(self, **kwargs): context = super(OrganizationRequestBaseView, self).get_context_data(**kwargs) - context['title'] = _('Managing join requests for %s') % self.object.name - context['tab'] = self.tab + context["title"] = _("Managing join requests for %s") % self.object.name + context["tab"] = self.tab return context class OrganizationRequestView(OrganizationRequestBaseView): - template_name = 'organization/requests/pending.html' - tab = 'pending' + template_name = "organization/requests/pending.html" + tab = "pending" def get_context_data(self, **kwargs): context = super(OrganizationRequestView, self).get_context_data(**kwargs) - context['formset'] = self.formset + context["formset"] = self.formset return context def get(self, request, *args, **kwargs): self.object = self.get_object() self.formset = OrganizationRequestFormSet( - queryset=OrganizationRequest.objects.filter(state='P', organization=self.object), + queryset=OrganizationRequest.objects.filter( + state="P", organization=self.object + ), ) context = self.get_context_data(object=self.object) return self.render_to_response(context) @@ -279,24 +387,43 @@ class OrganizationRequestView(OrganizationRequestBaseView): if formset.is_valid(): if organization.slots is not None: deleted_set = set(formset.deleted_forms) - to_approve = sum(form.cleaned_data['state'] == 'A' for form in formset.forms if form not in deleted_set) + to_approve = sum( + form.cleaned_data["state"] == "A" + for form in formset.forms + if form not in deleted_set + ) can_add = organization.slots - organization.members.count() if to_approve > can_add: - messages.error(request, _('Your organization can only receive %d more members. ' - 'You cannot approve %d users.') % (can_add, to_approve)) - return self.render_to_response(self.get_context_data(object=organization)) + messages.error( + request, + _( + "Your organization can only receive %d more members. " + "You cannot approve %d users." + ) + % (can_add, to_approve), + ) + return self.render_to_response( + self.get_context_data(object=organization) + ) approved, rejected = 0, 0 for obj in formset.save(): - if obj.state == 'A': + if obj.state == "A": obj.user.organizations.add(obj.organization) approved += 1 - elif obj.state == 'R': + elif obj.state == "R": rejected += 1 - messages.success(request, - ungettext('Approved %d user.', 'Approved %d users.', approved) % approved + '\n' + - ungettext('Rejected %d user.', 'Rejected %d users.', rejected) % rejected) - cache.delete(make_template_fragment_key('org_member_count', (organization.id,))) + messages.success( + request, + ungettext("Approved %d user.", "Approved %d users.", approved) + % approved + + "\n" + + ungettext("Rejected %d user.", "Rejected %d users.", rejected) + % rejected, + ) + cache.delete( + make_template_fragment_key("org_member_count", (organization.id,)) + ) return HttpResponseRedirect(request.get_full_path()) return self.render_to_response(self.get_context_data(object=organization)) @@ -304,9 +431,9 @@ class OrganizationRequestView(OrganizationRequestBaseView): class OrganizationRequestLog(OrganizationRequestBaseView): - states = ('A', 'R') - tab = 'log' - template_name = 'organization/requests/log.html' + states = ("A", "R") + tab = "log" + template_name = "organization/requests/log.html" def get(self, request, *args, **kwargs): self.object = self.get_object() @@ -315,17 +442,17 @@ class OrganizationRequestLog(OrganizationRequestBaseView): def get_context_data(self, **kwargs): context = super(OrganizationRequestLog, self).get_context_data(**kwargs) - context['requests'] = self.object.requests.filter(state__in=self.states) + context["requests"] = self.object.requests.filter(state__in=self.states) return context class EditOrganization(LoginRequiredMixin, TitleMixin, OrganizationMixin, UpdateView): - template_name = 'organization/edit.html' + template_name = "organization/edit.html" model = Organization form_class = EditOrganizationForm def get_title(self): - return _('Editing %s') % self.object.name + return _("Editing %s") % self.object.name def get_object(self, queryset=None): object = super(EditOrganization, self).get_object() @@ -335,13 +462,14 @@ class EditOrganization(LoginRequiredMixin, TitleMixin, OrganizationMixin, Update def get_form(self, form_class=None): form = super(EditOrganization, self).get_form(form_class) - form.fields['admins'].queryset = \ - Profile.objects.filter(Q(organizations=self.object) | Q(admin_of=self.object)).distinct() + form.fields["admins"].queryset = Profile.objects.filter( + Q(organizations=self.object) | Q(admin_of=self.object) + ).distinct() return form def form_valid(self, form): with transaction.atomic(), revisions.create_revision(): - revisions.set_comment(_('Edited from site')) + revisions.set_comment(_("Edited from site")) revisions.set_user(self.request.user) return super(EditOrganization, self).form_valid(form) @@ -349,27 +477,45 @@ class EditOrganization(LoginRequiredMixin, TitleMixin, OrganizationMixin, Update try: return super(EditOrganization, self).dispatch(request, *args, **kwargs) except PermissionDenied: - return generic_message(request, _("Can't edit organization"), - _('You are not allowed to edit this organization.'), status=403) + return generic_message( + request, + _("Can't edit organization"), + _("You are not allowed to edit this organization."), + status=403, + ) -class KickUserWidgetView(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View): +class KickUserWidgetView( + LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View +): def post(self, request, *args, **kwargs): organization = self.get_object() if not self.can_edit_organization(organization): - return generic_message(request, _("Can't edit organization"), - _('You are not allowed to kick people from this organization.'), status=403) + return generic_message( + request, + _("Can't edit organization"), + _("You are not allowed to kick people from this organization."), + status=403, + ) try: - user = Profile.objects.get(id=request.POST.get('user', None)) + user = Profile.objects.get(id=request.POST.get("user", None)) except Profile.DoesNotExist: - return generic_message(request, _("Can't kick user"), - _('The user you are trying to kick does not exist!'), status=400) + return generic_message( + request, + _("Can't kick user"), + _("The user you are trying to kick does not exist!"), + status=400, + ) if not organization.members.filter(id=user.id).exists(): - return generic_message(request, _("Can't kick user"), - _('The user you are trying to kick is not in organization: %s.') % - organization.name, status=400) + return generic_message( + request, + _("Can't kick user"), + _("The user you are trying to kick is not in organization: %s.") + % organization.name, + status=400, + ) organization.members.remove(user) return HttpResponseRedirect(organization.get_users_url()) diff --git a/judge/views/preview.py b/judge/views/preview.py index 5fbd2e8..e4581d4 100644 --- a/judge/views/preview.py +++ b/judge/views/preview.py @@ -5,46 +5,48 @@ from django.views.generic.base import ContextMixin, TemplateResponseMixin, View class MarkdownPreviewView(TemplateResponseMixin, ContextMixin, View): def post(self, request, *args, **kwargs): try: - self.preview_data = data = request.POST['preview'] + self.preview_data = data = request.POST["preview"] except KeyError: - return HttpResponseBadRequest('No preview data specified.') + return HttpResponseBadRequest("No preview data specified.") - return self.render_to_response(self.get_context_data( - preview_data=data, - )) + return self.render_to_response( + self.get_context_data( + preview_data=data, + ) + ) class ProblemMarkdownPreviewView(MarkdownPreviewView): - template_name = 'problem/preview.html' + template_name = "problem/preview.html" class BlogMarkdownPreviewView(MarkdownPreviewView): - template_name = 'blog/preview.html' + template_name = "blog/preview.html" class ContestMarkdownPreviewView(MarkdownPreviewView): - template_name = 'contest/preview.html' + template_name = "contest/preview.html" class CommentMarkdownPreviewView(MarkdownPreviewView): - template_name = 'comments/preview.html' + template_name = "comments/preview.html" class ProfileMarkdownPreviewView(MarkdownPreviewView): - template_name = 'user/preview.html' + template_name = "user/preview.html" class OrganizationMarkdownPreviewView(MarkdownPreviewView): - template_name = 'organization/preview.html' + template_name = "organization/preview.html" class SolutionMarkdownPreviewView(MarkdownPreviewView): - template_name = 'solution-preview.html' + template_name = "solution-preview.html" class LicenseMarkdownPreviewView(MarkdownPreviewView): - template_name = 'license-preview.html' + template_name = "license-preview.html" class TicketMarkdownPreviewView(MarkdownPreviewView): - template_name = 'ticket/preview.html' + template_name = "ticket/preview.html" diff --git a/judge/views/problem.py b/judge/views/problem.py index a00e9df..08bcb54 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -13,7 +13,13 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.db.models import Count, F, Prefetch, Q, Sum, Case, When, IntegerField from django.db.utils import ProgrammingError -from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect, JsonResponse +from django.http import ( + Http404, + HttpResponse, + HttpResponseForbidden, + HttpResponseRedirect, + JsonResponse, +) from django.shortcuts import get_object_or_404, render from django.template.loader import get_template from django.urls import reverse @@ -28,17 +34,43 @@ from django.views.generic.detail import SingleObjectMixin from judge.comments import CommentedDetailView from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm -from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemClarification, \ - ProblemGroup, ProblemTranslation, ProblemType, ProblemPointsVote, RuntimeVersion, Solution, Submission, SubmissionSource, \ - TranslatedProblemForeignKeyQuerySet, Organization , VolunteerProblemVote +from judge.models import ( + ContestProblem, + ContestSubmission, + Judge, + Language, + Problem, + ProblemClarification, + ProblemGroup, + ProblemTranslation, + ProblemType, + ProblemPointsVote, + RuntimeVersion, + Solution, + Submission, + SubmissionSource, + TranslatedProblemForeignKeyQuerySet, + Organization, + VolunteerProblemVote, +) from judge.pdf_problems import DefaultPdfMaker, HAS_PDF from judge.utils.diggpaginator import DiggPaginator from judge.utils.opengraph import generate_opengraph -from judge.utils.problems import contest_attempted_ids, contest_completed_ids, hot_problems, user_attempted_ids, \ - user_completed_ids +from judge.utils.problems import ( + contest_attempted_ids, + contest_completed_ids, + hot_problems, + user_attempted_ids, + user_completed_ids, +) from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.tickets import own_ticket_filter -from judge.utils.views import QueryStringSortMixin, SingleObjectFormView, TitleMixin, generic_message +from judge.utils.views import ( + QueryStringSortMixin, + SingleObjectFormView, + TitleMixin, + generic_message, +) from judge.ml.collab_filter import CollabFilter @@ -50,14 +82,17 @@ def get_contest_problem(problem, profile): def get_contest_submission_count(problem, profile, virtual): - return profile.current_contest.submissions.exclude(submission__status__in=['IE']) \ - .filter(problem__problem=problem, participation__virtual=virtual).count() + return ( + profile.current_contest.submissions.exclude(submission__status__in=["IE"]) + .filter(problem__problem=problem, participation__virtual=virtual) + .count() + ) class ProblemMixin(object): model = Problem - slug_url_kwarg = 'problem' - slug_field = 'code' + slug_url_kwarg = "problem" + slug_field = "code" def get_object(self, queryset=None): problem = super(ProblemMixin, self).get_object(queryset) @@ -67,8 +102,12 @@ class ProblemMixin(object): def no_such_problem(self): code = self.kwargs.get(self.slug_url_kwarg, None) - return generic_message(self.request, _('No such problem'), - _('Could not find a problem with the code "%s".') % code, status=404) + return generic_message( + self.request, + _("No such problem"), + _('Could not find a problem with the code "%s".') % code, + status=404, + ) def get(self, request, *args, **kwargs): try: @@ -93,8 +132,11 @@ class SolvedProblemMixin(object): @cached_property def in_contest(self): - return self.profile is not None and self.profile.current_contest is not None \ + return ( + self.profile is not None + and self.profile.current_contest is not None and self.request.in_contest_mode + ) @cached_property def contest(self): @@ -107,174 +149,221 @@ class SolvedProblemMixin(object): return self.request.profile -class ProblemSolution(SolvedProblemMixin, ProblemMixin, TitleMixin, CommentedDetailView): - context_object_name = 'problem' - template_name = 'problem/editorial.html' +class ProblemSolution( + SolvedProblemMixin, ProblemMixin, TitleMixin, CommentedDetailView +): + context_object_name = "problem" + template_name = "problem/editorial.html" def get_title(self): - return _('Editorial for {0}').format(self.object.name) + return _("Editorial for {0}").format(self.object.name) def get_content_title(self): - return format_html(_(u'Editorial for {0}'), self.object.name, - reverse('problem_detail', args=[self.object.code])) + return format_html( + _('Editorial for {0}'), + self.object.name, + reverse("problem_detail", args=[self.object.code]), + ) def get_context_data(self, **kwargs): context = super(ProblemSolution, self).get_context_data(**kwargs) solution = get_object_or_404(Solution, problem=self.object) - if (not solution.is_public or solution.publish_on > timezone.now()) and \ - not self.request.user.has_perm('judge.see_private_solution'): + if ( + not solution.is_public or solution.publish_on > timezone.now() + ) and not self.request.user.has_perm("judge.see_private_solution"): raise Http404() - context['solution'] = solution - context['has_solved_problem'] = self.object.id in self.get_completed_problems() + context["solution"] = solution + context["has_solved_problem"] = self.object.id in self.get_completed_problems() return context def get_comment_page(self): - return 's:' + self.object.code + return "s:" + self.object.code -class ProblemRaw(ProblemMixin, TitleMixin, TemplateResponseMixin, SingleObjectMixin, View): - context_object_name = 'problem' - template_name = 'problem/raw.html' +class ProblemRaw( + ProblemMixin, TitleMixin, TemplateResponseMixin, SingleObjectMixin, View +): + context_object_name = "problem" + template_name = "problem/raw.html" def get_title(self): return self.object.name def get_context_data(self, **kwargs): context = super(ProblemRaw, self).get_context_data(**kwargs) - context['problem_name'] = self.object.name - context['url'] = self.request.build_absolute_uri() - context['description'] = self.object.description + context["problem_name"] = self.object.name + context["url"] = self.request.build_absolute_uri() + context["description"] = self.object.description return context def get(self, request, *args, **kwargs): self.object = self.get_object() with translation.override(settings.LANGUAGE_CODE): - return self.render_to_response(self.get_context_data( - object=self.object, - )) + return self.render_to_response( + self.get_context_data( + object=self.object, + ) + ) class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView): - context_object_name = 'problem' - template_name = 'problem/problem.html' + context_object_name = "problem" + template_name = "problem/problem.html" def get_comment_page(self): - return 'p:%s' % self.object.code + return "p:%s" % self.object.code def get_context_data(self, **kwargs): context = super(ProblemDetail, self).get_context_data(**kwargs) user = self.request.user authed = user.is_authenticated - context['has_submissions'] = authed and Submission.objects.filter(user=user.profile, - problem=self.object).exists() - contest_problem = (None if not authed or user.profile.current_contest is None else - get_contest_problem(self.object, user.profile)) - context['contest_problem'] = contest_problem + context["has_submissions"] = ( + authed + and Submission.objects.filter( + user=user.profile, problem=self.object + ).exists() + ) + contest_problem = ( + None + if not authed or user.profile.current_contest is None + else get_contest_problem(self.object, user.profile) + ) + context["contest_problem"] = contest_problem if contest_problem: clarifications = self.object.clarifications - context['has_clarifications'] = clarifications.count() > 0 - context['clarifications'] = clarifications.order_by('-date') - context['submission_limit'] = contest_problem.max_submissions + context["has_clarifications"] = clarifications.count() > 0 + context["clarifications"] = clarifications.order_by("-date") + context["submission_limit"] = contest_problem.max_submissions if contest_problem.max_submissions: - context['submissions_left'] = max(contest_problem.max_submissions - - get_contest_submission_count(self.object, user.profile, - user.profile.current_contest.virtual), 0) + context["submissions_left"] = max( + contest_problem.max_submissions + - get_contest_submission_count( + self.object, user.profile, user.profile.current_contest.virtual + ), + 0, + ) - context['available_judges'] = Judge.objects.filter(online=True, problems=self.object) - context['show_languages'] = self.object.allowed_languages.count() != Language.objects.count() - context['has_pdf_render'] = HAS_PDF - context['completed_problem_ids'] = self.get_completed_problems() - context['attempted_problems'] = self.get_attempted_problems() + context["available_judges"] = Judge.objects.filter( + online=True, problems=self.object + ) + context["show_languages"] = ( + self.object.allowed_languages.count() != Language.objects.count() + ) + context["has_pdf_render"] = HAS_PDF + context["completed_problem_ids"] = self.get_completed_problems() + context["attempted_problems"] = self.get_attempted_problems() can_edit = self.object.is_editable_by(user) - context['can_edit_problem'] = can_edit + context["can_edit_problem"] = can_edit if user.is_authenticated: tickets = self.object.tickets if not can_edit: tickets = tickets.filter(own_ticket_filter(user.profile.id)) - context['has_tickets'] = tickets.exists() - context['num_open_tickets'] = tickets.filter(is_open=True).values('id').distinct().count() + context["has_tickets"] = tickets.exists() + context["num_open_tickets"] = ( + tickets.filter(is_open=True).values("id").distinct().count() + ) try: - context['editorial'] = Solution.objects.get(problem=self.object) + context["editorial"] = Solution.objects.get(problem=self.object) except ObjectDoesNotExist: pass try: - translation = self.object.translations.get(language=self.request.LANGUAGE_CODE) + translation = self.object.translations.get( + language=self.request.LANGUAGE_CODE + ) except ProblemTranslation.DoesNotExist: - context['title'] = self.object.name - context['language'] = settings.LANGUAGE_CODE - context['description'] = self.object.description - context['translated'] = False + context["title"] = self.object.name + context["language"] = settings.LANGUAGE_CODE + context["description"] = self.object.description + context["translated"] = False else: - context['title'] = translation.name - context['language'] = self.request.LANGUAGE_CODE - context['description'] = translation.description - context['translated'] = True + context["title"] = translation.name + context["language"] = self.request.LANGUAGE_CODE + context["description"] = translation.description + context["translated"] = True if not self.object.og_image or not self.object.summary: - metadata = generate_opengraph('generated-meta-problem:%s:%d' % (context['language'], self.object.id), - context['description'], 'problem') - context['meta_description'] = self.object.summary or metadata[0] - context['og_image'] = self.object.og_image or metadata[1] + metadata = generate_opengraph( + "generated-meta-problem:%s:%d" % (context["language"], self.object.id), + context["description"], + "problem", + ) + context["meta_description"] = self.object.summary or metadata[0] + context["og_image"] = self.object.og_image or metadata[1] - context['can_vote'] = self.object.can_vote(self.request) - if context['can_vote']: + context["can_vote"] = self.object.can_vote(self.request) + if context["can_vote"]: try: - context['vote'] = ProblemPointsVote.objects.get(voter=user.profile, problem=self.object) + context["vote"] = ProblemPointsVote.objects.get( + voter=user.profile, problem=self.object + ) except ObjectDoesNotExist: - context['vote'] = None + context["vote"] = None else: - context['vote'] = None + context["vote"] = None - context['has_votes'] = False + context["has_votes"] = False if user.is_superuser: - all_votes = list(self.object.problem_points_votes.order_by('points').values_list('points', flat=True)) - context['all_votes'] = all_votes - context['has_votes'] = len(all_votes) > 0 - context['max_possible_vote'] = 600 - context['min_possible_vote'] = 100 + all_votes = list( + self.object.problem_points_votes.order_by("points").values_list( + "points", flat=True + ) + ) + context["all_votes"] = all_votes + context["has_votes"] = len(all_votes) > 0 + context["max_possible_vote"] = 600 + context["min_possible_vote"] = 100 return context class DeleteVote(ProblemMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): - return HttpResponseForbidden(status=405, content_type='text/plain') + return HttpResponseForbidden(status=405, content_type="text/plain") def post(self, request, *args, **kwargs): self.object = self.get_object() if not request.user.is_authenticated: - return HttpResponseForbidden('Not signed in.', content_type='text/plain') + return HttpResponseForbidden("Not signed in.", content_type="text/plain") elif self.object.can_vote(request.user): - ProblemPointsVote.objects.filter(voter=request.profile, problem=self.object).delete() - return HttpResponse('success', content_type='text/plain') + ProblemPointsVote.objects.filter( + voter=request.profile, problem=self.object + ).delete() + return HttpResponse("success", content_type="text/plain") else: - return HttpResponseForbidden('Not allowed to delete votes on this problem.', content_type='text/plain') + return HttpResponseForbidden( + "Not allowed to delete votes on this problem.", + content_type="text/plain", + ) class Vote(ProblemMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): - return HttpResponseForbidden(status=405, content_type='text/plain') + return HttpResponseForbidden(status=405, content_type="text/plain") def post(self, request, *args, **kwargs): self.object = self.get_object() if not self.object.can_vote(request): # Not allowed to vote for some reason. - return HttpResponseForbidden('Not allowed to vote on this problem.', content_type='text/plain') + return HttpResponseForbidden( + "Not allowed to vote on this problem.", content_type="text/plain" + ) form = ProblemPointsVoteForm(request.POST) if form.is_valid(): with transaction.atomic(): # Delete any pre existing votes. - ProblemPointsVote.objects.filter(voter=request.profile, problem=self.object).delete() + ProblemPointsVote.objects.filter( + voter=request.profile, problem=self.object + ).delete() vote = form.save(commit=False) vote.voter = request.profile vote.problem = self.object vote.save() - return JsonResponse({'points': vote.points}) + return JsonResponse({"points": vote.points}) else: return JsonResponse(form.errors, status=400) @@ -284,14 +373,14 @@ class LatexError(Exception): class ProblemPdfView(ProblemMixin, SingleObjectMixin, View): - logger = logging.getLogger('judge.problem.pdf') + logger = logging.getLogger("judge.problem.pdf") languages = set(map(itemgetter(0), settings.LANGUAGES)) def get(self, request, *args, **kwargs): if not HAS_PDF: raise Http404() - language = kwargs.get('language', self.request.LANGUAGE_CODE) + language = kwargs.get("language", self.request.LANGUAGE_CODE) if language not in self.languages: raise Http404() @@ -301,74 +390,103 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View): except ProblemTranslation.DoesNotExist: trans = None - cache = os.path.join(settings.DMOJ_PDF_PROBLEM_CACHE, '%s.%s.pdf' % (problem.code, language)) + cache = os.path.join( + settings.DMOJ_PDF_PROBLEM_CACHE, "%s.%s.pdf" % (problem.code, language) + ) if not os.path.exists(cache): - self.logger.info('Rendering: %s.%s.pdf', problem.code, language) + self.logger.info("Rendering: %s.%s.pdf", problem.code, language) with DefaultPdfMaker() as maker, translation.override(language): problem_name = problem.name if trans is None else trans.name - maker.html = get_template('problem/raw.html').render({ - 'problem': problem, - 'problem_name': problem_name, - 'description': problem.description if trans is None else trans.description, - 'url': request.build_absolute_uri(), - 'math_engine': maker.math_engine, - }).replace('"//', '"https://').replace("'//", "'https://") + maker.html = ( + get_template("problem/raw.html") + .render( + { + "problem": problem, + "problem_name": problem_name, + "description": problem.description + if trans is None + else trans.description, + "url": request.build_absolute_uri(), + "math_engine": maker.math_engine, + } + ) + .replace('"//', '"https://') + .replace("'//", "'https://") + ) maker.title = problem_name - assets = ['style.css', 'pygment-github.css'] - if maker.math_engine == 'jax': - assets.append('mathjax_config.js') + assets = ["style.css", "pygment-github.css"] + if maker.math_engine == "jax": + assets.append("mathjax_config.js") for file in assets: maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) maker.make() if not maker.success: - self.logger.error('Failed to render PDF for %s', problem.code) - return HttpResponse(maker.log, status=500, content_type='text/plain') + self.logger.error("Failed to render PDF for %s", problem.code) + return HttpResponse( + maker.log, status=500, content_type="text/plain" + ) shutil.move(maker.pdffile, cache) response = HttpResponse() - if hasattr(settings, 'DMOJ_PDF_PROBLEM_INTERNAL') and \ - request.META.get('SERVER_SOFTWARE', '').startswith('nginx/'): - response['X-Accel-Redirect'] = '%s/%s.%s.pdf' % (settings.DMOJ_PDF_PROBLEM_INTERNAL, problem.code, language) + if hasattr(settings, "DMOJ_PDF_PROBLEM_INTERNAL") and request.META.get( + "SERVER_SOFTWARE", "" + ).startswith("nginx/"): + response["X-Accel-Redirect"] = "%s/%s.%s.pdf" % ( + settings.DMOJ_PDF_PROBLEM_INTERNAL, + problem.code, + language, + ) else: - with open(cache, 'rb') as f: + with open(cache, "rb") as f: response.content = f.read() - response['Content-Type'] = 'application/pdf' - response['Content-Disposition'] = 'inline; filename=%s.%s.pdf' % (problem.code, language) + response["Content-Type"] = "application/pdf" + response["Content-Disposition"] = "inline; filename=%s.%s.pdf" % ( + problem.code, + language, + ) return response class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView): model = Problem - title = gettext_lazy('Problems') - context_object_name = 'problems' - template_name = 'problem/list.html' + title = gettext_lazy("Problems") + context_object_name = "problems" + template_name = "problem/list.html" paginate_by = 50 - sql_sort = frozenset(('date', 'points', 'ac_rate', 'user_count', 'code')) - manual_sort = frozenset(('name', 'group', 'solved', 'type')) + sql_sort = frozenset(("date", "points", "ac_rate", "user_count", "code")) + manual_sort = frozenset(("name", "group", "solved", "type")) all_sorts = sql_sort | manual_sort - default_desc = frozenset(('date', 'points', 'ac_rate', 'user_count')) - default_sort = '-date' + default_desc = frozenset(("date", "points", "ac_rate", "user_count")) + default_sort = "-date" first_page_href = None - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - paginator = DiggPaginator(queryset, per_page, body=6, padding=2, orphans=orphans, - allow_empty_first_page=allow_empty_first_page, **kwargs) + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + paginator = DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) if not self.in_contest: # Get the number of pages and then add in this magic. # noinspection PyStatementEffect paginator.num_pages queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE) - sort_key = self.order.lstrip('-') + sort_key = self.order.lstrip("-") if sort_key in self.sql_sort: queryset = queryset.order_by(self.order) - elif sort_key == 'name': - queryset = queryset.order_by(self.order.replace('name', 'i18n_name')) - elif sort_key == 'group': - queryset = queryset.order_by(self.order + '__name') - elif sort_key == 'solved': + elif sort_key == "name": + queryset = queryset.order_by(self.order.replace("name", "i18n_name")) + elif sort_key == "group": + queryset = queryset.order_by(self.order + "__name") + elif sort_key == "solved": if self.request.user.is_authenticated: profile = self.request.profile solved = user_completed_ids(profile) @@ -382,12 +500,18 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView return -1 queryset = list(queryset) - queryset.sort(key=_solved_sort_order, reverse=self.order.startswith('-')) - elif sort_key == 'type': + queryset.sort( + key=_solved_sort_order, reverse=self.order.startswith("-") + ) + elif sort_key == "type": if self.show_types: queryset = list(queryset) - queryset.sort(key=lambda problem: problem.types_list[0] if problem.types_list else '', - reverse=self.order.startswith('-')) + queryset.sort( + key=lambda problem: problem.types_list[0] + if problem.types_list + else "", + reverse=self.order.startswith("-"), + ) paginator.object_list = queryset return paginator @@ -398,24 +522,40 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView return self.request.profile def get_contest_queryset(self): - queryset = self.profile.current_contest.contest.contest_problems.select_related('problem__group') \ - .defer('problem__description').order_by('problem__code') \ - .annotate(user_count=Count('submission__participation', distinct=True)) \ - .order_by('order') - queryset = TranslatedProblemForeignKeyQuerySet.add_problem_i18n_name(queryset, 'i18n_name', - self.request.LANGUAGE_CODE, - 'problem__name') - return [{ - 'id': p['problem_id'], - 'code': p['problem__code'], - 'name': p['problem__name'], - 'i18n_name': p['i18n_name'], - 'group': {'full_name': p['problem__group__full_name']}, - 'points': p['points'], - 'partial': p['partial'], - 'user_count': p['user_count'], - } for p in queryset.values('problem_id', 'problem__code', 'problem__name', 'i18n_name', - 'problem__group__full_name', 'points', 'partial', 'user_count')] + queryset = ( + self.profile.current_contest.contest.contest_problems.select_related( + "problem__group" + ) + .defer("problem__description") + .order_by("problem__code") + .annotate(user_count=Count("submission__participation", distinct=True)) + .order_by("order") + ) + queryset = TranslatedProblemForeignKeyQuerySet.add_problem_i18n_name( + queryset, "i18n_name", self.request.LANGUAGE_CODE, "problem__name" + ) + return [ + { + "id": p["problem_id"], + "code": p["problem__code"], + "name": p["problem__name"], + "i18n_name": p["i18n_name"], + "group": {"full_name": p["problem__group__full_name"]}, + "points": p["points"], + "partial": p["partial"], + "user_count": p["user_count"], + } + for p in queryset.values( + "problem_id", + "problem__code", + "problem__name", + "i18n_name", + "problem__group__full_name", + "points", + "partial", + "user_count", + ) + ] def get_normal_queryset(self): filter = Q(is_public=True) @@ -423,42 +563,63 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView filter |= Q(authors=self.profile) filter |= Q(curators=self.profile) filter |= Q(testers=self.profile) - queryset = Problem.objects.filter(filter).select_related('group').defer('description') - if not self.request.user.has_perm('see_organization_problem'): + queryset = ( + Problem.objects.filter(filter).select_related("group").defer("description") + ) + if not self.request.user.has_perm("see_organization_problem"): filter = Q(is_organization_private=False) if self.profile is not None: filter |= Q(organizations__in=self.profile.organizations.all()) queryset = queryset.filter(filter) if self.profile is not None and self.hide_solved: - queryset = queryset.exclude(id__in=Submission.objects.filter(user=self.profile, points=F('problem__points')) - .values_list('problem__id', flat=True)) + queryset = queryset.exclude( + id__in=Submission.objects.filter( + user=self.profile, points=F("problem__points") + ).values_list("problem__id", flat=True) + ) if self.org_query: queryset = queryset.filter( - Q(organizations__in=self.org_query) | - Q(contests__contest__organizations__in=self.org_query)) + Q(organizations__in=self.org_query) + | Q(contests__contest__organizations__in=self.org_query) + ) if self.show_types: - queryset = queryset.prefetch_related('types') + queryset = queryset.prefetch_related("types") if self.category is not None: queryset = queryset.filter(group__id=self.category) if self.selected_types: queryset = queryset.filter(types__in=self.selected_types) - if 'search' in self.request.GET: - self.search_query = query = ' '.join(self.request.GET.getlist('search')).strip() + if "search" in self.request.GET: + self.search_query = query = " ".join( + self.request.GET.getlist("search") + ).strip() if query: if settings.ENABLE_FTS and self.full_text: - queryset = queryset.search(query, queryset.BOOLEAN).extra(order_by=['-relevance']) + queryset = queryset.search(query, queryset.BOOLEAN).extra( + order_by=["-relevance"] + ) else: queryset = queryset.filter( - Q(code__icontains=query) | Q(name__icontains=query) | - Q(translations__name__icontains=query, translations__language=self.request.LANGUAGE_CODE)) + Q(code__icontains=query) + | Q(name__icontains=query) + | Q( + translations__name__icontains=query, + translations__language=self.request.LANGUAGE_CODE, + ) + ) self.prepoint_queryset = queryset if self.point_start is not None: queryset = queryset.filter(points__gte=self.point_start) if self.point_end is not None: queryset = queryset.filter(points__lte=self.point_end) queryset = queryset.annotate( - has_public_editorial=Sum(Case(When(solution__is_public=True, then=1), - default=0, output_field=IntegerField()))) + has_public_editorial=Sum( + Case( + When(solution__is_public=True, then=1), + default=0, + output_field=IntegerField(), + ) + ) + ) return queryset.distinct() @@ -470,82 +631,108 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView def get_context_data(self, **kwargs): context = super(ProblemList, self).get_context_data(**kwargs) - - context['hide_solved'] = 0 if self.in_contest else int(self.hide_solved) - context['show_types'] = 0 if self.in_contest else int(self.show_types) - context['full_text'] = 0 if self.in_contest else int(self.full_text) - context['show_editorial'] = 0 if self.in_contest else int(self.show_editorial) - context['have_editorial'] = 0 if self.in_contest else int(self.have_editorial) - context['organizations'] = Organization.objects.all() - context['category'] = self.category - context['categories'] = ProblemGroup.objects.all() + context["hide_solved"] = 0 if self.in_contest else int(self.hide_solved) + context["show_types"] = 0 if self.in_contest else int(self.show_types) + context["full_text"] = 0 if self.in_contest else int(self.full_text) + context["show_editorial"] = 0 if self.in_contest else int(self.show_editorial) + context["have_editorial"] = 0 if self.in_contest else int(self.have_editorial) + + context["organizations"] = Organization.objects.all() + context["category"] = self.category + context["categories"] = ProblemGroup.objects.all() if self.show_types: - context['selected_types'] = self.selected_types - context['problem_types'] = ProblemType.objects.all() - context['has_fts'] = settings.ENABLE_FTS - context['org_query'] = self.org_query - context['search_query'] = self.search_query - context['completed_problem_ids'] = self.get_completed_problems() - context['attempted_problems'] = self.get_attempted_problems() - context['page_type'] = 'list' + context["selected_types"] = self.selected_types + context["problem_types"] = ProblemType.objects.all() + context["has_fts"] = settings.ENABLE_FTS + context["org_query"] = self.org_query + context["search_query"] = self.search_query + context["completed_problem_ids"] = self.get_completed_problems() + context["attempted_problems"] = self.get_attempted_problems() + context["page_type"] = "list" context.update(self.get_sort_paginate_context()) if not self.in_contest: context.update(self.get_sort_context()) - context['point_start'], context['point_end'], context['point_values'] = self.get_noui_slider_points() + ( + context["point_start"], + context["point_end"], + context["point_values"], + ) = self.get_noui_slider_points() else: - context['point_start'], context['point_end'], context['point_values'] = 0, 0, {} - context['hide_contest_scoreboard'] = self.contest.scoreboard_visibility in \ - (self.contest.SCOREBOARD_AFTER_CONTEST, self.contest.SCOREBOARD_AFTER_PARTICIPATION) - context['has_clarifications'] = False - + context["point_start"], context["point_end"], context["point_values"] = ( + 0, + 0, + {}, + ) + context["hide_contest_scoreboard"] = self.contest.scoreboard_visibility in ( + self.contest.SCOREBOARD_AFTER_CONTEST, + self.contest.SCOREBOARD_AFTER_PARTICIPATION, + ) + context["has_clarifications"] = False + if self.request.user.is_authenticated: participation = self.request.profile.current_contest if participation: - clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all()) - context['has_clarifications'] = clarifications.count() > 0 - context['clarifications'] = clarifications.order_by('-date') + clarifications = ProblemClarification.objects.filter( + problem__in=participation.contest.problems.all() + ) + context["has_clarifications"] = clarifications.count() > 0 + context["clarifications"] = clarifications.order_by("-date") if participation.contest.is_editable_by(self.request.user): - context['can_edit_contest'] = True - - context['page_prefix'] = None - context['page_suffix'] = suffix = ( - '?' + self.request.GET.urlencode()) if self.request.GET else '' - context['first_page_href'] = (self.first_page_href or '.') + suffix - + context["can_edit_contest"] = True + + context["page_prefix"] = None + context["page_suffix"] = suffix = ( + ("?" + self.request.GET.urlencode()) if self.request.GET else "" + ) + context["first_page_href"] = (self.first_page_href or ".") + suffix + return context def get_noui_slider_points(self): - points = sorted(self.prepoint_queryset.values_list('points', flat=True).distinct()) + points = sorted( + self.prepoint_queryset.values_list("points", flat=True).distinct() + ) if not points: return 0, 0, {} if len(points) == 1: - return points[0], points[0], { - 'min': points[0] - 1, - 'max': points[0] + 1, - } + return ( + points[0], + points[0], + { + "min": points[0] - 1, + "max": points[0] + 1, + }, + ) start, end = points[0], points[-1] if self.point_start is not None: start = self.point_start if self.point_end is not None: end = self.point_end - points_map = {0.0: 'min', 1.0: 'max'} + points_map = {0.0: "min", 1.0: "max"} size = len(points) - 1 - return start, end, {points_map.get(i / size, '%.2f%%' % (100 * i / size,)): j for i, j in enumerate(points)} + return ( + start, + end, + { + points_map.get(i / size, "%.2f%%" % (100 * i / size,)): j + for i, j in enumerate(points) + }, + ) def GET_with_session(self, request, key): if not request.GET: return request.session.get(key, False) - return request.GET.get(key, None) == '1' + return request.GET.get(key, None) == "1" def setup_problem_list(self, request): - self.hide_solved = self.GET_with_session(request, 'hide_solved') - self.show_types = self.GET_with_session(request, 'show_types') - self.full_text = self.GET_with_session(request, 'full_text') - self.show_editorial = self.GET_with_session(request, 'show_editorial') - self.have_editorial = self.GET_with_session(request, 'have_editorial') - + self.hide_solved = self.GET_with_session(request, "hide_solved") + self.show_types = self.GET_with_session(request, "show_types") + self.full_text = self.GET_with_session(request, "full_text") + self.show_editorial = self.GET_with_session(request, "show_editorial") + self.have_editorial = self.GET_with_session(request, "have_editorial") + self.search_query = None self.category = None self.org_query = [] @@ -554,23 +741,23 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView # This actually copies into the instance dictionary... self.all_sorts = set(self.all_sorts) if not self.show_types: - self.all_sorts.discard('type') + self.all_sorts.discard("type") - self.category = safe_int_or_none(request.GET.get('category')) - if 'type' in request.GET: + self.category = safe_int_or_none(request.GET.get("category")) + if "type" in request.GET: try: - self.selected_types = list(map(int, request.GET.getlist('type'))) + self.selected_types = list(map(int, request.GET.getlist("type"))) except ValueError: pass - if 'orgs' in request.GET: + if "orgs" in request.GET: try: - self.org_query = list(map(int, request.GET.getlist('orgs'))) + self.org_query = list(map(int, request.GET.getlist("orgs"))) except ValueError: pass - self.point_start = safe_float_or_none(request.GET.get('point_start')) - self.point_end = safe_float_or_none(request.GET.get('point_end')) + self.point_start = safe_float_or_none(request.GET.get("point_start")) + self.point_end = safe_float_or_none(request.GET.get("point_end")) def get(self, request, *args, **kwargs): self.setup_problem_list(request) @@ -578,39 +765,52 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView try: return super(ProblemList, self).get(request, *args, **kwargs) except ProgrammingError as e: - return generic_message(request, 'FTS syntax error', e.args[1], status=400) + return generic_message(request, "FTS syntax error", e.args[1], status=400) def post(self, request, *args, **kwargs): - to_update = ('hide_solved', 'show_types', 'full_text', - 'show_editorial', 'have_editorial') + to_update = ( + "hide_solved", + "show_types", + "full_text", + "show_editorial", + "have_editorial", + ) for key in to_update: if key in request.GET: - val = request.GET.get(key) == '1' + val = request.GET.get(key) == "1" request.session[key] = val else: request.session[key] = False return HttpResponseRedirect(request.get_full_path()) -cf_logger = logging.getLogger('judge.ml.collab_filter') +cf_logger = logging.getLogger("judge.ml.collab_filter") class ProblemFeed(ProblemList): model = Problem - context_object_name = 'problems' + context_object_name = "problems" paginate_by = 20 - title = _('Problem feed') + title = _("Problem feed") feed_type = None def GET_with_session(self, request, key): if not request.GET: - return request.session.get(key, key=='hide_solved') - return request.GET.get(key, None) == '1' + return request.session.get(key, key == "hide_solved") + return request.GET.get(key, None) == "1" - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - return DiggPaginator(queryset, per_page, body=6, padding=2, - orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs) + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + return DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) # arr = [[], [], ..] def merge_recommendation(self, arr): @@ -633,76 +833,94 @@ class ProblemFeed(ProblemList): def get_queryset(self): queryset = super(ProblemFeed, self).get_queryset() - + if self.have_editorial: queryset = queryset.filter(has_public_editorial=1) user = self.request.profile - if self.feed_type == 'new': - return queryset.order_by('-date') - elif user and self.feed_type == 'volunteer': - voted_problems = user.volunteer_problem_votes.values_list('problem', flat=True) - return queryset.exclude(id__in=voted_problems).order_by('?') + if self.feed_type == "new": + return queryset.order_by("-date") + elif user and self.feed_type == "volunteer": + voted_problems = user.volunteer_problem_votes.values_list( + "problem", flat=True + ) + return queryset.exclude(id__in=voted_problems).order_by("?") if not settings.ML_OUTPUT_PATH or not user: - return queryset.order_by('?') - + return queryset.order_by("?") + # Logging log_data = { - 'user': self.request.user.username, - 'cf': { - 'dot': {}, - 'cosine': {}, - }, - 'cf_time': { - 'dot': {}, - 'cosine': {} + "user": self.request.user.username, + "cf": { + "dot": {}, + "cosine": {}, }, + "cf_time": {"dot": {}, "cosine": {}}, } - cf_model = CollabFilter('collab_filter', log_time=log_data['cf']) - cf_time_model = CollabFilter('collab_filter_time', log_time=log_data['cf_time']) + cf_model = CollabFilter("collab_filter", log_time=log_data["cf"]) + cf_time_model = CollabFilter("collab_filter_time", log_time=log_data["cf_time"]) hot_problems_recommendations = [ - problem for problem in hot_problems(timedelta(days=7), 20) - if problem in queryset + problem + for problem in hot_problems(timedelta(days=7), 20) + if problem in queryset ] - q = self.merge_recommendation([ - cf_model.user_recommendations(user, queryset, cf_model.DOT, 100, - log_time=log_data['cf']['dot']), - cf_model.user_recommendations(user, queryset, cf_model.COSINE, 100, - log_time=log_data['cf']['cosine']), - cf_time_model.user_recommendations(user, queryset, cf_time_model.COSINE, 100, - log_time=log_data['cf_time']['cosine']), - cf_time_model.user_recommendations(user, queryset, cf_time_model.DOT, 100, - log_time=log_data['cf_time']['dot']), - hot_problems_recommendations - ]) + q = self.merge_recommendation( + [ + cf_model.user_recommendations( + user, queryset, cf_model.DOT, 100, log_time=log_data["cf"]["dot"] + ), + cf_model.user_recommendations( + user, + queryset, + cf_model.COSINE, + 100, + log_time=log_data["cf"]["cosine"], + ), + cf_time_model.user_recommendations( + user, + queryset, + cf_time_model.COSINE, + 100, + log_time=log_data["cf_time"]["cosine"], + ), + cf_time_model.user_recommendations( + user, + queryset, + cf_time_model.DOT, + 100, + log_time=log_data["cf_time"]["dot"], + ), + hot_problems_recommendations, + ] + ) cf_logger.info(log_data) return q def get_context_data(self, **kwargs): context = super(ProblemFeed, self).get_context_data(**kwargs) - context['page_type'] = 'feed' - context['title'] = self.title - context['feed_type'] = self.feed_type + context["page_type"] = "feed" + context["title"] = self.title + context["feed_type"] = self.feed_type return context def get(self, request, *args, **kwargs): if request.in_contest_mode: - return HttpResponseRedirect(reverse('problem_list')) + return HttpResponseRedirect(reverse("problem_list")) return super(ProblemFeed, self).get(request, *args, **kwargs) class LanguageTemplateAjax(View): def get(self, request, *args, **kwargs): try: - language = get_object_or_404(Language, id=int(request.GET.get('id', 0))) + language = get_object_or_404(Language, id=int(request.GET.get("id", 0))) except ValueError: raise Http404() - return HttpResponse(language.template, content_type='text/plain') + return HttpResponse(language.template, content_type="text/plain") class RandomProblem(ProblemList): @@ -714,48 +932,83 @@ class RandomProblem(ProblemList): queryset = self.get_normal_queryset() count = queryset.count() if not count: - return HttpResponseRedirect('%s%s%s' % (reverse('problem_list'), request.META['QUERY_STRING'] and '?', - request.META['QUERY_STRING'])) + return HttpResponseRedirect( + "%s%s%s" + % ( + reverse("problem_list"), + request.META["QUERY_STRING"] and "?", + request.META["QUERY_STRING"], + ) + ) return HttpResponseRedirect(queryset[randrange(count)].get_absolute_url()) -user_logger = logging.getLogger('judge.user') +user_logger = logging.getLogger("judge.user") @login_required def problem_submit(request, problem, submission=None): - if submission is not None and not request.user.has_perm('judge.resubmit_other') and \ - get_object_or_404(Submission, id=int(submission)).user.user != request.user: + if ( + submission is not None + and not request.user.has_perm("judge.resubmit_other") + and get_object_or_404(Submission, id=int(submission)).user.user != request.user + ): raise PermissionDenied() profile = request.profile problem = get_object_or_404(Problem, code=problem) if not problem.is_accessible_by(request.user): - if request.method == 'POST': - user_logger.info('Naughty user %s wants to submit to %s without permission', - request.user.username, problem.code) - return HttpResponseForbidden('

Not allowed to submit. Try later.

') + if request.method == "POST": + user_logger.info( + "Naughty user %s wants to submit to %s without permission", + request.user.username, + problem.code, + ) + return HttpResponseForbidden("

Not allowed to submit. Try later.

") raise Http404() if problem.is_editable_by(request.user): - judge_choices = tuple(Judge.objects.filter(online=True, problems=problem).values_list('name', 'name')) + judge_choices = tuple( + Judge.objects.filter(online=True, problems=problem).values_list( + "name", "name" + ) + ) else: judge_choices = () - if request.method == 'POST': - form = ProblemSubmitForm(request.POST, judge_choices=judge_choices, - instance=Submission(user=profile, problem=problem)) + if request.method == "POST": + form = ProblemSubmitForm( + request.POST, + judge_choices=judge_choices, + instance=Submission(user=profile, problem=problem), + ) if form.is_valid(): - if (not request.user.has_perm('judge.spam_submission') and - Submission.objects.filter(user=profile, was_rejudged=False) - .exclude(status__in=['D', 'IE', 'CE', 'AB']).count() >= settings.DMOJ_SUBMISSION_LIMIT): - return HttpResponse('

You submitted too many submissions.

', status=429) - if not problem.allowed_languages.filter(id=form.cleaned_data['language'].id).exists(): + if ( + not request.user.has_perm("judge.spam_submission") + and Submission.objects.filter(user=profile, was_rejudged=False) + .exclude(status__in=["D", "IE", "CE", "AB"]) + .count() + >= settings.DMOJ_SUBMISSION_LIMIT + ): + return HttpResponse( + "

You submitted too many submissions.

", status=429 + ) + if not problem.allowed_languages.filter( + id=form.cleaned_data["language"].id + ).exists(): raise PermissionDenied() - if not request.user.is_superuser and problem.banned_users.filter(id=profile.id).exists(): - return generic_message(request, _('Banned from submitting'), - _('You have been declared persona non grata for this problem. ' - 'You are permanently barred from submitting this problem.')) + if ( + not request.user.is_superuser + and problem.banned_users.filter(id=profile.id).exists() + ): + return generic_message( + request, + _("Banned from submitting"), + _( + "You have been declared persona non grata for this problem. " + "You are permanently barred from submitting this problem." + ), + ) with transaction.atomic(): if profile.current_contest is not None: @@ -766,51 +1019,72 @@ def problem_submit(request, problem, submission=None): model = form.save() else: max_subs = contest_problem.max_submissions - if max_subs and get_contest_submission_count(problem, profile, - profile.current_contest.virtual) >= max_subs: - return generic_message(request, _('Too many submissions'), - _('You have exceeded the submission limit for this problem.')) + if ( + max_subs + and get_contest_submission_count( + problem, profile, profile.current_contest.virtual + ) + >= max_subs + ): + return generic_message( + request, + _("Too many submissions"), + _( + "You have exceeded the submission limit for this problem." + ), + ) model = form.save() model.contest_object_id = contest_id - contest = ContestSubmission(submission=model, problem=contest_problem, - participation=profile.current_contest) + contest = ContestSubmission( + submission=model, + problem=contest_problem, + participation=profile.current_contest, + ) contest.save() else: model = form.save() # Create the SubmissionSource object - source = SubmissionSource(submission=model, source=form.cleaned_data['source']) + source = SubmissionSource( + submission=model, source=form.cleaned_data["source"] + ) source.save() profile.update_contest() # Save a query model.source = source - model.judge(rejudge=False, judge_id=form.cleaned_data['judge']) + model.judge(rejudge=False, judge_id=form.cleaned_data["judge"]) - return HttpResponseRedirect(reverse('submission_status', args=[str(model.id)])) + return HttpResponseRedirect( + reverse("submission_status", args=[str(model.id)]) + ) else: form_data = form.cleaned_data if submission is not None: sub = get_object_or_404(Submission, id=int(submission)) else: - initial = {'language': profile.language} + initial = {"language": profile.language} if submission is not None: try: - sub = get_object_or_404(Submission.objects.select_related('source', 'language'), id=int(submission)) - initial['source'] = sub.source.source - initial['language'] = sub.language + sub = get_object_or_404( + Submission.objects.select_related("source", "language"), + id=int(submission), + ) + initial["source"] = sub.source.source + initial["language"] = sub.language except ValueError: raise Http404() form = ProblemSubmitForm(judge_choices=judge_choices, initial=initial) form_data = initial - form.fields['language'].queryset = ( - problem.usable_languages.order_by('name', 'key') - .prefetch_related(Prefetch('runtimeversion_set', RuntimeVersion.objects.order_by('priority'))) + form.fields["language"].queryset = problem.usable_languages.order_by( + "name", "key" + ).prefetch_related( + Prefetch("runtimeversion_set", RuntimeVersion.objects.order_by("priority")) ) - if 'language' in form_data: - form.fields['source'].widget.mode = form_data['language'].ace - form.fields['source'].widget.theme = profile.ace_theme + if "language" in form_data: + form.fields["source"].widget.mode = form_data["language"].ace + form.fields["source"].widget.theme = profile.ace_theme if submission is not None: default_lang = sub.language @@ -820,37 +1094,52 @@ def problem_submit(request, problem, submission=None): submission_limit = submissions_left = None if profile.current_contest is not None: try: - submission_limit = problem.contests.get(contest=profile.current_contest.contest).max_submissions + submission_limit = problem.contests.get( + contest=profile.current_contest.contest + ).max_submissions except ContestProblem.DoesNotExist: pass else: if submission_limit: - submissions_left = submission_limit - get_contest_submission_count(problem, profile, - profile.current_contest.virtual) - return render(request, 'problem/submit.html', { - 'form': form, - 'title': _('Submit to %(problem)s') % { - 'problem': problem.translated_name(request.LANGUAGE_CODE), + submissions_left = submission_limit - get_contest_submission_count( + problem, profile, profile.current_contest.virtual + ) + return render( + request, + "problem/submit.html", + { + "form": form, + "title": _("Submit to %(problem)s") + % { + "problem": problem.translated_name(request.LANGUAGE_CODE), + }, + "content_title": mark_safe( + escape(_("Submit to %(problem)s")) + % { + "problem": format_html( + '{1}', + reverse("problem_detail", args=[problem.code]), + problem.translated_name(request.LANGUAGE_CODE), + ), + } + ), + "langs": Language.objects.all(), + "no_judges": not form.fields["language"].queryset, + "submission_limit": submission_limit, + "submissions_left": submissions_left, + "ACE_URL": settings.ACE_URL, + "default_lang": default_lang, }, - 'content_title': mark_safe(escape(_('Submit to %(problem)s')) % { - 'problem': format_html('{1}', - reverse('problem_detail', args=[problem.code]), - problem.translated_name(request.LANGUAGE_CODE)), - }), - 'langs': Language.objects.all(), - 'no_judges': not form.fields['language'].queryset, - 'submission_limit': submission_limit, - 'submissions_left': submissions_left, - 'ACE_URL': settings.ACE_URL, - 'default_lang': default_lang, - }) + ) -class ProblemClone(ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView): - title = _('Clone Problem') - template_name = 'problem/clone.html' +class ProblemClone( + ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView +): + title = _("Clone Problem") + template_name = "problem/clone.html" form_class = ProblemCloneForm - permission_required = 'judge.clone_problem' + permission_required = "judge.clone_problem" def form_valid(self, form): problem = self.object @@ -862,11 +1151,13 @@ class ProblemClone(ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObje problem.is_public = False problem.ac_rate = 0 problem.user_count = 0 - problem.code = form.cleaned_data['code'] + problem.code = form.cleaned_data["code"] problem.save() problem.authors.add(self.request.profile) problem.allowed_languages.set(languages) problem.language_limits.set(language_limits) problem.types.set(types) - return HttpResponseRedirect(reverse('admin:judge_problem_change', args=(problem.id,))) \ No newline at end of file + return HttpResponseRedirect( + reverse("admin:judge_problem_change", args=(problem.id,)) + ) diff --git a/judge/views/problem_data.py b/judge/views/problem_data.py index eb38b84..0f0b38a 100644 --- a/judge/views/problem_data.py +++ b/judge/views/problem_data.py @@ -18,7 +18,15 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.core.files import File from django.core.exceptions import ValidationError -from django.forms import BaseModelFormSet, HiddenInput, ModelForm, NumberInput, Select, formset_factory, FileInput +from django.forms import ( + BaseModelFormSet, + HiddenInput, + ModelForm, + NumberInput, + Select, + formset_factory, + FileInput, +) from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render from django.urls import reverse @@ -28,46 +36,65 @@ from django.utils.translation import gettext as _ from django.views.generic import DetailView from judge.highlight_code import highlight_code -from judge.models import Problem, ProblemData, ProblemTestCase, Submission, problem_data_storage +from judge.models import ( + Problem, + ProblemData, + ProblemTestCase, + Submission, + problem_data_storage, +) from judge.utils.problem_data import ProblemDataCompiler from judge.utils.unicode import utf8text from judge.utils.views import TitleMixin -from judge.utils.fine_uploader import combine_chunks, save_upload, handle_upload, FineUploadFileInput, FineUploadForm +from judge.utils.fine_uploader import ( + combine_chunks, + save_upload, + handle_upload, + FineUploadFileInput, + FineUploadForm, +) from judge.views.problem import ProblemMixin mimetypes.init() -mimetypes.add_type('application/x-yaml', '.yml') +mimetypes.add_type("application/x-yaml", ".yml") def checker_args_cleaner(self): - data = self.cleaned_data['checker_args'] + data = self.cleaned_data["checker_args"] if not data or data.isspace(): - return '' + return "" try: if not isinstance(json.loads(data), dict): - raise ValidationError(_('Checker arguments must be a JSON object')) + raise ValidationError(_("Checker arguments must be a JSON object")) except ValueError: - raise ValidationError(_('Checker arguments is invalid JSON')) + raise ValidationError(_("Checker arguments is invalid JSON")) return data class ProblemDataForm(ModelForm): def clean_zipfile(self): - if hasattr(self, 'zip_valid') and not self.zip_valid: - raise ValidationError(_('Your zip file is invalid!')) - return self.cleaned_data['zipfile'] + if hasattr(self, "zip_valid") and not self.zip_valid: + raise ValidationError(_("Your zip file is invalid!")) + return self.cleaned_data["zipfile"] clean_checker_args = checker_args_cleaner class Meta: model = ProblemData - fields = ['zipfile', 'checker', 'checker_args', 'custom_checker', 'custom_validator', 'interactive_judge'] + fields = [ + "zipfile", + "checker", + "checker_args", + "custom_checker", + "custom_validator", + "interactive_judge", + ] widgets = { - 'zipfile': FineUploadFileInput, - 'checker_args': HiddenInput, - 'generator': HiddenInput, - 'output_limit': HiddenInput, - 'output_prefix': HiddenInput, + "zipfile": FineUploadFileInput, + "checker_args": HiddenInput, + "generator": HiddenInput, + "output_limit": HiddenInput, + "output_prefix": HiddenInput, } @@ -76,25 +103,35 @@ class ProblemCaseForm(ModelForm): class Meta: model = ProblemTestCase - fields = ('order', 'type', 'input_file', 'output_file', 'points', - 'is_pretest', 'checker', 'checker_args') #, 'output_limit', 'output_prefix', 'generator_args') + fields = ( + "order", + "type", + "input_file", + "output_file", + "points", + "is_pretest", + "checker", + "checker_args", + ) # , 'output_limit', 'output_prefix', 'generator_args') widgets = { # 'generator_args': HiddenInput, - 'type': Select(attrs={'style': 'width: 100%'}), - 'points': NumberInput(attrs={'style': 'width: 4em'}), + "type": Select(attrs={"style": "width: 100%"}), + "points": NumberInput(attrs={"style": "width: 4em"}), # 'output_prefix': NumberInput(attrs={'style': 'width: 4.5em'}), # 'output_limit': NumberInput(attrs={'style': 'width: 6em'}), # 'checker_args': HiddenInput, } - -class ProblemCaseFormSet(formset_factory(ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1, - can_delete=True)): +class ProblemCaseFormSet( + formset_factory( + ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1, can_delete=True + ) +): model = ProblemTestCase def __init__(self, *args, **kwargs): - self.valid_files = kwargs.pop('valid_files', None) + self.valid_files = kwargs.pop("valid_files", None) super(ProblemCaseFormSet, self).__init__(*args, **kwargs) def _construct_form(self, i, **kwargs): @@ -114,14 +151,17 @@ class ProblemManagerMixin(LoginRequiredMixin, ProblemMixin, DetailView): class ProblemSubmissionDiff(TitleMixin, ProblemMixin, DetailView): - template_name = 'problem/submission-diff.html' + template_name = "problem/submission-diff.html" def get_title(self): - return _('Comparing submissions for {0}').format(self.object.name) + return _("Comparing submissions for {0}").format(self.object.name) def get_content_title(self): - return format_html(_('Comparing submissions for {0}'), self.object.name, - reverse('problem_detail', args=[self.object.code])) + return format_html( + _('Comparing submissions for {0}'), + self.object.name, + reverse("problem_detail", args=[self.object.code]), + ) def get_object(self, queryset=None): problem = super(ProblemSubmissionDiff, self).get_object(queryset) @@ -132,51 +172,67 @@ class ProblemSubmissionDiff(TitleMixin, ProblemMixin, DetailView): def get_context_data(self, **kwargs): context = super(ProblemSubmissionDiff, self).get_context_data(**kwargs) try: - ids = self.request.GET.getlist('id') + ids = self.request.GET.getlist("id") subs = Submission.objects.filter(id__in=ids) except ValueError: raise Http404 if not subs: raise Http404 - context['submissions'] = subs + context["submissions"] = subs # If we have associated data we can do better than just guess - data = ProblemTestCase.objects.filter(dataset=self.object, type='C') + data = ProblemTestCase.objects.filter(dataset=self.object, type="C") if data: num_cases = data.count() else: num_cases = subs.first().test_cases.count() - context['num_cases'] = num_cases + context["num_cases"] = num_cases return context class ProblemDataView(TitleMixin, ProblemManagerMixin): - template_name = 'problem/data.html' + template_name = "problem/data.html" def get_title(self): - return _('Editing data for {0}').format(self.object.name) + return _("Editing data for {0}").format(self.object.name) def get_content_title(self): - return mark_safe(escape(_('Editing data for %s')) % ( - format_html('{0}', self.object.name, - reverse('problem_detail', args=[self.object.code])))) + return mark_safe( + escape(_("Editing data for %s")) + % ( + format_html( + '{0}', + self.object.name, + reverse("problem_detail", args=[self.object.code]), + ) + ) + ) def get_data_form(self, post=False): - return ProblemDataForm(data=self.request.POST if post else None, prefix='problem-data', - files=self.request.FILES if post else None, - instance=ProblemData.objects.get_or_create(problem=self.object)[0]) + return ProblemDataForm( + data=self.request.POST if post else None, + prefix="problem-data", + files=self.request.FILES if post else None, + instance=ProblemData.objects.get_or_create(problem=self.object)[0], + ) def get_case_formset(self, files, post=False): - return ProblemCaseFormSet(data=self.request.POST if post else None, prefix='cases', valid_files=files, - queryset=ProblemTestCase.objects.filter(dataset_id=self.object.pk).order_by('order')) + return ProblemCaseFormSet( + data=self.request.POST if post else None, + prefix="cases", + valid_files=files, + queryset=ProblemTestCase.objects.filter(dataset_id=self.object.pk).order_by( + "order" + ), + ) def get_valid_files(self, data, post=False): try: - if post and 'problem-data-zipfile-clear' in self.request.POST: + if post and "problem-data-zipfile-clear" in self.request.POST: return [] - elif post and 'problem-data-zipfile' in self.request.FILES: - return ZipFile(self.request.FILES['problem-data-zipfile']).namelist() + elif post and "problem-data-zipfile" in self.request.FILES: + return ZipFile(self.request.FILES["problem-data-zipfile"]).namelist() elif data.zipfile: return ZipFile(data.zipfile.path).namelist() except BadZipfile: @@ -185,14 +241,18 @@ class ProblemDataView(TitleMixin, ProblemManagerMixin): def get_context_data(self, **kwargs): context = super(ProblemDataView, self).get_context_data(**kwargs) - if 'data_form' not in context: - context['data_form'] = self.get_data_form() - valid_files = context['valid_files'] = self.get_valid_files(context['data_form'].instance) - context['data_form'].zip_valid = valid_files is not False - context['cases_formset'] = self.get_case_formset(valid_files) - context['valid_files_json'] = mark_safe(json.dumps(context['valid_files'])) - context['valid_files'] = set(context['valid_files']) - context['all_case_forms'] = chain(context['cases_formset'], [context['cases_formset'].empty_form]) + if "data_form" not in context: + context["data_form"] = self.get_data_form() + valid_files = context["valid_files"] = self.get_valid_files( + context["data_form"].instance + ) + context["data_form"].zip_valid = valid_files is not False + context["cases_formset"] = self.get_case_formset(valid_files) + context["valid_files_json"] = mark_safe(json.dumps(context["valid_files"])) + context["valid_files"] = set(context["valid_files"]) + context["all_case_forms"] = chain( + context["cases_formset"], [context["cases_formset"].empty_form] + ) return context def post(self, request, *args, **kwargs): @@ -208,10 +268,17 @@ class ProblemDataView(TitleMixin, ProblemManagerMixin): case.save() for case in cases_formset.deleted_objects: case.delete() - ProblemDataCompiler.generate(problem, data, problem.cases.order_by('order'), valid_files) + ProblemDataCompiler.generate( + problem, data, problem.cases.order_by("order"), valid_files + ) return HttpResponseRedirect(request.get_full_path()) - return self.render_to_response(self.get_context_data(data_form=data_form, cases_formset=cases_formset, - valid_files=valid_files)) + return self.render_to_response( + self.get_context_data( + data_form=data_form, + cases_formset=cases_formset, + valid_files=valid_files, + ) + ) put = post @@ -223,16 +290,22 @@ def problem_data_file(request, problem, path): raise Http404() response = HttpResponse() - if hasattr(settings, 'DMOJ_PROBLEM_DATA_INTERNAL') and request.META.get('SERVER_SOFTWARE', '').startswith('nginx/'): - response['X-Accel-Redirect'] = '%s/%s/%s' % (settings.DMOJ_PROBLEM_DATA_INTERNAL, problem, path) + if hasattr(settings, "DMOJ_PROBLEM_DATA_INTERNAL") and request.META.get( + "SERVER_SOFTWARE", "" + ).startswith("nginx/"): + response["X-Accel-Redirect"] = "%s/%s/%s" % ( + settings.DMOJ_PROBLEM_DATA_INTERNAL, + problem, + path, + ) else: try: - with problem_data_storage.open(os.path.join(problem, path), 'rb') as f: + with problem_data_storage.open(os.path.join(problem, path), "rb") as f: response.content = f.read() except IOError: raise Http404() - response['Content-Type'] = 'application/octet-stream' + response["Content-Type"] = "application/octet-stream" return response @@ -243,39 +316,53 @@ def problem_init_view(request, problem): raise Http404() try: - with problem_data_storage.open(os.path.join(problem.code, 'init.yml'), 'rb') as f: - data = utf8text(f.read()).rstrip('\n') + with problem_data_storage.open( + os.path.join(problem.code, "init.yml"), "rb" + ) as f: + data = utf8text(f.read()).rstrip("\n") except IOError: raise Http404() - return render(request, 'problem/yaml.html', { - 'raw_source': data, 'highlighted_source': highlight_code(data, 'yaml', linenos=False), - 'title': _('Generated init.yml for %s') % problem.name, - 'content_title': mark_safe(escape(_('Generated init.yml for %s')) % ( - format_html('{0}', problem.name, - reverse('problem_detail', args=[problem.code])))), - }) + return render( + request, + "problem/yaml.html", + { + "raw_source": data, + "highlighted_source": highlight_code(data, "yaml", linenos=False), + "title": _("Generated init.yml for %s") % problem.name, + "content_title": mark_safe( + escape(_("Generated init.yml for %s")) + % ( + format_html( + '{0}', + problem.name, + reverse("problem_detail", args=[problem.code]), + ) + ) + ), + }, + ) class ProblemZipUploadView(ProblemManagerMixin, View): def dispatch(self, *args, **kwargs): return super(ProblemZipUploadView, self).dispatch(*args, **kwargs) - + def post(self, request, *args, **kwargs): self.object = problem = self.get_object() problem_data = get_object_or_404(ProblemData, problem=self.object) form = FineUploadForm(request.POST, request.FILES) - + if form.is_valid(): - fileuid = form.cleaned_data['qquuid'] - filename = form.cleaned_data['qqfilename'] + fileuid = form.cleaned_data["qquuid"] + filename = form.cleaned_data["qqfilename"] dest = os.path.join(gettempdir(), fileuid) def post_upload(): zip_dest = os.path.join(dest, filename) try: - ZipFile(zip_dest).namelist() # check if this file is valid - with open(zip_dest, 'rb') as f: + ZipFile(zip_dest).namelist() # check if this file is valid + with open(zip_dest, "rb") as f: problem_data.zipfile.delete() problem_data.zipfile.save(filename, File(f)) f.close() @@ -283,11 +370,16 @@ class ProblemZipUploadView(ProblemManagerMixin, View): raise Exception(e) finally: shutil.rmtree(dest) - + try: - handle_upload(request.FILES['qqfile'], form.cleaned_data, dest, post_upload=post_upload) + handle_upload( + request.FILES["qqfile"], + form.cleaned_data, + dest, + post_upload=post_upload, + ) except Exception as e: - return JsonResponse({'success': False, 'error': str(e)}) - return JsonResponse({'success': True}) + return JsonResponse({"success": False, "error": str(e)}) + return JsonResponse({"success": True}) else: - return HttpResponse(status_code=400) \ No newline at end of file + return HttpResponse(status_code=400) diff --git a/judge/views/problem_manage.py b/judge/views/problem_manage.py index 07e94ec..5dd0aef 100644 --- a/judge/views/problem_manage.py +++ b/judge/views/problem_manage.py @@ -5,7 +5,12 @@ import zipfile, tempfile from celery.result import AsyncResult from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseRedirect, +) from django.urls import reverse from django.utils.html import escape, format_html from django.utils.safestring import mark_safe @@ -46,33 +51,46 @@ class ManageProblemSubmissionActionMixin(ManageProblemSubmissionMixin): class ManageProblemSubmissionView(TitleMixin, ManageProblemSubmissionMixin, DetailView): - template_name = 'problem/manage_submission.html' + template_name = "problem/manage_submission.html" def get_title(self): - return _('Managing submissions for %s') % (self.object.name,) + return _("Managing submissions for %s") % (self.object.name,) def get_content_title(self): - return mark_safe(escape(_('Managing submissions for %s')) % ( - format_html('{0}', self.object.name, - reverse('problem_detail', args=[self.object.code])))) + return mark_safe( + escape(_("Managing submissions for %s")) + % ( + format_html( + '{0}', + self.object.name, + reverse("problem_detail", args=[self.object.code]), + ) + ) + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['submission_count'] = self.object.submission_set.count() - context['languages'] = [(lang_id, short_name or key) for lang_id, key, short_name in - Language.objects.values_list('id', 'key', 'short_name')] - context['results'] = sorted(map(itemgetter(0), Submission.RESULT)) + context["submission_count"] = self.object.submission_set.count() + context["languages"] = [ + (lang_id, short_name or key) + for lang_id, key, short_name in Language.objects.values_list( + "id", "key", "short_name" + ) + ] + context["results"] = sorted(map(itemgetter(0), Submission.RESULT)) return context -class BaseActionSubmissionsView(PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView): - permission_required = 'judge.rejudge_submission_lot' +class BaseActionSubmissionsView( + PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView +): + permission_required = "judge.rejudge_submission_lot" def perform_action(self): - if self.request.POST.get('use_range', 'off') == 'on': + if self.request.POST.get("use_range", "off") == "on": try: - start = int(self.request.POST.get('start')) - end = int(self.request.POST.get('end')) + start = int(self.request.POST.get("start")) + end = int(self.request.POST.get("end")) except (KeyError, ValueError): return HttpResponseBadRequest() id_range = (start, end) @@ -80,11 +98,13 @@ class BaseActionSubmissionsView(PermissionRequiredMixin, ManageProblemSubmission id_range = None try: - languages = list(map(int, self.request.POST.getlist('language'))) + languages = list(map(int, self.request.POST.getlist("language"))) except ValueError: return HttpResponseBadRequest() - return self.generate_response(id_range, languages, self.request.POST.getlist('result')) + return self.generate_response( + id_range, languages, self.request.POST.getlist("result") + ) def generate_response(self, id_range, languages, results): raise NotImplementedError() @@ -92,45 +112,64 @@ class BaseActionSubmissionsView(PermissionRequiredMixin, ManageProblemSubmission class ActionSubmissionsView(BaseActionSubmissionsView): def rejudge_response(self, id_range, languages, results): - status = rejudge_problem_filter.delay(self.object.id, id_range, languages, results) + status = rejudge_problem_filter.delay( + self.object.id, id_range, languages, results + ) return redirect_to_task_status( - status, message=_('Rejudging selected submissions for %s...') % (self.object.name,), - redirect=reverse('problem_submissions_rejudge_success', args=[self.object.code, status.id]), + status, + message=_("Rejudging selected submissions for %s...") % (self.object.name,), + redirect=reverse( + "problem_submissions_rejudge_success", + args=[self.object.code, status.id], + ), ) def download_response(self, id_range, languages, results): if not self.request.user.is_superuser: raise Http404 - + queryset = Submission.objects.filter(problem_id=self.object.id) submissions = apply_submission_filter(queryset, id_range, languages, results) with tempfile.SpooledTemporaryFile() as tmp: - with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as archive: + with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as archive: for submission in submissions: - file_name = str(submission.id) + '_' + str(submission.user) + '.' + str(submission.language.key) + file_name = ( + str(submission.id) + + "_" + + str(submission.user) + + "." + + str(submission.language.key) + ) archive.writestr(file_name, submission.source.source) - + # Reset file pointer tmp.seek(0) # Write file data to response - response = HttpResponse(tmp.read(), content_type='application/x-zip-compressed') - response['Content-Disposition'] = 'attachment; filename="%s"' % (str(self.object.code) + '_submissions.zip') + response = HttpResponse( + tmp.read(), content_type="application/x-zip-compressed" + ) + response["Content-Disposition"] = 'attachment; filename="%s"' % ( + str(self.object.code) + "_submissions.zip" + ) return response def generate_response(self, id_range, languages, results): - action = self.request.POST.get('action') - if (action == 'rejudge'): + action = self.request.POST.get("action") + if action == "rejudge": return self.rejudge_response(id_range, languages, results) - elif (action == 'download'): + elif action == "download": return self.download_response(id_range, languages, results) else: return Http404() + class PreviewActionSubmissionsView(BaseActionSubmissionsView): def generate_response(self, id_range, languages, results): - queryset = apply_submission_filter(self.object.submission_set.all(), id_range, languages, results) + queryset = apply_submission_filter( + self.object.submission_set.all(), id_range, languages, results + ) return HttpResponse(str(queryset.count())) @@ -138,8 +177,12 @@ class RescoreAllSubmissionsView(ManageProblemSubmissionActionMixin, BaseDetailVi def perform_action(self): status = rescore_problem.delay(self.object.id) return redirect_to_task_status( - status, message=_('Rescoring all submissions for %s...') % (self.object.name,), - redirect=reverse('problem_submissions_rescore_success', args=[self.object.code, status.id]), + status, + message=_("Rescoring all submissions for %s...") % (self.object.name,), + redirect=reverse( + "problem_submissions_rescore_success", + args=[self.object.code, status.id], + ), ) @@ -147,15 +190,29 @@ def rejudge_success(request, problem, task_id): count = AsyncResult(task_id).result if not isinstance(count, int): raise Http404() - messages.success(request, ngettext('Successfully scheduled %d submission for rejudging.', - 'Successfully scheduled %d submissions for rejudging.', count) % (count,)) - return HttpResponseRedirect(reverse('problem_manage_submissions', args=[problem])) + messages.success( + request, + ngettext( + "Successfully scheduled %d submission for rejudging.", + "Successfully scheduled %d submissions for rejudging.", + count, + ) + % (count,), + ) + return HttpResponseRedirect(reverse("problem_manage_submissions", args=[problem])) def rescore_success(request, problem, task_id): count = AsyncResult(task_id).result if not isinstance(count, int): raise Http404() - messages.success(request, ngettext('%d submission were successfully rescored.', - '%d submissions were successfully rescored.', count) % (count,)) - return HttpResponseRedirect(reverse('problem_manage_submissions', args=[problem])) + messages.success( + request, + ngettext( + "%d submission were successfully rescored.", + "%d submissions were successfully rescored.", + count, + ) + % (count,), + ) + return HttpResponseRedirect(reverse("problem_manage_submissions", args=[problem])) diff --git a/judge/views/ranked_submission.py b/judge/views/ranked_submission.py index f368d41..8038ac1 100644 --- a/judge/views/ranked_submission.py +++ b/judge/views/ranked_submission.py @@ -7,27 +7,31 @@ from judge.utils.problems import get_result_data from judge.utils.raw_sql import join_sql_subquery from judge.views.submission import ForceContestMixin, ProblemSubmissions -__all__ = ['RankedSubmissions', 'ContestRankedSubmission'] +__all__ = ["RankedSubmissions", "ContestRankedSubmission"] class RankedSubmissions(ProblemSubmissions): - tab = 'best_submissions_list' + tab = "best_submissions_list" dynamic_update = False def get_queryset(self): if self.in_contest: - contest_join = '''INNER JOIN judge_contestsubmission AS cs ON (sub.id = cs.submission_id) - INNER JOIN judge_contestparticipation AS cp ON (cs.participation_id = cp.id)''' - points = 'cs.points' - constraint = 'AND cp.contest_id = %s' + contest_join = """INNER JOIN judge_contestsubmission AS cs ON (sub.id = cs.submission_id) + INNER JOIN judge_contestparticipation AS cp ON (cs.participation_id = cp.id)""" + points = "cs.points" + constraint = "AND cp.contest_id = %s" else: - contest_join = '' - points = 'sub.points' - constraint = '' - queryset = super(RankedSubmissions, self).get_queryset().filter(user__is_unlisted=False) + contest_join = "" + points = "sub.points" + constraint = "" + queryset = ( + super(RankedSubmissions, self) + .get_queryset() + .filter(user__is_unlisted=False) + ) join_sql_subquery( queryset, - subquery=''' + subquery=""" SELECT sub.id AS id FROM ( SELECT sub.user_id AS uid, MAX(sub.points) AS points @@ -44,22 +48,30 @@ class RankedSubmissions(ProblemSubmissions): ON (sub.user_id = fastest.uid AND sub.time = fastest.time) {contest_join} WHERE sub.problem_id = %s AND {points} > 0 {constraint} GROUP BY sub.user_id - '''.format(points=points, contest_join=contest_join, constraint=constraint), - params=[self.problem.id, self.contest.id] * 3 if self.in_contest else [self.problem.id] * 3, - alias='best_subs', join_fields=[('id', 'id')], + """.format( + points=points, contest_join=contest_join, constraint=constraint + ), + params=[self.problem.id, self.contest.id] * 3 + if self.in_contest + else [self.problem.id] * 3, + alias="best_subs", + join_fields=[("id", "id")], ) if self.in_contest: - return queryset.order_by('-contest__points', 'time') + return queryset.order_by("-contest__points", "time") else: - return queryset.order_by('-points', 'time') + return queryset.order_by("-points", "time") def get_title(self): - return _('Best solutions for %s') % self.problem_name + return _("Best solutions for %s") % self.problem_name def get_content_title(self): - return format_html(_('Best solutions for {0}'), self.problem_name, - reverse('problem_detail', args=[self.problem.code])) + return format_html( + _('Best solutions for {0}'), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + ) def _get_result_data(self): return get_result_data(super(RankedSubmissions, self).get_queryset().order_by()) @@ -68,22 +80,35 @@ class RankedSubmissions(ProblemSubmissions): class ContestRankedSubmission(ForceContestMixin, RankedSubmissions): def get_title(self): if self.problem.is_accessible_by(self.request.user): - return _('Best solutions for %(problem)s in %(contest)s') % { - 'problem': self.problem_name, 'contest': self.contest.name, + return _("Best solutions for %(problem)s in %(contest)s") % { + "problem": self.problem_name, + "contest": self.contest.name, } - return _('Best solutions for problem %(number)s in %(contest)s') % { - 'number': self.get_problem_number(self.problem), 'contest': self.contest.name, + return _("Best solutions for problem %(number)s in %(contest)s") % { + "number": self.get_problem_number(self.problem), + "contest": self.contest.name, } def get_content_title(self): if self.problem.is_accessible_by(self.request.user): - return format_html(_('Best solutions for {0} in {2}'), - self.problem_name, reverse('problem_detail', args=[self.problem.code]), - self.contest.name, reverse('contest_view', args=[self.contest.key])) - return format_html(_('Best solutions for problem {0} in {1}'), - self.get_problem_number(self.problem), self.contest.name, - reverse('contest_view', args=[self.contest.key])) + return format_html( + _('Best solutions for {0} in {2}'), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + self.contest.name, + reverse("contest_view", args=[self.contest.key]), + ) + return format_html( + _('Best solutions for problem {0} in {1}'), + self.get_problem_number(self.problem), + self.contest.name, + reverse("contest_view", args=[self.contest.key]), + ) def _get_result_data(self): - return get_result_data(Submission.objects.filter( - problem_id=self.problem.id, contest__participation__contest_id=self.contest.id)) + return get_result_data( + Submission.objects.filter( + problem_id=self.problem.id, + contest__participation__contest_id=self.contest.id, + ) + ) diff --git a/judge/views/register.py b/judge/views/register.py index e7ba8c9..0856f00 100644 --- a/judge/views/register.py +++ b/judge/views/register.py @@ -8,8 +8,10 @@ from django.contrib.auth.password_validation import get_default_password_validat from django.forms import ChoiceField, ModelChoiceField from django.shortcuts import render from django.utils.translation import gettext, gettext_lazy as _ -from registration.backends.default.views import (ActivationView as OldActivationView, - RegistrationView as OldRegistrationView) +from registration.backends.default.views import ( + ActivationView as OldActivationView, + RegistrationView as OldRegistrationView, +) from registration.forms import RegistrationForm from sortedm2m.forms import SortedMultipleChoiceField @@ -18,101 +20,138 @@ from judge.utils.recaptcha import ReCaptchaField, ReCaptchaWidget from judge.utils.subscription import Subscription, newsletter_id from judge.widgets import Select2MultipleWidget, Select2Widget -valid_id = re.compile(r'^\w+$') +valid_id = re.compile(r"^\w+$") bad_mail_regex = list(map(re.compile, settings.BAD_MAIL_PROVIDER_REGEX)) class CustomRegistrationForm(RegistrationForm): - username = forms.RegexField(regex=r'^\w+$', max_length=30, label=_('Username'), - error_messages={'invalid': _('A username must contain letters, ' - 'numbers, or underscores')}) - timezone = ChoiceField(label=_('Timezone'), choices=TIMEZONE, - widget=Select2Widget(attrs={'style': 'width:100%'})) - language = ModelChoiceField(queryset=Language.objects.all(), label=_('Preferred language'), empty_label=None, - widget=Select2Widget(attrs={'style': 'width:100%'})) - organizations = SortedMultipleChoiceField(queryset=Organization.objects.filter(is_open=True), - label=_('Organizations'), required=False, - widget=Select2MultipleWidget(attrs={'style': 'width:100%'})) + username = forms.RegexField( + regex=r"^\w+$", + max_length=30, + label=_("Username"), + error_messages={ + "invalid": _("A username must contain letters, " "numbers, or underscores") + }, + ) + timezone = ChoiceField( + label=_("Timezone"), + choices=TIMEZONE, + widget=Select2Widget(attrs={"style": "width:100%"}), + ) + language = ModelChoiceField( + queryset=Language.objects.all(), + label=_("Preferred language"), + empty_label=None, + widget=Select2Widget(attrs={"style": "width:100%"}), + ) + organizations = SortedMultipleChoiceField( + queryset=Organization.objects.filter(is_open=True), + label=_("Organizations"), + required=False, + widget=Select2MultipleWidget(attrs={"style": "width:100%"}), + ) if newsletter_id is not None: - newsletter = forms.BooleanField(label=_('Subscribe to newsletter?'), initial=True, required=False) + newsletter = forms.BooleanField( + label=_("Subscribe to newsletter?"), initial=True, required=False + ) if ReCaptchaField is not None: captcha = ReCaptchaField(widget=ReCaptchaWidget()) def clean_organizations(self): - organizations = self.cleaned_data.get('organizations') or [] + organizations = self.cleaned_data.get("organizations") or [] max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT if sum(org.is_open for org in organizations) > max_orgs: raise forms.ValidationError( - _('You may not be part of more than {count} public organizations.').format(count=max_orgs)) + _( + "You may not be part of more than {count} public organizations." + ).format(count=max_orgs) + ) - return self.cleaned_data['organizations'] + return self.cleaned_data["organizations"] def clean_email(self): - if User.objects.filter(email=self.cleaned_data['email']).exists(): - raise forms.ValidationError(gettext('The email address "%s" is already taken. Only one registration ' - 'is allowed per address.') % self.cleaned_data['email']) - if '@' in self.cleaned_data['email']: - domain = self.cleaned_data['email'].split('@')[-1].lower() - if (domain in settings.BAD_MAIL_PROVIDERS or - any(regex.match(domain) for regex in bad_mail_regex)): - raise forms.ValidationError(gettext('Your email provider is not allowed due to history of abuse. ' - 'Please use a reputable email provider.')) - return self.cleaned_data['email'] + if User.objects.filter(email=self.cleaned_data["email"]).exists(): + raise forms.ValidationError( + gettext( + 'The email address "%s" is already taken. Only one registration ' + "is allowed per address." + ) + % self.cleaned_data["email"] + ) + if "@" in self.cleaned_data["email"]: + domain = self.cleaned_data["email"].split("@")[-1].lower() + if domain in settings.BAD_MAIL_PROVIDERS or any( + regex.match(domain) for regex in bad_mail_regex + ): + raise forms.ValidationError( + gettext( + "Your email provider is not allowed due to history of abuse. " + "Please use a reputable email provider." + ) + ) + return self.cleaned_data["email"] class RegistrationView(OldRegistrationView): - title = _('Registration') + title = _("Registration") form_class = CustomRegistrationForm - template_name = 'registration/registration_form.html' + template_name = "registration/registration_form.html" def get_context_data(self, **kwargs): - if 'title' not in kwargs: - kwargs['title'] = self.title + if "title" not in kwargs: + kwargs["title"] = self.title tzmap = settings.TIMEZONE_MAP - kwargs['TIMEZONE_MAP'] = tzmap or 'http://momentjs.com/static/img/world.png' - kwargs['TIMEZONE_BG'] = settings.TIMEZONE_BG if tzmap else '#4E7CAD' - kwargs['password_validators'] = get_default_password_validators() - kwargs['tos_url'] = settings.TERMS_OF_SERVICE_URL + kwargs["TIMEZONE_MAP"] = tzmap or "http://momentjs.com/static/img/world.png" + kwargs["TIMEZONE_BG"] = settings.TIMEZONE_BG if tzmap else "#4E7CAD" + kwargs["password_validators"] = get_default_password_validators() + kwargs["tos_url"] = settings.TERMS_OF_SERVICE_URL return super(RegistrationView, self).get_context_data(**kwargs) def register(self, form): user = super(RegistrationView, self).register(form) - profile, _ = Profile.objects.get_or_create(user=user, defaults={ - 'language': Language.get_default_language(), - }) + profile, _ = Profile.objects.get_or_create( + user=user, + defaults={ + "language": Language.get_default_language(), + }, + ) cleaned_data = form.cleaned_data - profile.timezone = cleaned_data['timezone'] - profile.language = cleaned_data['language'] - profile.organizations.add(*cleaned_data['organizations']) + profile.timezone = cleaned_data["timezone"] + profile.language = cleaned_data["language"] + profile.organizations.add(*cleaned_data["organizations"]) profile.save() - if newsletter_id is not None and cleaned_data['newsletter']: + if newsletter_id is not None and cleaned_data["newsletter"]: Subscription(user=user, newsletter_id=newsletter_id, subscribed=True).save() return user def get_initial(self, *args, **kwargs): initial = super(RegistrationView, self).get_initial(*args, **kwargs) - initial['timezone'] = settings.DEFAULT_USER_TIME_ZONE - initial['language'] = Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE) + initial["timezone"] = settings.DEFAULT_USER_TIME_ZONE + initial["language"] = Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE) return initial class ActivationView(OldActivationView): - title = _('Registration') - template_name = 'registration/activate.html' + title = _("Registration") + template_name = "registration/activate.html" def get_context_data(self, **kwargs): - if 'title' not in kwargs: - kwargs['title'] = self.title + if "title" not in kwargs: + kwargs["title"] = self.title return super(ActivationView, self).get_context_data(**kwargs) def social_auth_error(request): - return render(request, 'generic-message.html', { - 'title': gettext('Authentication failure'), - 'message': request.GET.get('message'), - }) + return render( + request, + "generic-message.html", + { + "title": gettext("Authentication failure"), + "message": request.GET.get("message"), + }, + ) diff --git a/judge/views/select2.py b/judge/views/select2.py index 1a0bece..6068c5c 100644 --- a/judge/views/select2.py +++ b/judge/views/select2.py @@ -12,7 +12,7 @@ from judge.models import Comment, Contest, Organization, Problem, Profile def _get_user_queryset(term): qs = Profile.objects - if term.endswith(' '): + if term.endswith(" "): qs = qs.filter(user__username=term.strip()) else: qs = qs.filter(user__username__icontains=term) @@ -24,18 +24,22 @@ class Select2View(BaseListView): def get(self, request, *args, **kwargs): self.request = request - self.term = kwargs.get('term', request.GET.get('term', '')) + self.term = kwargs.get("term", request.GET.get("term", "")) self.object_list = self.get_queryset() context = self.get_context_data() - return JsonResponse({ - 'results': [ - { - 'text': smart_text(self.get_name(obj)), - 'id': obj.pk, - } for obj in context['object_list']], - 'more': context['page_obj'].has_next(), - }) + return JsonResponse( + { + "results": [ + { + "text": smart_text(self.get_name(obj)), + "id": obj.pk, + } + for obj in context["object_list"] + ], + "more": context["page_obj"].has_next(), + } + ) def get_name(self, obj): return str(obj) @@ -43,7 +47,11 @@ class Select2View(BaseListView): class UserSelect2View(Select2View): def get_queryset(self): - return _get_user_queryset(self.term).annotate(username=F('user__username')).only('id') + return ( + _get_user_queryset(self.term) + .annotate(username=F("user__username")) + .only("id") + ) def get_name(self, obj): return obj.username @@ -56,14 +64,18 @@ class OrganizationSelect2View(Select2View): class ProblemSelect2View(Select2View): def get_queryset(self): - return Problem.get_visible_problems(self.request.user) \ - .filter(Q(code__icontains=self.term) | Q(name__icontains=self.term)).distinct() + return ( + Problem.get_visible_problems(self.request.user) + .filter(Q(code__icontains=self.term) | Q(name__icontains=self.term)) + .distinct() + ) class ContestSelect2View(Select2View): def get_queryset(self): - return Contest.get_visible_contests(self.request.user) \ - .filter(Q(key__icontains=self.term) | Q(name__icontains=self.term)) + return Contest.get_visible_contests(self.request.user).filter( + Q(key__icontains=self.term) | Q(name__icontains=self.term) + ) class CommentSelect2View(Select2View): @@ -80,24 +92,32 @@ class UserSearchSelect2View(BaseListView): def get(self, request, *args, **kwargs): self.request = request self.kwargs = kwargs - self.term = kwargs.get('term', request.GET.get('term', '')) - self.gravatar_size = request.GET.get('gravatar_size', 128) - self.gravatar_default = request.GET.get('gravatar_default', None) + self.term = kwargs.get("term", request.GET.get("term", "")) + self.gravatar_size = request.GET.get("gravatar_size", 128) + self.gravatar_default = request.GET.get("gravatar_default", None) - self.object_list = self.get_queryset().values_list('pk', 'user__username', 'user__email', 'display_rank') + self.object_list = self.get_queryset().values_list( + "pk", "user__username", "user__email", "display_rank" + ) context = self.get_context_data() - return JsonResponse({ - 'results': [ - { - 'text': username, - 'id': username, - 'gravatar_url': gravatar(email, self.gravatar_size, self.gravatar_default), - 'display_rank': display_rank, - } for pk, username, email, display_rank in context['object_list']], - 'more': context['page_obj'].has_next(), - }) + return JsonResponse( + { + "results": [ + { + "text": username, + "id": username, + "gravatar_url": gravatar( + email, self.gravatar_size, self.gravatar_default + ), + "display_rank": display_rank, + } + for pk, username, email, display_rank in context["object_list"] + ], + "more": context["page_obj"].has_next(), + } + ) def get_name(self, obj): return str(obj) @@ -105,53 +125,66 @@ class UserSearchSelect2View(BaseListView): class ContestUserSearchSelect2View(UserSearchSelect2View): def get_queryset(self): - contest = get_object_or_404(Contest, key=self.kwargs['contest']) - if not contest.is_accessible_by(self.request.user) or not contest.can_see_full_scoreboard(self.request.user): + contest = get_object_or_404(Contest, key=self.kwargs["contest"]) + if not contest.is_accessible_by( + self.request.user + ) or not contest.can_see_full_scoreboard(self.request.user): raise Http404() - return Profile.objects.filter(contest_history__contest=contest, - user__username__icontains=self.term).distinct() + return Profile.objects.filter( + contest_history__contest=contest, user__username__icontains=self.term + ).distinct() class TicketUserSelect2View(UserSearchSelect2View): def get_queryset(self): - return Profile.objects.filter(tickets__isnull=False, - user__username__icontains=self.term).distinct() + return Profile.objects.filter( + tickets__isnull=False, user__username__icontains=self.term + ).distinct() class AssigneeSelect2View(UserSearchSelect2View): def get_queryset(self): - return Profile.objects.filter(assigned_tickets__isnull=False, - user__username__icontains=self.term).distinct() + return Profile.objects.filter( + assigned_tickets__isnull=False, user__username__icontains=self.term + ).distinct() class ChatUserSearchSelect2View(BaseListView): paginate_by = 20 - def get_queryset(self): # TODO: add block + def get_queryset(self): # TODO: add block return _get_user_queryset(self.term) def get(self, request, *args, **kwargs): self.request = request self.kwargs = kwargs - self.term = kwargs.get('term', request.GET.get('term', '')) - self.gravatar_size = request.GET.get('gravatar_size', 128) - self.gravatar_default = request.GET.get('gravatar_default', None) + self.term = kwargs.get("term", request.GET.get("term", "")) + self.gravatar_size = request.GET.get("gravatar_size", 128) + self.gravatar_default = request.GET.get("gravatar_default", None) - self.object_list = self.get_queryset().values_list('pk', 'user__username', 'user__email', 'display_rank') + self.object_list = self.get_queryset().values_list( + "pk", "user__username", "user__email", "display_rank" + ) context = self.get_context_data() - return JsonResponse({ - 'results': [ - { - 'text': username, - 'id': encrypt_url(request.profile.id, pk), - 'gravatar_url': gravatar(email, self.gravatar_size, self.gravatar_default), - 'display_rank': display_rank, - } for pk, username, email, display_rank in context['object_list']], - 'more': context['page_obj'].has_next(), - }) + return JsonResponse( + { + "results": [ + { + "text": username, + "id": encrypt_url(request.profile.id, pk), + "gravatar_url": gravatar( + email, self.gravatar_size, self.gravatar_default + ), + "display_rank": display_rank, + } + for pk, username, email, display_rank in context["object_list"] + ], + "more": context["page_obj"].has_next(), + } + ) def get_name(self, obj): - return str(obj) \ No newline at end of file + return str(obj) diff --git a/judge/views/stats.py b/judge/views/stats.py index 41a852b..e8c74ef 100644 --- a/judge/views/stats.py +++ b/judge/views/stats.py @@ -9,31 +9,50 @@ from django.shortcuts import render from django.utils.translation import gettext as _ from judge.models import Language, Submission -from judge.utils.stats import chart_colors, get_bar_chart, get_pie_chart, highlight_colors +from judge.utils.stats import ( + chart_colors, + get_bar_chart, + get_pie_chart, + highlight_colors, +) -ac_count = Count(Case(When(submission__result='AC', then=Value(1)), output_field=IntegerField())) +ac_count = Count( + Case(When(submission__result="AC", then=Value(1)), output_field=IntegerField()) +) def repeat_chain(iterable): return chain.from_iterable(repeat(iterable)) -def language_data(request, language_count=Language.objects.annotate(count=Count('submission'))): - languages = language_count.filter(count__gt=0).values('key', 'name', 'count').order_by('-count') +def language_data( + request, language_count=Language.objects.annotate(count=Count("submission")) +): + languages = ( + language_count.filter(count__gt=0) + .values("key", "name", "count") + .order_by("-count") + ) num_languages = min(len(languages), settings.DMOJ_STATS_LANGUAGE_THRESHOLD) - other_count = sum(map(itemgetter('count'), languages[num_languages:])) + other_count = sum(map(itemgetter("count"), languages[num_languages:])) - return JsonResponse({ - 'labels': list(map(itemgetter('name'), languages[:num_languages])) + ['Other'], - 'datasets': [ - { - 'backgroundColor': chart_colors[:num_languages] + ['#FDB45C'], - 'highlightBackgroundColor': highlight_colors[:num_languages] + ['#FFC870'], - 'data': list(map(itemgetter('count'), languages[:num_languages])) + [other_count], - }, - ], - }, safe=False) + return JsonResponse( + { + "labels": list(map(itemgetter("name"), languages[:num_languages])) + + ["Other"], + "datasets": [ + { + "backgroundColor": chart_colors[:num_languages] + ["#FDB45C"], + "highlightBackgroundColor": highlight_colors[:num_languages] + + ["#FFC870"], + "data": list(map(itemgetter("count"), languages[:num_languages])) + + [other_count], + }, + ], + }, + safe=False, + ) def ac_language_data(request): @@ -42,27 +61,42 @@ def ac_language_data(request): def status_data(request, statuses=None): if not statuses: - statuses = (Submission.objects.values('result').annotate(count=Count('result')) - .values('result', 'count').order_by('-count')) + statuses = ( + Submission.objects.values("result") + .annotate(count=Count("result")) + .values("result", "count") + .order_by("-count") + ) data = [] for status in statuses: - res = status['result'] + res = status["result"] if not res: continue - count = status['count'] + count = status["count"] data.append((str(Submission.USER_DISPLAY_CODES[res]), count)) return JsonResponse(get_pie_chart(data), safe=False) def ac_rate(request): - rate = CombinedExpression(ac_count / Count('submission'), '*', Value(100.0), output_field=FloatField()) - data = Language.objects.annotate(total=Count('submission'), ac_rate=rate).filter(total__gt=0) \ - .order_by('total').values_list('name', 'ac_rate') + rate = CombinedExpression( + ac_count / Count("submission"), "*", Value(100.0), output_field=FloatField() + ) + data = ( + Language.objects.annotate(total=Count("submission"), ac_rate=rate) + .filter(total__gt=0) + .order_by("total") + .values_list("name", "ac_rate") + ) return JsonResponse(get_bar_chart(list(data))) def language(request): - return render(request, 'stats/language.html', { - 'title': _('Language statistics'), 'tab': 'language', - }) + return render( + request, + "stats/language.html", + { + "title": _("Language statistics"), + "tab": "language", + }, + ) diff --git a/judge/views/status.py b/judge/views/status.py index a7f0b26..25902ec 100644 --- a/judge/views/status.py +++ b/judge/views/status.py @@ -8,35 +8,43 @@ from packaging import version from judge.models import Judge, Language, RuntimeVersion -__all__ = ['status_all', 'status_table'] +__all__ = ["status_all", "status_table"] def get_judges(request): if request.user.is_superuser or request.user.is_staff: - return True, Judge.objects.order_by('-online', 'name') + return True, Judge.objects.order_by("-online", "name") else: return False, Judge.objects.filter(online=True) def status_all(request): see_all, judges = get_judges(request) - return render(request, 'status/judge-status.html', { - 'title': _('Status'), - 'judges': judges, - 'see_all_judges': see_all, - }) + return render( + request, + "status/judge-status.html", + { + "title": _("Status"), + "judges": judges, + "see_all_judges": see_all, + }, + ) def status_table(request): see_all, judges = get_judges(request) - return render(request, 'status/judge-status-table.html', { - 'judges': judges, - 'see_all_judges': see_all, - }) + return render( + request, + "status/judge-status-table.html", + { + "judges": judges, + "see_all_judges": see_all, + }, + ) class LatestList(list): - __slots__ = ('versions', 'is_latest') + __slots__ = ("versions", "is_latest") def compare_version_list(x, y): @@ -61,11 +69,13 @@ def version_matrix(request): judges = {judge.id: judge.name for judge in Judge.objects.filter(online=True)} languages = Language.objects.filter(judges__online=True).distinct() - for runtime in RuntimeVersion.objects.filter(judge__online=True).order_by('priority'): + for runtime in RuntimeVersion.objects.filter(judge__online=True).order_by( + "priority" + ): matrix[runtime.judge_id][runtime.language_id].append(runtime) for judge, data in six.iteritems(matrix): - name_tuple = judges[judge].rpartition('.') + name_tuple = judges[judge].rpartition(".") groups[name_tuple[0] or name_tuple[-1]].append((judges[judge], data)) matrix = {} @@ -103,9 +113,13 @@ def version_matrix(request): versions.is_latest = versions.versions == latest[language] languages = sorted(languages, key=lambda lang: version.parse(lang.name)) - return render(request, 'status/versions.html', { - 'title': _('Version matrix'), - 'judges': sorted(matrix.keys()), - 'languages': languages, - 'matrix': matrix, - }) + return render( + request, + "status/versions.html", + { + "title": _("Version matrix"), + "judges": sorted(matrix.keys()), + "languages": languages, + "matrix": matrix, + }, + ) diff --git a/judge/views/submission.py b/judge/views/submission.py index d10da79..42d941c 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -50,16 +50,33 @@ from judge.utils.views import TitleMixin def submission_related(queryset): - return queryset.select_related('user__user', 'problem', 'language') \ - .only('id', 'user__user__username', 'user__display_rank', 'user__rating', 'problem__name', - 'problem__code', 'problem__is_public', 'language__short_name', 'language__key', 'date', 'time', 'memory', - 'points', 'result', 'status', 'case_points', 'case_total', 'current_testcase', 'contest_object') + return queryset.select_related("user__user", "problem", "language").only( + "id", + "user__user__username", + "user__display_rank", + "user__rating", + "problem__name", + "problem__code", + "problem__is_public", + "language__short_name", + "language__key", + "date", + "time", + "memory", + "points", + "result", + "status", + "case_points", + "case_total", + "current_testcase", + "contest_object", + ) class SubmissionMixin(object): model = Submission - context_object_name = 'submission' - pk_url_kwarg = 'submission' + context_object_name = "submission" + pk_url_kwarg = "submission" class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView): @@ -67,63 +84,76 @@ class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, Deta submission = super(SubmissionDetailBase, self).get_object(queryset) profile = self.request.profile problem = submission.problem - if self.request.user.has_perm('judge.view_all_submission'): + if self.request.user.has_perm("judge.view_all_submission"): return submission if submission.user_id == profile.id: return submission if problem.is_editor(profile): return submission if problem.is_public or problem.testers.filter(id=profile.id).exists(): - if Submission.objects.filter(user_id=profile.id, result='AC', problem_id=problem.id, - points=problem.points).exists(): + if Submission.objects.filter( + user_id=profile.id, + result="AC", + problem_id=problem.id, + points=problem.points, + ).exists(): return submission - if (hasattr(submission, 'contest') and - submission.contest.participation.contest.is_editable_by(self.request.user)): + if hasattr( + submission, "contest" + ) and submission.contest.participation.contest.is_editable_by( + self.request.user + ): return submission - + raise PermissionDenied() def get_title(self): submission = self.object - return _('Submission of %(problem)s by %(user)s') % { - 'problem': submission.problem.translated_name(self.request.LANGUAGE_CODE), - 'user': submission.user.user.username, + return _("Submission of %(problem)s by %(user)s") % { + "problem": submission.problem.translated_name(self.request.LANGUAGE_CODE), + "user": submission.user.user.username, } def get_content_title(self): submission = self.object - return mark_safe(escape(_('Submission of %(problem)s by %(user)s')) % { - 'problem': format_html('{1}', - reverse('problem_detail', args=[ - submission.problem.code]), - submission.problem.translated_name(self.request.LANGUAGE_CODE)), - 'user': format_html('{1}', - reverse('user_page', args=[ - submission.user.user.username]), - submission.user.user.username), - }) + return mark_safe( + escape(_("Submission of %(problem)s by %(user)s")) + % { + "problem": format_html( + '{1}', + reverse("problem_detail", args=[submission.problem.code]), + submission.problem.translated_name(self.request.LANGUAGE_CODE), + ), + "user": format_html( + '{1}', + reverse("user_page", args=[submission.user.user.username]), + submission.user.user.username, + ), + } + ) class SubmissionSource(SubmissionDetailBase): - template_name = 'submission/source.html' + template_name = "submission/source.html" def get_queryset(self): - return super().get_queryset().select_related('source') + return super().get_queryset().select_related("source") def get_context_data(self, **kwargs): context = super(SubmissionSource, self).get_context_data(**kwargs) submission = self.object - context['raw_source'] = submission.source.source.rstrip('\n') - context['highlighted_source'] = highlight_code( - submission.source.source, submission.language.pygments, linenos=False) + context["raw_source"] = submission.source.source.rstrip("\n") + context["highlighted_source"] = highlight_code( + submission.source.source, submission.language.pygments, linenos=False + ) return context def make_batch(batch, cases): - result = {'id': batch, 'cases': cases} + result = {"id": batch, "cases": cases} if batch: - result['points'] = min(map(attrgetter('points'), cases)) - result['total'] = max(map(attrgetter('total'), cases)) + result["points"] = min(map(attrgetter("points"), cases)) + result["total"] = max(map(attrgetter("total"), cases)) return result @@ -143,33 +173,37 @@ def group_test_cases(cases): def get_cases_data(submission): - testcases = ProblemTestCase.objects.filter(dataset=submission.problem)\ - .order_by('order') - - if (submission.is_pretested): + testcases = ProblemTestCase.objects.filter(dataset=submission.problem).order_by( + "order" + ) + + if submission.is_pretested: testcases = testcases.filter(is_pretest=True) files = [] for case in testcases: - if case.input_file: files.append(case.input_file) - if case.output_file: files.append(case.output_file) + if case.input_file: + files.append(case.input_file) + if case.output_file: + files.append(case.output_file) case_data = get_problem_case(submission.problem, files) problem_data = {} count = 0 for case in testcases: - if case.type != 'C': continue + if case.type != "C": + continue count += 1 problem_data[count] = { - 'input': case_data[case.input_file] if case.input_file else '', - 'answer': case_data[case.output_file] if case.output_file else '', + "input": case_data[case.input_file] if case.input_file else "", + "answer": case_data[case.output_file] if case.output_file else "", } return problem_data class SubmissionStatus(SubmissionDetailBase): - template_name = 'submission/status.html' + template_name = "submission/status.html" def access_testcases_in_contest(self): contest = self.object.contest_or_none @@ -186,46 +220,48 @@ class SubmissionStatus(SubmissionDetailBase): def get_context_data(self, **kwargs): context = super(SubmissionStatus, self).get_context_data(**kwargs) submission = self.object - context['last_msg'] = event.last() - context['batches'] = group_test_cases(submission.test_cases.all()) - context['time_limit'] = submission.problem.time_limit - context['can_see_testcases'] = False - + context["last_msg"] = event.last() + context["batches"] = group_test_cases(submission.test_cases.all()) + context["time_limit"] = submission.problem.time_limit + context["can_see_testcases"] = False + contest = submission.contest_or_none prefix_length = 0 can_see_testcases = self.access_testcases_in_contest() - if (contest is not None): + if contest is not None: prefix_length = contest.problem.output_prefix_override - + if contest is None or prefix_length > 0 or can_see_testcases: - context['cases_data'] = get_cases_data(submission) - context['can_see_testcases'] = True + context["cases_data"] = get_cases_data(submission) + context["can_see_testcases"] = True try: lang_limit = submission.problem.language_limits.get( - language=submission.language) + language=submission.language + ) except ObjectDoesNotExist: pass else: - context['time_limit'] = lang_limit.time_limit + context["time_limit"] = lang_limit.time_limit return context class SubmissionTestCaseQuery(SubmissionStatus): - template_name = 'submission/status-testcases.html' + template_name = "submission/status-testcases.html" def get(self, request, *args, **kwargs): - if 'id' not in request.GET or not request.GET['id'].isdigit(): + if "id" not in request.GET or not request.GET["id"].isdigit(): return HttpResponseBadRequest() self.kwargs[self.pk_url_kwarg] = kwargs[self.pk_url_kwarg] = int( - request.GET['id']) + request.GET["id"] + ) return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs) class SubmissionSourceRaw(SubmissionSource): def get(self, request, *args, **kwargs): submission = self.get_object() - return HttpResponse(submission.source.source, content_type='text/plain') + return HttpResponse(submission.source.source, content_type="text/plain") @require_POST @@ -234,28 +270,29 @@ def abort_submission(request, submission): # if (not request.user.is_authenticated or (submission.was_rejudged or (request.profile != submission.user)) and # not request.user.has_perm('abort_any_submission')): # raise PermissionDenied() - if (not request.user.is_authenticated - or not request.user.has_perm('abort_any_submission')): + if not request.user.is_authenticated or not request.user.has_perm( + "abort_any_submission" + ): raise PermissionDenied() submission.abort() - return HttpResponseRedirect(reverse('submission_status', args=(submission.id,))) + return HttpResponseRedirect(reverse("submission_status", args=(submission.id,))) class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): model = Submission paginate_by = 50 show_problem = True - title = gettext_lazy('All submissions') - content_title = gettext_lazy('All submissions') - tab = 'all_submissions_list' - template_name = 'submission/list.html' - context_object_name = 'submissions' + title = gettext_lazy("All submissions") + content_title = gettext_lazy("All submissions") + tab = "all_submissions_list" + template_name = "submission/list.html" + context_object_name = "submissions" first_page_href = None def get_result_data(self): result = self._get_result_data() - for category in result['categories']: - category['name'] = _(category['name']) + for category in result["categories"]: + category["name"] = _(category["name"]) return result def _get_result_data(self): @@ -266,8 +303,11 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): @cached_property def in_contest(self): - return self.request.user.is_authenticated and self.request.profile.current_contest is not None \ + return ( + self.request.user.is_authenticated + and self.request.profile.current_contest is not None and self.request.in_contest_mode + ) @cached_property def contest(self): @@ -276,34 +316,46 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): def _get_queryset(self): queryset = Submission.objects.all() use_straight_join(queryset) - queryset = submission_related(queryset.order_by('-id')) + queryset = submission_related(queryset.order_by("-id")) if self.show_problem: - queryset = queryset.prefetch_related(Prefetch('problem__translations', - queryset=ProblemTranslation.objects.filter( - language=self.request.LANGUAGE_CODE), to_attr='_trans')) + queryset = queryset.prefetch_related( + Prefetch( + "problem__translations", + queryset=ProblemTranslation.objects.filter( + language=self.request.LANGUAGE_CODE + ), + to_attr="_trans", + ) + ) if self.in_contest: queryset = queryset.filter(contest_object=self.contest) if not self.contest.can_see_full_scoreboard(self.request.user): queryset = queryset.filter(user=self.request.profile) else: - queryset = queryset.select_related( - 'contest_object').defer('contest_object__description') + queryset = queryset.select_related("contest_object").defer( + "contest_object__description" + ) # This is not technically correct since contest organizers *should* see these, but # the join would be far too messy - if not self.request.user.has_perm('judge.see_private_contest'): + if not self.request.user.has_perm("judge.see_private_contest"): # Show submissions for any contest you can edit or visible scoreboard - contest_queryset = Contest.objects.filter(Q(authors=self.request.profile) | - Q(curators=self.request.profile) | - Q(scoreboard_visibility=Contest.SCOREBOARD_VISIBLE) | - Q(end_time__lt=timezone.now())).distinct() - queryset = queryset.filter(Q(user=self.request.profile) | - Q(contest_object__in=contest_queryset) | - Q(contest_object__isnull=True)) + contest_queryset = Contest.objects.filter( + Q(authors=self.request.profile) + | Q(curators=self.request.profile) + | Q(scoreboard_visibility=Contest.SCOREBOARD_VISIBLE) + | Q(end_time__lt=timezone.now()) + ).distinct() + queryset = queryset.filter( + Q(user=self.request.profile) + | Q(contest_object__in=contest_queryset) + | Q(contest_object__isnull=True) + ) if self.selected_languages: queryset = queryset.filter( - language__in=Language.objects.filter(key__in=self.selected_languages)) + language__in=Language.objects.filter(key__in=self.selected_languages) + ) if self.selected_statuses: queryset = queryset.filter(result__in=self.selected_statuses) @@ -314,10 +366,15 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): if not self.in_contest: join_sql_subquery( queryset, - subquery=str(Problem.get_visible_problems(self.request.user).distinct().only('id').query), + subquery=str( + Problem.get_visible_problems(self.request.user) + .distinct() + .only("id") + .query + ), params=[], - join_fields=[('problem_id', 'id')], - alias='visible_problems', + join_fields=[("problem_id", "id")], + alias="visible_problems", ) return queryset @@ -325,43 +382,49 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): return None def get_all_submissions_page(self): - return reverse('all_submissions') + return reverse("all_submissions") def get_searchable_status_codes(self): - hidden_codes = ['SC'] + hidden_codes = ["SC"] if not self.request.user.is_superuser and not self.request.user.is_staff: - hidden_codes += ['IE'] - return [(key, value) for key, value in Submission.RESULT if key not in hidden_codes] + hidden_codes += ["IE"] + return [ + (key, value) for key, value in Submission.RESULT if key not in hidden_codes + ] def get_context_data(self, **kwargs): context = super(SubmissionsListBase, self).get_context_data(**kwargs) authenticated = self.request.user.is_authenticated - context['dynamic_update'] = False - context['show_problem'] = self.show_problem - context['completed_problem_ids'] = user_completed_ids( - self.request.profile) if authenticated else [] - context['authored_problem_ids'] = user_authored_ids( - self.request.profile) if authenticated else [] - context['editable_problem_ids'] = user_editable_ids( - self.request.profile) if authenticated else [] + context["dynamic_update"] = False + context["show_problem"] = self.show_problem + context["completed_problem_ids"] = ( + user_completed_ids(self.request.profile) if authenticated else [] + ) + context["authored_problem_ids"] = ( + user_authored_ids(self.request.profile) if authenticated else [] + ) + context["editable_problem_ids"] = ( + user_editable_ids(self.request.profile) if authenticated else [] + ) - context['all_languages'] = Language.objects.all( - ).values_list('key', 'name') - context['selected_languages'] = self.selected_languages + context["all_languages"] = Language.objects.all().values_list("key", "name") + context["selected_languages"] = self.selected_languages - context['all_statuses'] = self.get_searchable_status_codes() - context['selected_statuses'] = self.selected_statuses + context["all_statuses"] = self.get_searchable_status_codes() + context["selected_statuses"] = self.selected_statuses - context['results_json'] = mark_safe(json.dumps(self.get_result_data())) - context['results_colors_json'] = mark_safe( - json.dumps(settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS)) + context["results_json"] = mark_safe(json.dumps(self.get_result_data())) + context["results_colors_json"] = mark_safe( + json.dumps(settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS) + ) - context['page_suffix'] = suffix = ( - '?' + self.request.GET.urlencode()) if self.request.GET else '' - context['first_page_href'] = (self.first_page_href or '.') + suffix - context['my_submissions_link'] = self.get_my_submissions_page() - context['all_submissions_link'] = self.get_all_submissions_page() - context['tab'] = self.tab + context["page_suffix"] = suffix = ( + ("?" + self.request.GET.urlencode()) if self.request.GET else "" + ) + context["first_page_href"] = (self.first_page_href or ".") + suffix + context["my_submissions_link"] = self.get_my_submissions_page() + context["all_submissions_link"] = self.get_all_submissions_page() + context["tab"] = self.tab return context @@ -370,10 +433,10 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): if check is not None: return check - self.selected_languages = set(request.GET.getlist('language')) - self.selected_statuses = set(request.GET.getlist('status')) + self.selected_languages = set(request.GET.getlist("language")) + self.selected_statuses = set(request.GET.getlist("status")) - if 'results' in request.GET: + if "results" in request.GET: return JsonResponse(self.get_result_data()) return super(SubmissionsListBase, self).get(request, *args, **kwargs) @@ -381,50 +444,57 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): class UserMixin(object): def get(self, request, *args, **kwargs): - if 'user' not in kwargs: - raise ImproperlyConfigured('Must pass a user') - self.profile = get_object_or_404( - Profile, user__username=kwargs['user']) - self.username = kwargs['user'] + if "user" not in kwargs: + raise ImproperlyConfigured("Must pass a user") + self.profile = get_object_or_404(Profile, user__username=kwargs["user"]) + self.username = kwargs["user"] return super(UserMixin, self).get(request, *args, **kwargs) class ConditionalUserTabMixin(object): def get_context_data(self, **kwargs): - context = super(ConditionalUserTabMixin, - self).get_context_data(**kwargs) + context = super(ConditionalUserTabMixin, self).get_context_data(**kwargs) if self.request.user.is_authenticated and self.request.profile == self.profile: - context['tab'] = 'my_submissions_tab' + context["tab"] = "my_submissions_tab" else: - context['tab'] = 'user_submissions_tab' - context['tab_username'] = self.profile.user.username + context["tab"] = "user_submissions_tab" + context["tab_username"] = self.profile.user.username return context class AllUserSubmissions(ConditionalUserTabMixin, UserMixin, SubmissionsListBase): def get_queryset(self): - return super(AllUserSubmissions, self).get_queryset().filter(user_id=self.profile.id) + return ( + super(AllUserSubmissions, self) + .get_queryset() + .filter(user_id=self.profile.id) + ) def get_title(self): if self.request.user.is_authenticated and self.request.profile == self.profile: - return _('All my submissions') - return _('All submissions by %s') % self.username + return _("All my submissions") + return _("All submissions by %s") % self.username def get_content_title(self): if self.request.user.is_authenticated and self.request.profile == self.profile: - return format_html('All my submissions') - return format_html('All submissions by {0}', self.username, - reverse('user_page', args=[self.username])) + return format_html("All my submissions") + return format_html( + 'All submissions by {0}', + self.username, + reverse("user_page", args=[self.username]), + ) def get_my_submissions_page(self): if self.request.user.is_authenticated: - return reverse('all_user_submissions', kwargs={'user': self.request.user.username}) + return reverse( + "all_user_submissions", kwargs={"user": self.request.user.username} + ) def get_context_data(self, **kwargs): context = super(AllUserSubmissions, self).get_context_data(**kwargs) - context['dynamic_update'] = context['page_obj'].number == 1 - context['dynamic_user_id'] = self.profile.id - context['last_msg'] = event.last() + context["dynamic_update"] = context["page_obj"].number == 1 + context["dynamic_user_id"] = self.profile.id + context["last_msg"] = event.last() return context @@ -434,16 +504,28 @@ class ProblemSubmissionsBase(SubmissionsListBase): check_contest_in_access_check = True def get_queryset(self): - if self.in_contest and not self.contest.contest_problems.filter(problem_id=self.problem.id).exists(): + if ( + self.in_contest + and not self.contest.contest_problems.filter( + problem_id=self.problem.id + ).exists() + ): raise Http404() - return super(ProblemSubmissionsBase, self)._get_queryset().filter(problem_id=self.problem.id) + return ( + super(ProblemSubmissionsBase, self) + ._get_queryset() + .filter(problem_id=self.problem.id) + ) def get_title(self): - return _('All submissions for %s') % self.problem_name + return _("All submissions for %s") % self.problem_name def get_content_title(self): - return format_html('All submissions for {0}', self.problem_name, - reverse('problem_detail', args=[self.problem.code])) + return format_html( + 'All submissions for {0}', + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + ) def access_check_contest(self, request): if self.in_contest and not self.contest.can_see_own_scoreboard(request.user): @@ -457,33 +539,39 @@ class ProblemSubmissionsBase(SubmissionsListBase): self.access_check_contest(request) def get(self, request, *args, **kwargs): - if 'problem' not in kwargs: - raise ImproperlyConfigured(_('Must pass a problem')) - self.problem = get_object_or_404(Problem, code=kwargs['problem']) - self.problem_name = self.problem.translated_name( - self.request.LANGUAGE_CODE) + if "problem" not in kwargs: + raise ImproperlyConfigured(_("Must pass a problem")) + self.problem = get_object_or_404(Problem, code=kwargs["problem"]) + self.problem_name = self.problem.translated_name(self.request.LANGUAGE_CODE) return super(ProblemSubmissionsBase, self).get(request, *args, **kwargs) def get_all_submissions_page(self): - return reverse('chronological_submissions', kwargs={'problem': self.problem.code}) + return reverse( + "chronological_submissions", kwargs={"problem": self.problem.code} + ) def get_context_data(self, **kwargs): - context = super(ProblemSubmissionsBase, - self).get_context_data(**kwargs) + context = super(ProblemSubmissionsBase, self).get_context_data(**kwargs) if self.dynamic_update: - context['dynamic_update'] = context['page_obj'].number == 1 - context['dynamic_problem_id'] = self.problem.id - context['last_msg'] = event.last() - context['best_submissions_link'] = reverse('ranked_submissions', kwargs={ - 'problem': self.problem.code}) + context["dynamic_update"] = context["page_obj"].number == 1 + context["dynamic_problem_id"] = self.problem.id + context["last_msg"] = event.last() + context["best_submissions_link"] = reverse( + "ranked_submissions", kwargs={"problem": self.problem.code} + ) return context class ProblemSubmissions(ProblemSubmissionsBase): def get_my_submissions_page(self): if self.request.user.is_authenticated: - return reverse('user_submissions', kwargs={'problem': self.problem.code, - 'user': self.request.user.username}) + return reverse( + "user_submissions", + kwargs={ + "problem": self.problem.code, + "user": self.request.user.username, + }, + ) class UserProblemSubmissions(ConditionalUserTabMixin, UserMixin, ProblemSubmissions): @@ -491,7 +579,9 @@ class UserProblemSubmissions(ConditionalUserTabMixin, UserMixin, ProblemSubmissi @cached_property def is_own(self): - return self.request.user.is_authenticated and self.request.profile == self.profile + return ( + self.request.user.is_authenticated and self.request.profile == self.profile + ) def access_check(self, request): super(UserProblemSubmissions, self).access_check(request) @@ -500,60 +590,84 @@ class UserProblemSubmissions(ConditionalUserTabMixin, UserMixin, ProblemSubmissi self.access_check_contest(request) def get_queryset(self): - return super(UserProblemSubmissions, self).get_queryset().filter(user_id=self.profile.id) + return ( + super(UserProblemSubmissions, self) + .get_queryset() + .filter(user_id=self.profile.id) + ) def get_title(self): if self.is_own: - return _("My submissions for %(problem)s") % {'problem': self.problem_name} - return _("%(user)s's submissions for %(problem)s") % {'user': self.username, 'problem': self.problem_name} + return _("My submissions for %(problem)s") % {"problem": self.problem_name} + return _("%(user)s's submissions for %(problem)s") % { + "user": self.username, + "problem": self.problem_name, + } def get_content_title(self): if self.request.user.is_authenticated and self.request.profile == self.profile: - return format_html('''My submissions for {2}''', - self.username, reverse( - 'user_page', args=[self.username]), - self.problem_name, reverse('problem_detail', args=[self.problem.code])) - return format_html('''{0}'s submissions for {2}''', - self.username, reverse( - 'user_page', args=[self.username]), - self.problem_name, reverse('problem_detail', args=[self.problem.code])) + return format_html( + """My submissions for {2}""", + self.username, + reverse("user_page", args=[self.username]), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + ) + return format_html( + """{0}'s submissions for {2}""", + self.username, + reverse("user_page", args=[self.username]), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + ) def get_context_data(self, **kwargs): - context = super(UserProblemSubmissions, - self).get_context_data(**kwargs) - context['dynamic_user_id'] = self.profile.id + context = super(UserProblemSubmissions, self).get_context_data(**kwargs) + context["dynamic_user_id"] = self.profile.id return context def single_submission(request, submission_id, show_problem=True): request.no_profile_update = True authenticated = request.user.is_authenticated - submission = get_object_or_404(submission_related( - Submission.objects.all()), id=int(submission_id)) + submission = get_object_or_404( + submission_related(Submission.objects.all()), id=int(submission_id) + ) if not submission.problem.is_accessible_by(request.user): raise Http404() - return render(request, 'submission/row.html', { - 'submission': submission, - 'authored_problem_ids': user_authored_ids(request.profile) if authenticated else [], - 'completed_problem_ids': user_completed_ids(request.profile) if authenticated else [], - 'editable_problem_ids': user_editable_ids(request.profile) if authenticated else [], - 'show_problem': show_problem, - 'problem_name': show_problem and submission.problem.translated_name(request.LANGUAGE_CODE), - 'profile_id': request.profile.id if authenticated else 0, - }) + return render( + request, + "submission/row.html", + { + "submission": submission, + "authored_problem_ids": user_authored_ids(request.profile) + if authenticated + else [], + "completed_problem_ids": user_completed_ids(request.profile) + if authenticated + else [], + "editable_problem_ids": user_editable_ids(request.profile) + if authenticated + else [], + "show_problem": show_problem, + "problem_name": show_problem + and submission.problem.translated_name(request.LANGUAGE_CODE), + "profile_id": request.profile.id if authenticated else 0, + }, + ) def single_submission_query(request): request.no_profile_update = True - if 'id' not in request.GET or not request.GET['id'].isdigit(): + if "id" not in request.GET or not request.GET["id"].isdigit(): return HttpResponseBadRequest() try: - show_problem = int(request.GET.get('show_problem', '1')) + show_problem = int(request.GET.get("show_problem", "1")) except ValueError: return HttpResponseBadRequest() - return single_submission(request, int(request.GET['id']), bool(show_problem)) + return single_submission(request, int(request.GET["id"]), bool(show_problem)) class AllSubmissions(SubmissionsListBase): @@ -561,20 +675,22 @@ class AllSubmissions(SubmissionsListBase): def get_my_submissions_page(self): if self.request.user.is_authenticated: - return reverse('all_user_submissions', kwargs={'user': self.request.user.username}) + return reverse( + "all_user_submissions", kwargs={"user": self.request.user.username} + ) def get_context_data(self, **kwargs): context = super(AllSubmissions, self).get_context_data(**kwargs) - context['dynamic_update'] = context['page_obj'].number == 1 - context['last_msg'] = event.last() - context['stats_update_interval'] = self.stats_update_interval + context["dynamic_update"] = context["page_obj"].number == 1 + context["last_msg"] = event.last() + context["stats_update_interval"] = self.stats_update_interval return context def _get_result_data(self): if self.in_contest or self.selected_languages or self.selected_statuses: return super(AllSubmissions, self)._get_result_data() - key = 'global_submission_result_data' + key = "global_submission_result_data" result = cache.get(key) if result: return result @@ -595,28 +711,42 @@ class ForceContestMixin(object): def access_check(self, request): super(ForceContestMixin, self).access_check(request) - if not request.user.has_perm('judge.see_private_contest'): + if not request.user.has_perm("judge.see_private_contest"): if not self.contest.is_visible: raise Http404() - if self.contest.start_time is not None and self.contest.start_time > timezone.now(): + if ( + self.contest.start_time is not None + and self.contest.start_time > timezone.now() + ): raise Http404() def get_problem_number(self, problem): - return self.contest.contest_problems.select_related('problem').get(problem=problem).order + return ( + self.contest.contest_problems.select_related("problem") + .get(problem=problem) + .order + ) def get(self, request, *args, **kwargs): - if 'contest' not in kwargs: - raise ImproperlyConfigured(_('Must pass a contest')) - self._contest = get_object_or_404(Contest, key=kwargs['contest']) + if "contest" not in kwargs: + raise ImproperlyConfigured(_("Must pass a contest")) + self._contest = get_object_or_404(Contest, key=kwargs["contest"]) return super(ForceContestMixin, self).get(request, *args, **kwargs) class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): def get_title(self): if self.problem.is_accessible_by(self.request.user): - return "%s's submissions for %s in %s" % (self.username, self.problem_name, self.contest.name) + return "%s's submissions for %s in %s" % ( + self.username, + self.problem_name, + self.contest.name, + ) return "%s's submissions for problem %s in %s" % ( - self.username, self.get_problem_number(self.problem), self.contest.name) + self.username, + self.get_problem_number(self.problem), + self.contest.name, + ) def access_check(self, request): super(UserContestSubmissions, self).access_check(request) @@ -625,16 +755,26 @@ class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): def get_content_title(self): if self.problem.is_accessible_by(self.request.user): - return format_html(_('{0}\'s submissions for ' - '{2} in {4}'), - self.username, reverse( - 'user_page', args=[self.username]), - self.problem_name, reverse( - 'problem_detail', args=[self.problem.code]), - self.contest.name, reverse('contest_view', args=[self.contest.key])) - return format_html(_('{0}\'s submissions for ' - 'problem {2} in {3}'), - self.username, reverse( - 'user_page', args=[self.username]), - self.get_problem_number(self.problem), - self.contest.name, reverse('contest_view', args=[self.contest.key])) + return format_html( + _( + '{0}\'s submissions for ' + '{2} in {4}' + ), + self.username, + reverse("user_page", args=[self.username]), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + self.contest.name, + reverse("contest_view", args=[self.contest.key]), + ) + return format_html( + _( + '{0}\'s submissions for ' + 'problem {2} in {3}' + ), + self.username, + reverse("user_page", args=[self.username]), + self.get_problem_number(self.problem), + self.contest.name, + reverse("contest_view", args=[self.contest.key]), + ) diff --git a/judge/views/tasks.py b/judge/views/tasks.py index 42e123f..a1c0644 100644 --- a/judge/views/tasks.py +++ b/judge/views/tasks.py @@ -4,7 +4,12 @@ from uuid import UUID from celery.result import AsyncResult from django.core.exceptions import PermissionDenied -from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse +from django.http import ( + Http404, + HttpResponseBadRequest, + HttpResponseRedirect, + JsonResponse, +) from django.shortcuts import render from django.urls import reverse from django.utils.http import is_safe_url @@ -17,14 +22,19 @@ from judge.utils.views import short_circuit_middleware def get_task_status(task_id): result = AsyncResult(task_id) info = result.result - if result.state == 'PROGRESS': - return {'code': 'PROGRESS', 'done': info['done'], 'total': info['total'], 'stage': info['stage']} - elif result.state == 'SUCCESS': - return {'code': 'SUCCESS'} - elif result.state == 'FAILURE': - return {'code': 'FAILURE', 'error': str(info)} + if result.state == "PROGRESS": + return { + "code": "PROGRESS", + "done": info["done"], + "total": info["total"], + "stage": info["stage"], + } + elif result.state == "SUCCESS": + return {"code": "SUCCESS"} + elif result.state == "FAILURE": + return {"code": "FAILURE", "error": str(info)} else: - return {'code': 'WORKING'} + return {"code": "WORKING"} def task_status(request, task_id): @@ -33,34 +43,48 @@ def task_status(request, task_id): except ValueError: raise Http404() - redirect = request.GET.get('redirect') + redirect = request.GET.get("redirect") if not is_safe_url(redirect, allowed_hosts={request.get_host()}): redirect = None status = get_task_status(task_id) - if status['code'] == 'SUCCESS' and redirect: + if status["code"] == "SUCCESS" and redirect: return HttpResponseRedirect(redirect) - return render(request, 'task_status.html', { - 'task_id': task_id, 'task_status': json.dumps(status), - 'message': request.GET.get('message', ''), 'redirect': redirect or '', - }) + return render( + request, + "task_status.html", + { + "task_id": task_id, + "task_status": json.dumps(status), + "message": request.GET.get("message", ""), + "redirect": redirect or "", + }, + ) @short_circuit_middleware def task_status_ajax(request): - if 'id' not in request.GET: - return HttpResponseBadRequest('Need to pass GET parameter "id"', content_type='text/plain') - return JsonResponse(get_task_status(request.GET['id'])) + if "id" not in request.GET: + return HttpResponseBadRequest( + 'Need to pass GET parameter "id"', content_type="text/plain" + ) + return JsonResponse(get_task_status(request.GET["id"])) def demo_task(request, task, message): if not request.user.is_superuser: raise PermissionDenied() result = task.delay() - return redirect_to_task_status(result, message=message, redirect=reverse('home')) + return redirect_to_task_status(result, message=message, redirect=reverse("home")) -demo_success = partial(demo_task, task=success, message='Running example task that succeeds...') -demo_failure = partial(demo_task, task=failure, message='Running example task that fails...') -demo_progress = partial(demo_task, task=progress, message='Running example task that waits 10 seconds...') +demo_success = partial( + demo_task, task=success, message="Running example task that succeeds..." +) +demo_failure = partial( + demo_task, task=failure, message="Running example task that fails..." +) +demo_progress = partial( + demo_task, task=progress, message="Running example task that waits 10 seconds..." +) diff --git a/judge/views/ticket.py b/judge/views/ticket.py index afde514..3d62235 100644 --- a/judge/views/ticket.py +++ b/judge/views/ticket.py @@ -4,8 +4,18 @@ from itertools import chain from django import forms from django.contrib.auth.mixins import LoginRequiredMixin -from django.core.exceptions import ImproperlyConfigured, PermissionDenied, ValidationError -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse +from django.core.exceptions import ( + ImproperlyConfigured, + PermissionDenied, + ValidationError, +) +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseRedirect, + JsonResponse, +) from django.shortcuts import get_object_or_404 from django.template.defaultfilters import truncatechars from django.template.loader import get_template @@ -26,90 +36,107 @@ from judge.utils.views import SingleObjectFormView, TitleMixin, paginate_query_c from judge.views.problem import ProblemMixin from judge.widgets import HeavyPreviewPageDownWidget -ticket_widget = (forms.Textarea() if HeavyPreviewPageDownWidget is None else - HeavyPreviewPageDownWidget(preview=reverse_lazy('ticket_preview'), - preview_timeout=1000, hide_preview_button=True)) +ticket_widget = ( + forms.Textarea() + if HeavyPreviewPageDownWidget is None + else HeavyPreviewPageDownWidget( + preview=reverse_lazy("ticket_preview"), + preview_timeout=1000, + hide_preview_button=True, + ) +) def add_ticket_notifications(users, author, link, ticket): - html = f"{ticket.linked_item}" - + html = f'{ticket.linked_item}' + users = set(users) if author in users: users.remove(author) for user in users: - notification = Notification(owner=user, - html_link=html, - category='Ticket', - author=author) + notification = Notification( + owner=user, html_link=html, category="Ticket", author=author + ) notification.save() class TicketForm(forms.Form): - title = forms.CharField(max_length=100, label=gettext_lazy('Ticket title')) + title = forms.CharField(max_length=100, label=gettext_lazy("Ticket title")) body = forms.CharField(widget=ticket_widget) def __init__(self, request, *args, **kwargs): self.request = request super(TicketForm, self).__init__(*args, **kwargs) - self.fields['title'].widget.attrs.update({'placeholder': _('Ticket title')}) - self.fields['body'].widget.attrs.update({'placeholder': _('Issue description')}) + self.fields["title"].widget.attrs.update({"placeholder": _("Ticket title")}) + self.fields["body"].widget.attrs.update({"placeholder": _("Issue description")}) def clean(self): if self.request is not None and self.request.user.is_authenticated: profile = self.request.profile if profile.mute: - raise ValidationError(_('Your part is silent, little toad.')) + raise ValidationError(_("Your part is silent, little toad.")) return super(TicketForm, self).clean() class NewTicketView(LoginRequiredMixin, SingleObjectFormView): form_class = TicketForm - template_name = 'ticket/new.html' + template_name = "ticket/new.html" def get_assignees(self): return [] def get_form_kwargs(self): kwargs = super(NewTicketView, self).get_form_kwargs() - kwargs['request'] = self.request + kwargs["request"] = self.request return kwargs def form_valid(self, form): - ticket = Ticket(user=self.request.profile, title=form.cleaned_data['title']) + ticket = Ticket(user=self.request.profile, title=form.cleaned_data["title"]) ticket.linked_item = self.object ticket.save() - message = TicketMessage(ticket=ticket, user=ticket.user, body=form.cleaned_data['body']) + message = TicketMessage( + ticket=ticket, user=ticket.user, body=form.cleaned_data["body"] + ) message.save() ticket.assignees.set(self.get_assignees()) - link = reverse('ticket', args=[ticket.id]) + link = reverse("ticket", args=[ticket.id]) add_ticket_notifications(ticket.assignees.all(), ticket.user, link, ticket) if event.real: - event.post('tickets', { - 'type': 'new-ticket', 'id': ticket.id, - 'message': message.id, 'user': ticket.user_id, - 'assignees': list(ticket.assignees.values_list('id', flat=True)), - }) + event.post( + "tickets", + { + "type": "new-ticket", + "id": ticket.id, + "message": message.id, + "user": ticket.user_id, + "assignees": list(ticket.assignees.values_list("id", flat=True)), + }, + ) return HttpResponseRedirect(link) class NewProblemTicketView(ProblemMixin, TitleMixin, NewTicketView): - template_name = 'ticket/new_problem.html' + template_name = "ticket/new_problem.html" def get_assignees(self): return self.object.authors.all() def get_title(self): - return _('New ticket for %s') % self.object.name + return _("New ticket for %s") % self.object.name def get_content_title(self): - return mark_safe(escape(_('New ticket for %s')) % - format_html('{1}', reverse('problem_detail', args=[self.object.code]), - self.object.translated_name(self.request.LANGUAGE_CODE))) + return mark_safe( + escape(_("New ticket for %s")) + % format_html( + '{1}', + reverse("problem_detail", args=[self.object.code]), + self.object.translated_name(self.request.LANGUAGE_CODE), + ) + ) def form_valid(self, form): if not self.object.is_accessible_by(self.request.user): @@ -127,7 +154,7 @@ class TicketMixin(object): def get_object(self, queryset=None): ticket = super(TicketMixin, self).get_object(queryset) profile_id = self.request.profile.id - if self.request.user.has_perm('judge.change_ticket'): + if self.request.user.has_perm("judge.change_ticket"): return ticket if ticket.user_id == profile_id: return ticket @@ -141,39 +168,55 @@ class TicketMixin(object): class TicketView(TitleMixin, LoginRequiredMixin, TicketMixin, SingleObjectFormView): form_class = TicketCommentForm - template_name = 'ticket/ticket.html' - context_object_name = 'ticket' + template_name = "ticket/ticket.html" + context_object_name = "ticket" def form_valid(self, form): - message = TicketMessage(user=self.request.profile, - body=form.cleaned_data['body'], - ticket=self.object) + message = TicketMessage( + user=self.request.profile, + body=form.cleaned_data["body"], + ticket=self.object, + ) message.save() - link = '%s#message-%d' % (reverse('ticket', args=[self.object.id]), message.id) + link = "%s#message-%d" % (reverse("ticket", args=[self.object.id]), message.id) notify_list = list(chain(self.object.assignees.all(), [self.object.user])) add_ticket_notifications(notify_list, message.user, link, self.object) if event.real: - event.post('tickets', { - 'type': 'ticket-message', 'id': self.object.id, - 'message': message.id, 'user': self.object.user_id, - 'assignees': list(self.object.assignees.values_list('id', flat=True)), - }) - event.post('ticket-%d' % self.object.id, { - 'type': 'ticket-message', 'message': message.id, - }) + event.post( + "tickets", + { + "type": "ticket-message", + "id": self.object.id, + "message": message.id, + "user": self.object.user_id, + "assignees": list( + self.object.assignees.values_list("id", flat=True) + ), + }, + ) + event.post( + "ticket-%d" % self.object.id, + { + "type": "ticket-message", + "message": message.id, + }, + ) return HttpResponseRedirect(link) def get_title(self): - return _('%(title)s - Ticket %(id)d') % {'title': self.object.title, 'id': self.object.id} + return _("%(title)s - Ticket %(id)d") % { + "title": self.object.title, + "id": self.object.id, + } def get_context_data(self, **kwargs): context = super(TicketView, self).get_context_data(**kwargs) - context['ticket_messages'] = self.object.messages.select_related('user__user') - context['assignees'] = self.object.assignees.select_related('user') - context['last_msg'] = event.last() + context["ticket_messages"] = self.object.messages.select_related("user__user") + context["assignees"] = self.object.assignees.select_related("user") + context["last_msg"] = event.last() return context @@ -182,21 +225,32 @@ class TicketStatusChangeView(LoginRequiredMixin, TicketMixin, SingleObjectMixin, def post(self, request, *args, **kwargs): if self.open is None: - raise ImproperlyConfigured('Need to define open') + raise ImproperlyConfigured("Need to define open") ticket = self.get_object() if ticket.is_open != self.open: ticket.is_open = self.open ticket.save() if event.real: - event.post('tickets', { - 'type': 'ticket-status', 'id': ticket.id, - 'open': self.open, 'user': ticket.user_id, - 'assignees': list(ticket.assignees.values_list('id', flat=True)), - 'title': ticket.title, - }) - event.post('ticket-%d' % ticket.id, { - 'type': 'ticket-status', 'open': self.open, - }) + event.post( + "tickets", + { + "type": "ticket-status", + "id": ticket.id, + "open": self.open, + "user": ticket.user_id, + "assignees": list( + ticket.assignees.values_list("id", flat=True) + ), + "title": ticket.title, + }, + ) + event.post( + "ticket-%d" % ticket.id, + { + "type": "ticket-status", + "open": self.open, + }, + ) return HttpResponse(status=204) @@ -205,16 +259,16 @@ class TicketNotesForm(forms.Form): class TicketNotesEditView(LoginRequiredMixin, TicketMixin, SingleObjectFormView): - template_name = 'ticket/edit-notes.html' + template_name = "ticket/edit-notes.html" form_class = TicketNotesForm - context_object_name = 'ticket' + context_object_name = "ticket" def get_initial(self): - return {'notes': self.get_object().notes} + return {"notes": self.get_object().notes} def form_valid(self, form): ticket = self.get_object() - ticket.notes = notes = form.cleaned_data['notes'] + ticket.notes = notes = form.cleaned_data["notes"] ticket.save() if notes: return HttpResponse(linebreaks(notes, autoescape=True)) @@ -227,8 +281,8 @@ class TicketNotesEditView(LoginRequiredMixin, TicketMixin, SingleObjectFormView) class TicketList(LoginRequiredMixin, ListView): model = Ticket - template_name = 'ticket/list.html' - context_object_name = 'tickets' + template_name = "ticket/list.html" + context_object_name = "tickets" paginate_by = 50 paginator_class = DiggPaginator @@ -242,32 +296,38 @@ class TicketList(LoginRequiredMixin, ListView): @cached_property def can_edit_all(self): - return self.request.user.has_perm('judge.change_ticket') + return self.request.user.has_perm("judge.change_ticket") @cached_property def filter_users(self): - return self.request.GET.getlist('user') + return self.request.GET.getlist("user") @cached_property def filter_assignees(self): - return self.request.GET.getlist('assignee') + return self.request.GET.getlist("assignee") def GET_with_session(self, key): if not self.request.GET: return self.request.session.get(key, False) - return self.request.GET.get(key, None) == '1' + return self.request.GET.get(key, None) == "1" def _get_queryset(self): - return Ticket.objects.select_related('user__user').prefetch_related('assignees__user').order_by('-id') + return ( + Ticket.objects.select_related("user__user") + .prefetch_related("assignees__user") + .order_by("-id") + ) def get_queryset(self): queryset = self._get_queryset() - if self.GET_with_session('own'): + if self.GET_with_session("own"): queryset = queryset.filter(own_ticket_filter(self.profile.id)) elif not self.can_edit_all: queryset = filter_visible_tickets(queryset, self.user, self.profile) if self.filter_assignees: - queryset = queryset.filter(assignees__user__username__in=self.filter_assignees) + queryset = queryset.filter( + assignees__user__username__in=self.filter_assignees + ) if self.filter_users: queryset = queryset.filter(user__user__username__in=self.filter_users) return queryset.distinct() @@ -275,29 +335,41 @@ class TicketList(LoginRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super(TicketList, self).get_context_data(**kwargs) - page = context['page_obj'] - context['title'] = _('Tickets - Page %(number)d of %(total)d') % { - 'number': page.number, - 'total': page.paginator.num_pages, + page = context["page_obj"] + context["title"] = _("Tickets - Page %(number)d of %(total)d") % { + "number": page.number, + "total": page.paginator.num_pages, } - context['can_edit_all'] = self.can_edit_all - context['filter_status'] = { - 'own': self.GET_with_session('own'), 'user': self.filter_users, 'assignee': self.filter_assignees, - 'user_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_users) - .values_list('id', flat=True))), - 'assignee_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_assignees) - .values_list('id', flat=True))), - 'own_id': self.profile.id if self.GET_with_session('own') else 'null', + context["can_edit_all"] = self.can_edit_all + context["filter_status"] = { + "own": self.GET_with_session("own"), + "user": self.filter_users, + "assignee": self.filter_assignees, + "user_id": json.dumps( + list( + Profile.objects.filter( + user__username__in=self.filter_users + ).values_list("id", flat=True) + ) + ), + "assignee_id": json.dumps( + list( + Profile.objects.filter( + user__username__in=self.filter_assignees + ).values_list("id", flat=True) + ) + ), + "own_id": self.profile.id if self.GET_with_session("own") else "null", } - context['last_msg'] = event.last() + context["last_msg"] = event.last() context.update(paginate_query_context(self.request)) return context def post(self, request, *args, **kwargs): - to_update = ('own',) + to_update = ("own",) for key in to_update: if key in request.GET: - val = request.GET.get(key) == '1' + val = request.GET.get(key) == "1" request.session[key] = val else: request.session.pop(key, None) @@ -306,38 +378,56 @@ class TicketList(LoginRequiredMixin, ListView): class ProblemTicketListView(TicketList): def _get_queryset(self): - problem = get_object_or_404(Problem, code=self.kwargs.get('problem')) + problem = get_object_or_404(Problem, code=self.kwargs.get("problem")) if problem.is_editable_by(self.request.user): - return problem.tickets.order_by('-id') + return problem.tickets.order_by("-id") elif problem.is_accessible_by(self.request.user): - return problem.tickets.filter(own_ticket_filter(self.profile.id)).order_by('-id') + return problem.tickets.filter(own_ticket_filter(self.profile.id)).order_by( + "-id" + ) raise Http404() class TicketListDataAjax(TicketMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): try: - self.kwargs['pk'] = request.GET['id'] + self.kwargs["pk"] = request.GET["id"] except KeyError: return HttpResponseBadRequest() ticket = self.get_object() message = ticket.messages.first() - return JsonResponse({ - 'row': get_template('ticket/row.html').render({'ticket': ticket}, request), - 'notification': { - 'title': _('New Ticket: %s') % ticket.title, - 'body': '%s\n%s' % (_('#%(id)d, assigned to: %(users)s') % { - 'id': ticket.id, - 'users': (_(', ').join(ticket.assignees.values_list('user__username', flat=True)) or _('no one')), - }, truncatechars(message.body, 200)), - }, - }) + return JsonResponse( + { + "row": get_template("ticket/row.html").render( + {"ticket": ticket}, request + ), + "notification": { + "title": _("New Ticket: %s") % ticket.title, + "body": "%s\n%s" + % ( + _("#%(id)d, assigned to: %(users)s") + % { + "id": ticket.id, + "users": ( + _(", ").join( + ticket.assignees.values_list( + "user__username", flat=True + ) + ) + or _("no one") + ), + }, + truncatechars(message.body, 200), + ), + }, + } + ) class TicketMessageDataAjax(TicketMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): try: - message_id = request.GET['message'] + message_id = request.GET["message"] except KeyError: return HttpResponseBadRequest() ticket = self.get_object() @@ -345,10 +435,14 @@ class TicketMessageDataAjax(TicketMixin, SingleObjectMixin, View): message = ticket.messages.get(id=message_id) except TicketMessage.DoesNotExist: return HttpResponseBadRequest() - return JsonResponse({ - 'message': get_template('ticket/message.html').render({'message': message}, request), - 'notification': { - 'title': _('New Ticket Message For: %s') % ticket.title, - 'body': truncatechars(message.body, 200), - }, - }) + return JsonResponse( + { + "message": get_template("ticket/message.html").render( + {"message": message}, request + ), + "notification": { + "title": _("New Ticket Message For: %s") % ticket.title, + "body": truncatechars(message.body, 200), + }, + } + ) diff --git a/judge/views/totp.py b/judge/views/totp.py index 097137f..88fd63a 100644 --- a/judge/views/totp.py +++ b/judge/views/totp.py @@ -21,7 +21,7 @@ class TOTPView(TitleMixin, LoginRequiredMixin, FormView): def get_form_kwargs(self): result = super(TOTPView, self).get_form_kwargs() - result['totp_key'] = self.profile.totp_key + result["totp_key"] = self.profile.totp_key return result def dispatch(self, request, *args, **kwargs): @@ -35,12 +35,12 @@ class TOTPView(TitleMixin, LoginRequiredMixin, FormView): raise NotImplementedError() def next_page(self): - return HttpResponseRedirect(reverse('user_edit_profile')) + return HttpResponseRedirect(reverse("user_edit_profile")) class TOTPEnableView(TOTPView): - title = _('Enable Two Factor Authentication') - template_name = 'registration/totp_enable.html' + title = _("Enable Two Factor Authentication") + template_name = "registration/totp_enable.html" def get(self, request, *args, **kwargs): profile = self.profile @@ -54,20 +54,22 @@ class TOTPEnableView(TOTPView): def post(self, request, *args, **kwargs): if not self.profile.totp_key: - return HttpResponseBadRequest('No TOTP key generated on server side?') + return HttpResponseBadRequest("No TOTP key generated on server side?") return super(TOTPEnableView, self).post(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super(TOTPEnableView, self).get_context_data(**kwargs) - context['totp_key'] = self.profile.totp_key - context['qr_code'] = self.render_qr_code(self.request.user.username, self.profile.totp_key) + context["totp_key"] = self.profile.totp_key + context["qr_code"] = self.render_qr_code( + self.request.user.username, self.profile.totp_key + ) return context def form_valid(self, form): self.profile.is_totp_enabled = True self.profile.save() # Make sure users don't get prompted to enter code right after enabling: - self.request.session['2fa_passed'] = True + self.request.session["2fa_passed"] = True return self.next_page() @classmethod @@ -79,15 +81,17 @@ class TOTPEnableView(TOTPView): qr.add_data(uri) qr.make(fit=True) - image = qr.make_image(fill_color='black', back_color='white') + image = qr.make_image(fill_color="black", back_color="white") buf = BytesIO() - image.save(buf, format='PNG') - return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode('ascii') + image.save(buf, format="PNG") + return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode( + "ascii" + ) class TOTPDisableView(TOTPView): - title = _('Disable Two Factor Authentication') - template_name = 'registration/totp_disable.html' + title = _("Disable Two Factor Authentication") + template_name = "registration/totp_disable.html" def check_skip(self): if not self.profile.is_totp_enabled: @@ -102,21 +106,25 @@ class TOTPDisableView(TOTPView): class TOTPLoginView(SuccessURLAllowedHostsMixin, TOTPView): - title = _('Perform Two Factor Authentication') - template_name = 'registration/totp_auth.html' + title = _("Perform Two Factor Authentication") + template_name = "registration/totp_auth.html" def check_skip(self): - return not self.profile.is_totp_enabled or self.request.session.get('2fa_passed', False) + return not self.profile.is_totp_enabled or self.request.session.get( + "2fa_passed", False + ) def next_page(self): - redirect_to = self.request.GET.get('next', '') + redirect_to = self.request.GET.get("next", "") url_is_safe = is_safe_url( url=redirect_to, allowed_hosts=self.get_success_url_allowed_hosts(), require_https=self.request.is_secure(), ) - return HttpResponseRedirect((redirect_to if url_is_safe else '') or reverse('user_page')) + return HttpResponseRedirect( + (redirect_to if url_is_safe else "") or reverse("user_page") + ) def form_valid(self, form): - self.request.session['2fa_passed'] = True + self.request.session["2fa_passed"] = True return self.next_page() diff --git a/judge/views/user.py b/judge/views/user.py index a14617a..0f06664 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -14,7 +14,14 @@ from django.db.models import Count, Max, Min from django.db.models.fields import DateField from django.db.models.functions import Cast, ExtractYear from django.forms import Form -from django.http import Http404, HttpResponseRedirect, JsonResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponse +from django.http import ( + Http404, + HttpResponseRedirect, + JsonResponse, + HttpResponseForbidden, + HttpResponseBadRequest, + HttpResponse, +) from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.utils import timezone @@ -36,10 +43,16 @@ from judge.utils.problems import contest_completed_ids, user_completed_ids from judge.utils.ranker import ranker from judge.utils.subscription import Subscription from judge.utils.unicode import utf8text -from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, TitleMixin, generic_message, SingleObjectFormView +from judge.utils.views import ( + DiggPaginatorMixin, + QueryStringSortMixin, + TitleMixin, + generic_message, + SingleObjectFormView, +) from .contests import ContestRanking -__all__ = ['UserPage', 'UserAboutPage', 'UserProblemsPage', 'users', 'edit_profile'] +__all__ = ["UserPage", "UserAboutPage", "UserProblemsPage", "users", "edit_profile"] def remap_keys(iterable, mapping): @@ -48,16 +61,16 @@ def remap_keys(iterable, mapping): class UserMixin(object): model = Profile - slug_field = 'user__username' - slug_url_kwarg = 'user' - context_object_name = 'user' + slug_field = "user__username" + slug_url_kwarg = "user" + context_object_name = "user" def render_to_response(self, context, **response_kwargs): return super(UserMixin, self).render_to_response(context, **response_kwargs) class UserPage(TitleMixin, UserMixin, DetailView): - template_name = 'user/user-base.html' + template_name = "user/user-base.html" def get_object(self, queryset=None): if self.kwargs.get(self.slug_url_kwarg, None) is None: @@ -71,12 +84,18 @@ class UserPage(TitleMixin, UserMixin, DetailView): try: return super(UserPage, self).dispatch(request, *args, **kwargs) except Http404: - return generic_message(request, _('No such user'), _('No user handle "%s".') % - self.kwargs.get(self.slug_url_kwarg, None)) + return generic_message( + request, + _("No such user"), + _('No user handle "%s".') % self.kwargs.get(self.slug_url_kwarg, None), + ) def get_title(self): - return (_('My account') if self.request.user == self.object.user else - _('User %s') % self.object.user.username) + return ( + _("My account") + if self.request.user == self.object.user + else _("User %s") % self.object.user.username + ) def get_content_title(self): username = self.object.user.username @@ -92,8 +111,11 @@ class UserPage(TitleMixin, UserMixin, DetailView): @cached_property def in_contest(self): - return self.profile is not None and self.profile.current_contest is not None \ + return ( + self.profile is not None + and self.profile.current_contest is not None and self.request.in_contest_mode + ) def get_completed_problems(self): if self.in_contest: @@ -104,29 +126,49 @@ class UserPage(TitleMixin, UserMixin, DetailView): def get_context_data(self, **kwargs): context = super(UserPage, self).get_context_data(**kwargs) - context['followed'] = Friend.is_friend(self.request.profile, self.object) - context['hide_solved'] = int(self.hide_solved) - context['authored'] = self.object.authored_problems.filter(is_public=True, is_organization_private=False) \ - .order_by('code') + context["followed"] = Friend.is_friend(self.request.profile, self.object) + context["hide_solved"] = int(self.hide_solved) + context["authored"] = self.object.authored_problems.filter( + is_public=True, is_organization_private=False + ).order_by("code") - rating = self.object.ratings.order_by('-contest__end_time')[:1] - context['rating'] = rating[0] if rating else None + rating = self.object.ratings.order_by("-contest__end_time")[:1] + context["rating"] = rating[0] if rating else None - context['rank'] = Profile.objects.filter( - is_unlisted=False, performance_points__gt=self.object.performance_points, - ).count() + 1 + context["rank"] = ( + Profile.objects.filter( + is_unlisted=False, + performance_points__gt=self.object.performance_points, + ).count() + + 1 + ) if rating: - context['rating_rank'] = Profile.objects.filter( - is_unlisted=False, rating__gt=self.object.rating, - ).count() + 1 - context['rated_users'] = Profile.objects.filter(is_unlisted=False, rating__isnull=False).count() - context.update(self.object.ratings.aggregate(min_rating=Min('rating'), max_rating=Max('rating'), - contests=Count('contest'))) + context["rating_rank"] = ( + Profile.objects.filter( + is_unlisted=False, + rating__gt=self.object.rating, + ).count() + + 1 + ) + context["rated_users"] = Profile.objects.filter( + is_unlisted=False, rating__isnull=False + ).count() + context.update( + self.object.ratings.aggregate( + min_rating=Min("rating"), + max_rating=Max("rating"), + contests=Count("contest"), + ) + ) return context def get(self, request, *args, **kwargs): - self.hide_solved = request.GET.get('hide_solved') == '1' if 'hide_solved' in request.GET else False + self.hide_solved = ( + request.GET.get("hide_solved") == "1" + if "hide_solved" in request.GET + else False + ) return super(UserPage, self).get(request, *args, **kwargs) @@ -134,20 +176,27 @@ EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) class UserAboutPage(UserPage): - template_name = 'user/user-about.html' + template_name = "user/user-about.html" def get_awards(self, ratings): result = {} - sorted_ratings = sorted(ratings, - key=lambda x: (x.rank, -x.contest.end_time.timestamp())) + sorted_ratings = sorted( + ratings, key=lambda x: (x.rank, -x.contest.end_time.timestamp()) + ) - result['medals'] = [{ - 'label': rating.contest.name, - 'ranking': rating.rank, - 'link': reverse('contest_ranking', args=(rating.contest.key,)) + '#!' + self.object.username, - 'date': date_format(rating.contest.end_time, _('M j, Y')), - } for rating in sorted_ratings if rating.rank <= 3] + result["medals"] = [ + { + "label": rating.contest.name, + "ranking": rating.rank, + "link": reverse("contest_ranking", args=(rating.contest.key,)) + + "#!" + + self.object.username, + "date": date_format(rating.contest.end_time, _("M j, Y")), + } + for rating in sorted_ratings + if rating.rank <= 3 + ] num_awards = 0 for i in result: @@ -160,60 +209,86 @@ class UserAboutPage(UserPage): def get_context_data(self, **kwargs): context = super(UserAboutPage, self).get_context_data(**kwargs) - ratings = context['ratings'] = self.object.ratings.order_by('-contest__end_time').select_related('contest') \ - .defer('contest__description') - - context['rating_data'] = mark_safe(json.dumps([{ - 'label': rating.contest.name, - 'rating': rating.rating, - 'ranking': rating.rank, - 'link': reverse('contest_ranking', args=(rating.contest.key,)), - 'timestamp': (rating.contest.end_time - EPOCH).total_seconds() * 1000, - 'date': date_format(timezone.localtime(rating.contest.end_time), _('M j, Y, G:i')), - 'class': rating_class(rating.rating), - 'height': '%.3fem' % rating_progress(rating.rating), - } for rating in ratings])) - - context['awards'] = self.get_awards(ratings) - - if ratings: - user_data = self.object.ratings.aggregate(Min('rating'), Max('rating')) - global_data = Rating.objects.aggregate(Min('rating'), Max('rating')) - min_ever, max_ever = global_data['rating__min'], global_data['rating__max'] - min_user, max_user = user_data['rating__min'], user_data['rating__max'] - delta = max_user - min_user - ratio = (max_ever - max_user) / (max_ever - min_ever) if max_ever != min_ever else 1.0 - context['max_graph'] = max_user + ratio * delta - context['min_graph'] = min_user + ratio * delta - delta - - - submissions = ( - self.object.submission_set - .annotate(date_only=Cast('date', DateField())) - .values('date_only').annotate(cnt=Count('id')) + ratings = context["ratings"] = ( + self.object.ratings.order_by("-contest__end_time") + .select_related("contest") + .defer("contest__description") + ) + + context["rating_data"] = mark_safe( + json.dumps( + [ + { + "label": rating.contest.name, + "rating": rating.rating, + "ranking": rating.rank, + "link": reverse("contest_ranking", args=(rating.contest.key,)), + "timestamp": (rating.contest.end_time - EPOCH).total_seconds() + * 1000, + "date": date_format( + timezone.localtime(rating.contest.end_time), + _("M j, Y, G:i"), + ), + "class": rating_class(rating.rating), + "height": "%.3fem" % rating_progress(rating.rating), + } + for rating in ratings + ] + ) + ) + + context["awards"] = self.get_awards(ratings) + + if ratings: + user_data = self.object.ratings.aggregate(Min("rating"), Max("rating")) + global_data = Rating.objects.aggregate(Min("rating"), Max("rating")) + min_ever, max_ever = global_data["rating__min"], global_data["rating__max"] + min_user, max_user = user_data["rating__min"], user_data["rating__max"] + delta = max_user - min_user + ratio = ( + (max_ever - max_user) / (max_ever - min_ever) + if max_ever != min_ever + else 1.0 + ) + context["max_graph"] = max_user + ratio * delta + context["min_graph"] = min_user + ratio * delta - delta + + submissions = ( + self.object.submission_set.annotate(date_only=Cast("date", DateField())) + .values("date_only") + .annotate(cnt=Count("id")) + ) + + context["submission_data"] = mark_safe( + json.dumps( + { + date_counts["date_only"].isoformat(): date_counts["cnt"] + for date_counts in submissions + } + ) + ) + context["submission_metadata"] = mark_safe( + json.dumps( + { + "min_year": ( + self.object.submission_set.annotate( + year_only=ExtractYear("date") + ).aggregate(min_year=Min("year_only"))["min_year"] + ), + } + ) ) - context['submission_data'] = mark_safe(json.dumps({ - date_counts['date_only'].isoformat(): date_counts['cnt'] for date_counts in submissions - })) - context['submission_metadata'] = mark_safe(json.dumps({ - 'min_year': ( - self.object.submission_set - .annotate(year_only=ExtractYear('date')) - .aggregate(min_year=Min('year_only'))['min_year'] - ), - })) - return context # follow/unfollow user def post(self, request, user, *args, **kwargs): try: if not request.profile: - raise Exception('You have to login') - if (request.profile.username == user): - raise Exception('Cannot make friend with yourself') - + raise Exception("You have to login") + if request.profile.username == user: + raise Exception("Cannot make friend with yourself") + following_profile = Profile.objects.get(user__username=user) Friend.toggle_friend(request.profile, following_profile) finally: @@ -221,60 +296,86 @@ class UserAboutPage(UserPage): class UserProblemsPage(UserPage): - template_name = 'user/user-problems.html' + template_name = "user/user-problems.html" def get_context_data(self, **kwargs): context = super(UserProblemsPage, self).get_context_data(**kwargs) - result = Submission.objects.filter(user=self.object, points__gt=0, problem__is_public=True, - problem__is_organization_private=False) \ - .exclude(problem__in=self.get_completed_problems() if self.hide_solved else []) \ - .values('problem__id', 'problem__code', 'problem__name', 'problem__points', 'problem__group__full_name') \ - .distinct().annotate(points=Max('points')).order_by('problem__group__full_name', 'problem__code') + result = ( + Submission.objects.filter( + user=self.object, + points__gt=0, + problem__is_public=True, + problem__is_organization_private=False, + ) + .exclude( + problem__in=self.get_completed_problems() if self.hide_solved else [] + ) + .values( + "problem__id", + "problem__code", + "problem__name", + "problem__points", + "problem__group__full_name", + ) + .distinct() + .annotate(points=Max("points")) + .order_by("problem__group__full_name", "problem__code") + ) def process_group(group, problems_iter): problems = list(problems_iter) - points = sum(map(itemgetter('points'), problems)) - return {'name': group, 'problems': problems, 'points': points} + points = sum(map(itemgetter("points"), problems)) + return {"name": group, "problems": problems, "points": points} - context['best_submissions'] = [ - process_group(group, problems) for group, problems in itertools.groupby( - remap_keys(result, { - 'problem__code': 'code', 'problem__name': 'name', 'problem__points': 'total', - 'problem__group__full_name': 'group', - }), itemgetter('group')) + context["best_submissions"] = [ + process_group(group, problems) + for group, problems in itertools.groupby( + remap_keys( + result, + { + "problem__code": "code", + "problem__name": "name", + "problem__points": "total", + "problem__group__full_name": "group", + }, + ), + itemgetter("group"), + ) ] breakdown, has_more = get_pp_breakdown(self.object, start=0, end=10) - context['pp_breakdown'] = breakdown - context['pp_has_more'] = has_more + context["pp_breakdown"] = breakdown + context["pp_has_more"] = has_more return context class UserPerformancePointsAjax(UserProblemsPage): - template_name = 'user/pp-table-body.html' + template_name = "user/pp-table-body.html" def get_context_data(self, **kwargs): context = super(UserPerformancePointsAjax, self).get_context_data(**kwargs) try: - start = int(self.request.GET.get('start', 0)) - end = int(self.request.GET.get('end', settings.DMOJ_PP_ENTRIES)) + start = int(self.request.GET.get("start", 0)) + end = int(self.request.GET.get("end", settings.DMOJ_PP_ENTRIES)) if start < 0 or end < 0 or start > end: raise ValueError except ValueError: start, end = 0, 100 breakdown, self.has_more = get_pp_breakdown(self.object, start=start, end=end) - context['pp_breakdown'] = breakdown + context["pp_breakdown"] = breakdown return context def get(self, request, *args, **kwargs): httpresp = super(UserPerformancePointsAjax, self).get(request, *args, **kwargs) httpresp.render() - return JsonResponse({ - 'results': utf8text(httpresp.content), - 'has_more': self.has_more, - }) + return JsonResponse( + { + "results": utf8text(httpresp.content), + "has_more": self.has_more, + } + ) @login_required @@ -282,26 +383,39 @@ def edit_profile(request): profile = Profile.objects.get(user=request.user) if profile.mute: raise Http404() - if request.method == 'POST': + if request.method == "POST": form = ProfileForm(request.POST, instance=profile, user=request.user) if form.is_valid(): with transaction.atomic(), revisions.create_revision(): form.save() revisions.set_user(request.user) - revisions.set_comment(_('Updated on site')) + revisions.set_comment(_("Updated on site")) if newsletter_id is not None: try: - subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id) + subscription = Subscription.objects.get( + user=request.user, newsletter_id=newsletter_id + ) except Subscription.DoesNotExist: - if form.cleaned_data['newsletter']: - Subscription(user=request.user, newsletter_id=newsletter_id, subscribed=True).save() + if form.cleaned_data["newsletter"]: + Subscription( + user=request.user, + newsletter_id=newsletter_id, + subscribed=True, + ).save() else: - if subscription.subscribed != form.cleaned_data['newsletter']: - subscription.update(('unsubscribe', 'subscribe')[form.cleaned_data['newsletter']]) + if subscription.subscribed != form.cleaned_data["newsletter"]: + subscription.update( + ("unsubscribe", "subscribe")[ + form.cleaned_data["newsletter"] + ] + ) - perm = Permission.objects.get(codename='test_site', content_type=ContentType.objects.get_for_model(Profile)) - if form.cleaned_data['test_site']: + perm = Permission.objects.get( + codename="test_site", + content_type=ContentType.objects.get_for_model(Profile), + ) + if form.cleaned_data["test_site"]: request.user.user_permissions.add(perm) else: request.user.user_permissions.remove(perm) @@ -311,34 +425,42 @@ def edit_profile(request): form = ProfileForm(instance=profile, user=request.user) if newsletter_id is not None: try: - subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id) + subscription = Subscription.objects.get( + user=request.user, newsletter_id=newsletter_id + ) except Subscription.DoesNotExist: - form.fields['newsletter'].initial = False + form.fields["newsletter"].initial = False else: - form.fields['newsletter'].initial = subscription.subscribed - form.fields['test_site'].initial = request.user.has_perm('judge.test_site') + form.fields["newsletter"].initial = subscription.subscribed + form.fields["test_site"].initial = request.user.has_perm("judge.test_site") tzmap = settings.TIMEZONE_MAP print(settings.REGISTER_NAME_URL) - return render(request, 'user/edit-profile.html', { - 'edit_name_url': settings.REGISTER_NAME_URL, - 'require_staff_2fa': settings.DMOJ_REQUIRE_STAFF_2FA, - 'form': form, 'title': _('Edit profile'), 'profile': profile, - 'has_math_config': bool(settings.MATHOID_URL), - 'TIMEZONE_MAP': tzmap or 'http://momentjs.com/static/img/world.png', - 'TIMEZONE_BG': settings.TIMEZONE_BG if tzmap else '#4E7CAD', - }) + return render( + request, + "user/edit-profile.html", + { + "edit_name_url": settings.REGISTER_NAME_URL, + "require_staff_2fa": settings.DMOJ_REQUIRE_STAFF_2FA, + "form": form, + "title": _("Edit profile"), + "profile": profile, + "has_math_config": bool(settings.MATHOID_URL), + "TIMEZONE_MAP": tzmap or "http://momentjs.com/static/img/world.png", + "TIMEZONE_BG": settings.TIMEZONE_BG if tzmap else "#4E7CAD", + }, + ) class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView): model = Profile - title = gettext_lazy('Leaderboard') - context_object_name = 'users' - template_name = 'user/list.html' + title = gettext_lazy("Leaderboard") + context_object_name = "users" + template_name = "user/list.html" paginate_by = 100 - all_sorts = frozenset(('points', 'problem_count', 'rating', 'performance_points')) + all_sorts = frozenset(("points", "problem_count", "rating", "performance_points")) default_desc = all_sorts - default_sort = '-performance_points' + default_sort = "-performance_points" def filter_friend_queryset(self, queryset): friends = list(self.request.profile.get_friends()) @@ -346,18 +468,30 @@ class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView): return ret def get_queryset(self): - ret = Profile.objects.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user') \ - .only('display_rank', 'user__username', 'points', 'rating', 'performance_points', - 'problem_count') + ret = ( + Profile.objects.filter(is_unlisted=False) + .order_by(self.order, "id") + .select_related("user") + .only( + "display_rank", + "user__username", + "points", + "rating", + "performance_points", + "problem_count", + ) + ) - if (self.request.GET.get('friend') == 'true') and self.request.profile: + if (self.request.GET.get("friend") == "true") and self.request.profile: ret = self.filter_friend_queryset(ret) return ret def get_context_data(self, **kwargs): context = super(UserList, self).get_context_data(**kwargs) - context['users'] = ranker(context['users'], rank=self.paginate_by * (context['page_obj'].number - 1)) - context['first_page_href'] = '.' + context["users"] = ranker( + context["users"], rank=self.paginate_by * (context["page_obj"].number - 1) + ) + context["first_page_href"] = "." context.update(self.get_sort_context()) context.update(self.get_sort_paginate_context()) return context @@ -378,27 +512,36 @@ def users(request): if request.in_contest_mode: participation = request.profile.current_contest contest = participation.contest - return FixedContestRanking.as_view(contest=contest)(request, contest=contest.key) + return FixedContestRanking.as_view(contest=contest)( + request, contest=contest.key + ) return user_list_view(request) def user_ranking_redirect(request): try: - username = request.GET['handle'] + username = request.GET["handle"] except KeyError: raise Http404() user = get_object_or_404(Profile, user__username=username) - rank = Profile.objects.filter(is_unlisted=False, performance_points__gt=user.performance_points).count() + rank = Profile.objects.filter( + is_unlisted=False, performance_points__gt=user.performance_points + ).count() rank += Profile.objects.filter( - is_unlisted=False, performance_points__exact=user.performance_points, id__lt=user.id, + is_unlisted=False, + performance_points__exact=user.performance_points, + id__lt=user.id, ).count() page = rank // UserList.paginate_by - return HttpResponseRedirect('%s%s#!%s' % (reverse('user_list'), '?page=%d' % (page + 1) if page else '', username)) + return HttpResponseRedirect( + "%s%s#!%s" + % (reverse("user_list"), "?page=%d" % (page + 1) if page else "", username) + ) class UserLogoutView(TitleMixin, TemplateView): - template_name = 'registration/logout.html' - title = 'You have been successfully logged out.' + template_name = "registration/logout.html" + title = "You have been successfully logged out." def post(self, request, *args, **kwargs): auth_logout(request) @@ -406,8 +549,8 @@ class UserLogoutView(TitleMixin, TemplateView): class ImportUsersView(TitleMixin, TemplateView): - template_name = 'user/import/index.html' - title = _('Import Users') + template_name = "user/import/index.html" + title = _("Import Users") def get(self, *args, **kwargs): if self.request.user.is_superuser: @@ -416,43 +559,38 @@ class ImportUsersView(TitleMixin, TemplateView): def import_users_post_file(request): - if not request.user.is_superuser or request.method != 'POST': + if not request.user.is_superuser or request.method != "POST": return HttpResponseForbidden() - users = import_users.csv_to_dict(request.FILES['csv_file']) + users = import_users.csv_to_dict(request.FILES["csv_file"]) if not users: - return JsonResponse({ - 'done': False, - 'msg': 'No valid row found. Make sure row containing username.' - }) - - table_html = render_to_string('user/import/table_csv.html', { - 'data': users - }) - return JsonResponse({ - 'done': True, - 'html': table_html, - 'data': users - }) - + return JsonResponse( + { + "done": False, + "msg": "No valid row found. Make sure row containing username.", + } + ) + + table_html = render_to_string("user/import/table_csv.html", {"data": users}) + return JsonResponse({"done": True, "html": table_html, "data": users}) + def import_users_submit(request): import json - if not request.user.is_superuser or request.method != 'POST': + + if not request.user.is_superuser or request.method != "POST": return HttpResponseForbidden() - users = json.loads(request.body)['users'] + users = json.loads(request.body)["users"] log = import_users.import_users(users) - return JsonResponse({ - 'msg': log - }) + return JsonResponse({"msg": log}) def sample_import_users(request): - if not request.user.is_superuser or request.method != 'GET': + if not request.user.is_superuser or request.method != "GET": return HttpResponseForbidden() - filename = 'import_sample.csv' - content = ','.join(import_users.fields) + '\n' + ','.join(import_users.descriptions) - response = HttpResponse(content, content_type='text/plain') - response['Content-Disposition'] = 'attachment; filename={0}'.format(filename) - return response \ No newline at end of file + filename = "import_sample.csv" + content = ",".join(import_users.fields) + "\n" + ",".join(import_users.descriptions) + response = HttpResponse(content, content_type="text/plain") + response["Content-Disposition"] = "attachment; filename={0}".format(filename) + return response diff --git a/judge/views/volunteer.py b/judge/views/volunteer.py index 28c9d80..64c58f9 100644 --- a/judge/views/volunteer.py +++ b/judge/views/volunteer.py @@ -5,17 +5,17 @@ from judge.models import VolunteerProblemVote, Problem, ProblemType def vote_problem(request): - if not request.user or not request.user.has_perm('judge.suggest_problem_changes'): + if not request.user or not request.user.has_perm("judge.suggest_problem_changes"): return HttpResponseBadRequest() - if not request.method == 'POST': + if not request.method == "POST": return HttpResponseBadRequest() try: - types_id = request.POST.getlist('types[]') + types_id = request.POST.getlist("types[]") types = ProblemType.objects.filter(id__in=types_id) - problem = Problem.objects.get(code=request.POST['problem']) - knowledge_points = request.POST['knowledge_points'] - thinking_points = request.POST['thinking_points'] - feedback = request.POST['feedback'] + problem = Problem.objects.get(code=request.POST["problem"]) + knowledge_points = request.POST["knowledge_points"] + thinking_points = request.POST["thinking_points"] + feedback = request.POST["feedback"] except Exception as e: return HttpResponseBadRequest() @@ -23,7 +23,7 @@ def vote_problem(request): vote, _ = VolunteerProblemVote.objects.get_or_create( voter=request.profile, problem=problem, - defaults={'knowledge_points': 0, 'thinking_points': 0}, + defaults={"knowledge_points": 0, "thinking_points": 0}, ) vote.knowledge_points = knowledge_points vote.thinking_points = thinking_points diff --git a/judge/views/widgets.py b/judge/views/widgets.py index 57fe15b..f746704 100644 --- a/judge/views/widgets.py +++ b/judge/views/widgets.py @@ -2,60 +2,90 @@ import requests from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.exceptions import ImproperlyConfigured -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseForbidden, + HttpResponseRedirect, +) from django.utils.translation import gettext as _ from django.views.generic import View from judge.models import Submission -__all__ = ['rejudge_submission', 'DetectTimezone'] +__all__ = ["rejudge_submission", "DetectTimezone"] @login_required def rejudge_submission(request): - if request.method != 'POST' or not request.user.has_perm('judge.rejudge_submission') or \ - not request.user.has_perm('judge.edit_own_problem'): + if ( + request.method != "POST" + or not request.user.has_perm("judge.rejudge_submission") + or not request.user.has_perm("judge.edit_own_problem") + ): return HttpResponseForbidden() - if 'id' not in request.POST or not request.POST['id'].isdigit(): + if "id" not in request.POST or not request.POST["id"].isdigit(): return HttpResponseBadRequest() try: - submission = Submission.objects.get(id=request.POST['id']) + submission = Submission.objects.get(id=request.POST["id"]) except Submission.DoesNotExist: return HttpResponseBadRequest() - if not request.user.has_perm('judge.edit_all_problem') and \ - not submission.problem.is_editor(request.profile): + if not request.user.has_perm( + "judge.edit_all_problem" + ) and not submission.problem.is_editor(request.profile): return HttpResponseForbidden() submission.judge(rejudge=True) - redirect = request.POST.get('path', None) + redirect = request.POST.get("path", None) - return HttpResponseRedirect(redirect) if redirect else HttpResponse('success', content_type='text/plain') + return ( + HttpResponseRedirect(redirect) + if redirect + else HttpResponse("success", content_type="text/plain") + ) class DetectTimezone(View): def askgeo(self, lat, long): - if not hasattr(settings, 'ASKGEO_ACCOUNT_ID') or not hasattr(settings, 'ASKGEO_ACCOUNT_API_KEY'): + if not hasattr(settings, "ASKGEO_ACCOUNT_ID") or not hasattr( + settings, "ASKGEO_ACCOUNT_API_KEY" + ): raise ImproperlyConfigured() - data = requests.get('http://api.askgeo.com/v1/%s/%s/query.json?databases=TimeZone&points=%f,%f' % - (settings.ASKGEO_ACCOUNT_ID, settings.ASKGEO_ACCOUNT_API_KEY, lat, long)).json() + data = requests.get( + "http://api.askgeo.com/v1/%s/%s/query.json?databases=TimeZone&points=%f,%f" + % (settings.ASKGEO_ACCOUNT_ID, settings.ASKGEO_ACCOUNT_API_KEY, lat, long) + ).json() try: - return HttpResponse(data['data'][0]['TimeZone']['TimeZoneId'], content_type='text/plain') + return HttpResponse( + data["data"][0]["TimeZone"]["TimeZoneId"], content_type="text/plain" + ) except (IndexError, KeyError): - return HttpResponse(_('Invalid upstream data: %s') % data, content_type='text/plain', status=500) + return HttpResponse( + _("Invalid upstream data: %s") % data, + content_type="text/plain", + status=500, + ) def geonames(self, lat, long): - if not hasattr(settings, 'GEONAMES_USERNAME'): + if not hasattr(settings, "GEONAMES_USERNAME"): raise ImproperlyConfigured() - data = requests.get('http://api.geonames.org/timezoneJSON?lat=%f&lng=%f&username=%s' % - (lat, long, settings.GEONAMES_USERNAME)).json() + data = requests.get( + "http://api.geonames.org/timezoneJSON?lat=%f&lng=%f&username=%s" + % (lat, long, settings.GEONAMES_USERNAME) + ).json() try: - return HttpResponse(data['timezoneId'], content_type='text/plain') + return HttpResponse(data["timezoneId"], content_type="text/plain") except KeyError: - return HttpResponse(_('Invalid upstream data: %s') % data, content_type='text/plain', status=500) + return HttpResponse( + _("Invalid upstream data: %s") % data, + content_type="text/plain", + status=500, + ) def default(self, lat, long): raise Http404() @@ -63,10 +93,11 @@ class DetectTimezone(View): def get(self, request, *args, **kwargs): backend = settings.TIMEZONE_DETECT_BACKEND try: - lat, long = float(request.GET['lat']), float(request.GET['long']) + lat, long = float(request.GET["lat"]), float(request.GET["long"]) except (ValueError, KeyError): - return HttpResponse(_('Bad latitude or longitude'), content_type='text/plain', status=404) - return { - 'askgeo': self.askgeo, - 'geonames': self.geonames, - }.get(backend, self.default)(lat, long) + return HttpResponse( + _("Bad latitude or longitude"), content_type="text/plain", status=404 + ) + return {"askgeo": self.askgeo, "geonames": self.geonames,}.get( + backend, self.default + )(lat, long) diff --git a/judge/widgets/checkbox.py b/judge/widgets/checkbox.py index 0a7eebf..b4b2249 100644 --- a/judge/widgets/checkbox.py +++ b/judge/widgets/checkbox.py @@ -6,17 +6,25 @@ from django.utils.safestring import mark_safe class CheckboxSelectMultipleWithSelectAll(forms.CheckboxSelectMultiple): def render(self, name, value, attrs=None, renderer=None): - if 'id' not in attrs: - raise FieldError('id required') + if "id" not in attrs: + raise FieldError("id required") - select_all_id = attrs['id'] + '_all' - select_all_name = name + '_all' - original = super(CheckboxSelectMultipleWithSelectAll, self).render(name, value, attrs, renderer) - template = get_template('widgets/select_all.html') - return mark_safe(template.render({ - 'original_widget': original, - 'select_all_id': select_all_id, - 'select_all_name': select_all_name, - 'all_selected': all(choice[0] in value for choice in self.choices) if value else False, - 'empty': not self.choices, - })) + select_all_id = attrs["id"] + "_all" + select_all_name = name + "_all" + original = super(CheckboxSelectMultipleWithSelectAll, self).render( + name, value, attrs, renderer + ) + template = get_template("widgets/select_all.html") + return mark_safe( + template.render( + { + "original_widget": original, + "select_all_id": select_all_id, + "select_all_name": select_all_name, + "all_selected": all(choice[0] in value for choice in self.choices) + if value + else False, + "empty": not self.choices, + } + ) + ) diff --git a/judge/widgets/mixins.py b/judge/widgets/mixins.py index 0270276..6317bac 100644 --- a/judge/widgets/mixins.py +++ b/judge/widgets/mixins.py @@ -7,23 +7,27 @@ from lxml import html class CompressorWidgetMixin(object): - __template_css = dedent('''\ + __template_css = dedent( + """\ {% compress css %} {{ media.css }} {% endcompress %} - ''') + """ + ) - __template_js = dedent('''\ + __template_js = dedent( + """\ {% compress js %} {{ media.js }} {% endcompress %} - ''') + """ + ) __templates = { - (False, False): Template(''), - (True, False): Template('{% load compress %}' + __template_css), - (False, True): Template('{% load compress %}' + __template_js), - (True, True): Template('{% load compress %}' + __template_js + __template_css), + (False, False): Template(""), + (True, False): Template("{% load compress %}" + __template_css), + (False, True): Template("{% load compress %}" + __template_js), + (True, True): Template("{% load compress %}" + __template_js + __template_css), } compress_css = False @@ -34,14 +38,19 @@ class CompressorWidgetMixin(object): except ImportError: pass else: - if getattr(settings, 'COMPRESS_ENABLED', not settings.DEBUG): + if getattr(settings, "COMPRESS_ENABLED", not settings.DEBUG): + @property def media(self): media = super().media template = self.__templates[self.compress_css, self.compress_js] - result = html.fromstring(template.render(Context({'media': media}))) + result = html.fromstring(template.render(Context({"media": media}))) return forms.Media( - css={'all': [result.find('.//link').get('href')]} if self.compress_css else media._css, - js=[result.find('.//script').get('src')] if self.compress_js else media._js, + css={"all": [result.find(".//link").get("href")]} + if self.compress_css + else media._css, + js=[result.find(".//script").get("src")] + if self.compress_js + else media._js, ) diff --git a/judge/widgets/pagedown.py b/judge/widgets/pagedown.py index 0968107..3225dc7 100644 --- a/judge/widgets/pagedown.py +++ b/judge/widgets/pagedown.py @@ -6,9 +6,14 @@ from django.utils.html import conditional_escape from judge.widgets.mixins import CompressorWidgetMixin -__all__ = ['PagedownWidget', 'AdminPagedownWidget', - 'MathJaxPagedownWidget', 'MathJaxAdminPagedownWidget', - 'HeavyPreviewPageDownWidget', 'HeavyPreviewAdminPageDownWidget'] +__all__ = [ + "PagedownWidget", + "AdminPagedownWidget", + "MathJaxPagedownWidget", + "MathJaxAdminPagedownWidget", + "HeavyPreviewPageDownWidget", + "HeavyPreviewAdminPageDownWidget", +] try: from pagedown.widgets import PagedownWidget as OldPagedownWidget @@ -20,6 +25,7 @@ except ImportError: HeavyPreviewPageDownWidget = None HeavyPreviewAdminPageDownWidget = None else: + class PagedownWidget(CompressorWidgetMixin, OldPagedownWidget): # The goal here is to compress all the pagedown JS into one file. # We do not want any further compress down the chain, because @@ -28,23 +34,25 @@ else: compress_js = True def __init__(self, *args, **kwargs): - kwargs.setdefault('css', ('pagedown_widget.css',)) + kwargs.setdefault("css", ("pagedown_widget.css",)) super(PagedownWidget, self).__init__(*args, **kwargs) class AdminPagedownWidget(PagedownWidget, admin_widgets.AdminTextareaWidget): class Media: - css = {'all': [ - 'content-description.css', - 'admin/css/pagedown.css', - ]} - js = ['admin/js/pagedown.js'] + css = { + "all": [ + "content-description.css", + "admin/css/pagedown.css", + ] + } + js = ["admin/js/pagedown.js"] class MathJaxPagedownWidget(PagedownWidget): class Media: js = [ - 'mathjax_config.js', - 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML', - 'pagedown_math.js', + "mathjax_config.js", + "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML", + "pagedown_math.js", ] class MathJaxAdminPagedownWidget(AdminPagedownWidget, MathJaxPagedownWidget): @@ -52,40 +60,46 @@ else: class HeavyPreviewPageDownWidget(PagedownWidget): def __init__(self, *args, **kwargs): - kwargs.setdefault('template', 'pagedown.html') - self.preview_url = kwargs.pop('preview') - self.preview_timeout = kwargs.pop('preview_timeout', None) - self.hide_preview_button = kwargs.pop('hide_preview_button', False) + kwargs.setdefault("template", "pagedown.html") + self.preview_url = kwargs.pop("preview") + self.preview_timeout = kwargs.pop("preview_timeout", None) + self.hide_preview_button = kwargs.pop("hide_preview_button", False) super(HeavyPreviewPageDownWidget, self).__init__(*args, **kwargs) def render(self, name, value, attrs=None, renderer=None): if value is None: - value = '' - final_attrs = self.build_attrs(attrs, {'name': name}) - if 'class' not in final_attrs: - final_attrs['class'] = '' - final_attrs['class'] += ' wmd-input' - return get_template(self.template).render(self.get_template_context(final_attrs, value)) + value = "" + final_attrs = self.build_attrs(attrs, {"name": name}) + if "class" not in final_attrs: + final_attrs["class"] = "" + final_attrs["class"] += " wmd-input" + return get_template(self.template).render( + self.get_template_context(final_attrs, value) + ) def get_template_context(self, attrs, value): return { - 'attrs': flatatt(attrs), - 'body': conditional_escape(force_text(value)), - 'id': attrs['id'], - 'show_preview': self.show_preview, - 'preview_url': self.preview_url, - 'preview_timeout': self.preview_timeout, - 'extra_classes': 'dmmd-no-button' if self.hide_preview_button else None, + "attrs": flatatt(attrs), + "body": conditional_escape(force_text(value)), + "id": attrs["id"], + "show_preview": self.show_preview, + "preview_url": self.preview_url, + "preview_timeout": self.preview_timeout, + "extra_classes": "dmmd-no-button" if self.hide_preview_button else None, } class Media: - css = {'all': ['dmmd-preview.css']} - js = ['dmmd-preview.js'] + css = {"all": ["dmmd-preview.css"]} + js = ["dmmd-preview.js"] - class HeavyPreviewAdminPageDownWidget(AdminPagedownWidget, HeavyPreviewPageDownWidget): + class HeavyPreviewAdminPageDownWidget( + AdminPagedownWidget, HeavyPreviewPageDownWidget + ): class Media: - css = {'all': [ - 'pygment-github.css', - 'table.css', - 'ranks.css', - ]} + css = { + "all": [ + "pygment-github.css", + "table.css", + "ranks.css", + ] + } diff --git a/judge/widgets/select2.py b/judge/widgets/select2.py index af4843b..6a7db7f 100644 --- a/judge/widgets/select2.py +++ b/judge/widgets/select2.py @@ -46,13 +46,23 @@ from django.core import signing from django.forms.models import ModelChoiceIterator from django.urls import reverse_lazy -DEFAULT_SELECT2_JS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js' -DEFAULT_SELECT2_CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css' +DEFAULT_SELECT2_JS = "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js" +DEFAULT_SELECT2_CSS = ( + "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css" +) -__all__ = ['Select2Widget', 'Select2MultipleWidget', 'Select2TagWidget', - 'HeavySelect2Widget', 'HeavySelect2MultipleWidget', 'HeavySelect2TagWidget', - 'AdminSelect2Widget', 'AdminSelect2MultipleWidget', 'AdminHeavySelect2Widget', - 'AdminHeavySelect2MultipleWidget'] +__all__ = [ + "Select2Widget", + "Select2MultipleWidget", + "Select2TagWidget", + "HeavySelect2Widget", + "HeavySelect2MultipleWidget", + "HeavySelect2TagWidget", + "AdminSelect2Widget", + "AdminSelect2MultipleWidget", + "AdminHeavySelect2Widget", + "AdminHeavySelect2MultipleWidget", +] class Select2Mixin(object): @@ -68,22 +78,22 @@ class Select2Mixin(object): """Add select2 data attributes.""" attrs = super(Select2Mixin, self).build_attrs(base_attrs, extra_attrs) if self.is_required: - attrs.setdefault('data-allow-clear', 'false') + attrs.setdefault("data-allow-clear", "false") else: - attrs.setdefault('data-allow-clear', 'true') - attrs.setdefault('data-placeholder', '') + attrs.setdefault("data-allow-clear", "true") + attrs.setdefault("data-placeholder", "") - attrs.setdefault('data-minimum-input-length', 0) - if 'class' in attrs: - attrs['class'] += ' django-select2' + attrs.setdefault("data-minimum-input-length", 0) + if "class" in attrs: + attrs["class"] += " django-select2" else: - attrs['class'] = 'django-select2' + attrs["class"] = "django-select2" return attrs def optgroups(self, name, value, attrs=None): """Add empty option for clearable selects.""" if not self.is_required and not self.allow_multiple_selected: - self.choices = list(chain([('', '')], self.choices)) + self.choices = list(chain([("", "")], self.choices)) return super(Select2Mixin, self).optgroups(name, value, attrs=attrs) @property @@ -95,8 +105,8 @@ class Select2Mixin(object): https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property """ return forms.Media( - js=[settings.SELECT2_JS_URL, 'django_select2.js'], - css={'screen': [settings.SELECT2_CSS_URL]}, + js=[settings.SELECT2_JS_URL, "django_select2.js"], + css={"screen": [settings.SELECT2_CSS_URL]}, ) @@ -104,8 +114,12 @@ class AdminSelect2Mixin(Select2Mixin): @property def media(self): return forms.Media( - js=['admin/js/jquery.init.js', settings.SELECT2_JS_URL, 'django_select2.js'], - css={'screen': [settings.SELECT2_CSS_URL]}, + js=[ + "admin/js/jquery.init.js", + settings.SELECT2_JS_URL, + "django_select2.js", + ], + css={"screen": [settings.SELECT2_CSS_URL]}, ) @@ -115,9 +129,9 @@ class Select2TagMixin(object): def build_attrs(self, base_attrs, extra_attrs=None): """Add select2's tag attributes.""" extra_attrs = extra_attrs or {} - extra_attrs.setdefault('data-minimum-input-length', 1) - extra_attrs.setdefault('data-tags', 'true') - extra_attrs.setdefault('data-token-separators', [",", " "]) + extra_attrs.setdefault("data-minimum-input-length", 1) + extra_attrs.setdefault("data-tags", "true") + extra_attrs.setdefault("data-token-separators", [",", " "]) return super(Select2TagMixin, self).build_attrs(base_attrs, extra_attrs) @@ -182,8 +196,8 @@ class HeavySelect2Mixin(Select2Mixin): else: self.attrs = {} - self.data_view = kwargs.pop('data_view', None) - self.data_url = kwargs.pop('data_url', None) + self.data_view = kwargs.pop("data_view", None) + self.data_url = kwargs.pop("data_url", None) if not (self.data_view or self.data_url): raise ValueError('You must ether specify "data_view" or "data_url".') @@ -201,22 +215,22 @@ class HeavySelect2Mixin(Select2Mixin): # encrypt instance Id self.widget_id = signing.dumps(id(self)) - attrs['data-field_id'] = self.widget_id - attrs.setdefault('data-ajax--url', self.get_url()) - attrs.setdefault('data-ajax--cache', "true") - attrs.setdefault('data-ajax--type', "GET") - attrs.setdefault('data-minimum-input-length', 2) + attrs["data-field_id"] = self.widget_id + attrs.setdefault("data-ajax--url", self.get_url()) + attrs.setdefault("data-ajax--cache", "true") + attrs.setdefault("data-ajax--type", "GET") + attrs.setdefault("data-minimum-input-length", 2) - attrs['class'] += ' django-select2-heavy' + attrs["class"] += " django-select2-heavy" return attrs def format_value(self, value): result = super(HeavySelect2Mixin, self).format_value(value) if isinstance(self.choices, ModelChoiceIterator): chosen = copy(self.choices) - chosen.queryset = chosen.queryset.filter(pk__in=[ - int(i) for i in result if isinstance(i, int) or i.isdigit() - ]) + chosen.queryset = chosen.queryset.filter( + pk__in=[int(i) for i in result if isinstance(i, int) or i.isdigit()] + ) self.choices = set(chosen) return result diff --git a/manage.py b/manage.py index 498e387..f0470c1 100644 --- a/manage.py +++ b/manage.py @@ -11,6 +11,7 @@ if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") from django.core.management import execute_from_command_line + # noinspection PyUnresolvedReferences import django_2_2_pymysql_patch # noqa: F401, imported for side effect diff --git a/validator_template/template.py b/validator_template/template.py index 9144f70..af55fb5 100644 --- a/validator_template/template.py +++ b/validator_template/template.py @@ -4,7 +4,11 @@ import subprocess from dmoj.error import InternalError from dmoj.judgeenv import env, get_problem_root from dmoj.result import CheckerResult -from dmoj.utils.helper_files import compile_with_auxiliary_files, mktemp, parse_helper_file_error +from dmoj.utils.helper_files import ( + compile_with_auxiliary_files, + mktemp, + parse_helper_file_error, +) from dmoj.utils.unicode import utf8text executor = None @@ -17,60 +21,93 @@ def get_executor(files, lang, compiler_time_limit, problem_id): if not isinstance(files, list): files = [files] filenames = [os.path.join(get_problem_root(problem_id), f) for f in files] - executor = compile_with_auxiliary_files(filenames, - compiler_time_limit=compiler_time_limit) + executor = compile_with_auxiliary_files( + filenames, compiler_time_limit=compiler_time_limit + ) return executor + class Module: AC = 0 WA = 1 PARTIAL = 2 # a float from 0 to 1 - repartial = re.compile(r'^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$', re.M) + repartial = re.compile(r"^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$", re.M) @classmethod - def parse_return_code(cls, proc, executor, point_value, time_limit, memory_limit, feedback, name, stderr): + def parse_return_code( + cls, + proc, + executor, + point_value, + time_limit, + memory_limit, + feedback, + name, + stderr, + ): if proc.returncode == cls.AC: return CheckerResult(True, point_value, feedback=feedback) elif proc.returncode == cls.PARTIAL: match = cls.repartial.search(stderr) if not match: - raise InternalError('Invalid stderr for partial points: %r' % stderr) + raise InternalError("Invalid stderr for partial points: %r" % stderr) points = float(match.group(0)) if not 0 <= points <= 1: - raise InternalError('Invalid partial points: %d' % points) - - ac = (points == 1) + raise InternalError("Invalid partial points: %d" % points) + + ac = points == 1 return CheckerResult(ac, points * point_value, feedback=feedback) elif proc.returncode == cls.WA: return CheckerResult(False, 0, feedback=feedback) else: - parse_helper_file_error(proc, executor, name, stderr, time_limit, memory_limit) + parse_helper_file_error( + proc, executor, name, stderr, time_limit, memory_limit + ) -def check(process_output, judge_output, judge_input, - problem_id={{problemid}}, - files={{filecpp}}, - lang='CPP14', - time_limit=10, - memory_limit=1024 * 512, - compiler_time_limit=30, - feedback=True, - point_value=None, **kwargs) -> CheckerResult: +def check( + process_output, + judge_output, + judge_input, + problem_id={{problemid}}, + files={{filecpp}}, + lang="CPP14", + time_limit=10, + memory_limit=1024 * 512, + compiler_time_limit=30, + feedback=True, + point_value=None, + **kwargs +) -> CheckerResult: executor = get_executor(files, lang, compiler_time_limit, problem_id) - with mktemp(judge_input) as input_file, mktemp(process_output) as output_file, mktemp(judge_output) as judge_file: - try: - process = executor.launch(input_file.name, output_file.name, judge_file.name, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, memory=memory_limit, time=time_limit) + with mktemp(judge_input) as input_file, mktemp( + process_output + ) as output_file, mktemp(judge_output) as judge_file: + try: + process = executor.launch( + input_file.name, + output_file.name, + judge_file.name, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + memory=memory_limit, + time=time_limit, + ) proc_output, error = map(utf8text, process.communicate()) except Exception as err: - raise InternalError('Error while running checker: %r', err) + raise InternalError("Error while running checker: %r", err) - return Module.parse_return_code(process, executor, point_value, time_limit, - memory_limit, - feedback=utf8text(proc_output) - if feedback else None, name='checker', - stderr=error) \ No newline at end of file + return Module.parse_return_code( + process, + executor, + point_value, + time_limit, + memory_limit, + feedback=utf8text(proc_output) if feedback else None, + name="checker", + stderr=error, + ) From 153360755193bc0cba9af8ddeac4f2d90e55c69b Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 14 May 2022 13:22:35 -0500 Subject: [PATCH 054/959] Fix trans --- locale/vi/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index f9d68d3..1e853b8 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -3766,7 +3766,7 @@ msgid "" "%(time_limit)s window between %(start_time)s and " "%(end_time)s" msgstr "" -"Một thời gian dài %(time_limit)s trong khoản %(start_time)s từ " +"Kéo dài %(time_limit)s từ %(start_time)s đến " "%(end_time)s" #: templates/contest/contest.html:63 From 6047d292dcb9c5ea4c801b4255baff010f1c4625 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 17 May 2022 22:34:08 -0500 Subject: [PATCH 055/959] Add notif for comment page author --- judge/comments.py | 16 ++++++++++++++-- judge/models/comment.py | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/judge/comments.py b/judge/comments.py index 2b7579a..fdd0a81 100644 --- a/judge/comments.py +++ b/judge/comments.py @@ -131,10 +131,22 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): # add notification for reply if comment.parent and comment.parent.author != comment.author: - notification_rep = Notification( + notification_reply = Notification( owner=comment.parent.author, comment=comment, category="Reply" ) - notification_rep.save() + notification_reply.save() + + # add notification for page authors + page_authors = comment.page_object.authors.all() + for user in page_authors: + if user == comment.author: + continue + notification = Notification( + owner=user, comment=comment, category="Comment" + ) + notification.save() + # except Exception: + # pass add_mention_notifications(comment) diff --git a/judge/models/comment.py b/judge/models/comment.py index a4ffc57..c03df7e 100644 --- a/judge/models/comment.py +++ b/judge/models/comment.py @@ -15,7 +15,7 @@ from reversion.models import Version from judge.models.contest import Contest from judge.models.interface import BlogPost -from judge.models.problem import Problem +from judge.models.problem import Problem, Solution from judge.models.profile import Profile from judge.utils.cachedict import CacheDict @@ -171,6 +171,22 @@ class Comment(MPTTModel): def get_absolute_url(self): return "%s#comment-%d" % (self.link, self.id) + @cached_property + def page_object(self): + try: + page = self.page + if page.startswith("p:"): + return Problem.objects.get(code=page[2:]) + elif page.startswith("c:"): + return Contest.objects.get(key=page[2:]) + elif page.startswith("b:"): + return BlogPost.objects.get(id=page[2:]) + elif page.startswith("s:"): + return Solution.objects.get(problem__code=page[2:]) + return None + except ObjectDoesNotExist: + return None + def __str__(self): return "%(page)s by %(user)s" % { "page": self.page, From c0e27205ac83d4eb9cae8fe62d385a65e0f2e7ef Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 18 May 2022 12:34:21 -0500 Subject: [PATCH 056/959] Add console print when ws close --- templates/chat/chat.html | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/templates/chat/chat.html b/templates/chat/chat.html index e1fb66d..9897d71 100644 --- a/templates/chat/chat.html +++ b/templates/chat/chat.html @@ -98,7 +98,7 @@ let META_HEADER = [ })} window.load_dynamic_update = function (last_msg) { - return new EventReceiver( + var receiver = EventReceiver( "{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}", ['chat_lobby', 'chat_{{request.profile.id}}'], last_msg, function (message) { var room = (message.type == 'lobby') ? '' : message.room; @@ -110,6 +110,15 @@ let META_HEADER = [ } } ); + receiver.onwsclose = function (event) { + if (event.code == 1001) { + console.log('Navigated away'); + return; + } + console.log('You probably should refresh?'); + }; + + return receiver; } function refresh_status() { From cce42995b9f3cb38ed1c82cd9d302d8d739dd26e Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 18 May 2022 12:51:29 -0500 Subject: [PATCH 057/959] Fix previous bug --- templates/chat/chat.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/templates/chat/chat.html b/templates/chat/chat.html index 9897d71..3a1d631 100644 --- a/templates/chat/chat.html +++ b/templates/chat/chat.html @@ -98,7 +98,7 @@ let META_HEADER = [ })} window.load_dynamic_update = function (last_msg) { - var receiver = EventReceiver( + var receiver = new EventReceiver( "{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}", ['chat_lobby', 'chat_{{request.profile.id}}'], last_msg, function (message) { var room = (message.type == 'lobby') ? '' : message.room; @@ -208,9 +208,12 @@ let META_HEADER = [ if ($('#message-'+tmp_id).length) { $('#message-'+tmp_id).replaceWith(data); } - else { + else if ($('#body-block-'+tmp_id).length) { $('#body-block-'+tmp_id).replaceWith($body_block); } + else { + add_new_message(message, room); + } resize_emoji($body_block); MathJax.Hub.Queue(["Typeset",MathJax.Hub]); register_time($('.time-with-rel')); From 088d78d762f80920266698d9b258e00dd106106b Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 18 May 2022 13:39:20 -0500 Subject: [PATCH 058/959] Try fixing ws --- resources/event.js | 5 +++++ templates/chat/chat.html | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/event.js b/resources/event.js index 170940b..46583ec 100644 --- a/resources/event.js +++ b/resources/event.js @@ -60,6 +60,11 @@ function EventReceiver(websocket, poller, channels, last_msg, onmessage) { this.websocket.onclose = function (event) { if (event.code != 1000 && receiver.onwsclose !== null) receiver.onwsclose(event); + if (event.code == 1006) { + setTimeout(function() { + connect(); + }, 1000); + } } } else { this.websocket = null; diff --git a/templates/chat/chat.html b/templates/chat/chat.html index 3a1d631..f9cd626 100644 --- a/templates/chat/chat.html +++ b/templates/chat/chat.html @@ -115,7 +115,7 @@ let META_HEADER = [ console.log('Navigated away'); return; } - console.log('You probably should refresh?'); + console.log('Refreshing ws'); }; return receiver; From ac0b7afc1a57fa65bcb36811de94bcaf4c518b6d Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 18 May 2022 13:50:18 -0500 Subject: [PATCH 059/959] Try fixing ws --- resources/event.js | 59 +++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/resources/event.js b/resources/event.js index 46583ec..e9beb68 100644 --- a/resources/event.js +++ b/resources/event.js @@ -35,37 +35,38 @@ function EventReceiver(websocket, poller, channels, last_msg, onmessage) { this.onwsclose = null; if (window.WebSocket) { - this.websocket = new WebSocket(websocket); - var timeout = setTimeout(function () { - receiver.websocket.close(); - receiver.websocket = null; - init_poll(); - }, 2000); - this.websocket.onopen = function (event) { - clearTimeout(timeout); - this.send(JSON.stringify({ - command: 'start-msg', - start: last_msg - })); - this.send(JSON.stringify({ - command: 'set-filter', - filter: channels - })); - }; - this.websocket.onmessage = function (event) { - var data = JSON.parse(event.data); - receiver.onmessage(data.message); - receiver.last_msg = data.id; - }; - this.websocket.onclose = function (event) { - if (event.code != 1000 && receiver.onwsclose !== null) - receiver.onwsclose(event); - if (event.code == 1006) { - setTimeout(function() { - connect(); - }, 1000); + function connect() { + this.websocket = new WebSocket(websocket); + var timeout = setTimeout(function () { + receiver.websocket.close(); + receiver.websocket = null; + init_poll(); + }, 2000); + this.websocket.onopen = function (event) { + clearTimeout(timeout); + this.send(JSON.stringify({ + command: 'start-msg', + start: last_msg + })); + this.send(JSON.stringify({ + command: 'set-filter', + filter: channels + })); + }; + this.websocket.onmessage = function (event) { + var data = JSON.parse(event.data); + receiver.onmessage(data.message); + receiver.last_msg = data.id; + }; + this.websocket.onclose = function (event) { + if (event.code != 1000 && receiver.onwsclose !== null) + receiver.onwsclose(event); + if (event.code == 1006) { + setTimeout(connect, 1000); + } } } + connect(); } else { this.websocket = null; init_poll(); From 300572d6dac18fe40fd1a0b942d3ee0403a7f416 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 18 May 2022 14:02:23 -0500 Subject: [PATCH 060/959] Remove onwsclose in templates --- templates/chat/chat.html | 7 ------- templates/submission/list.html | 10 ---------- 2 files changed, 17 deletions(-) diff --git a/templates/chat/chat.html b/templates/chat/chat.html index f9cd626..a51bbbb 100644 --- a/templates/chat/chat.html +++ b/templates/chat/chat.html @@ -110,13 +110,6 @@ let META_HEADER = [ } } ); - receiver.onwsclose = function (event) { - if (event.code == 1001) { - console.log('Navigated away'); - return; - } - console.log('Refreshing ws'); - }; return receiver; } diff --git a/templates/submission/list.html b/templates/submission/list.html index e2afa8d..d5b25e4 100644 --- a/templates/submission/list.html +++ b/templates/submission/list.html @@ -231,16 +231,6 @@ } } ); - receiver.onwsclose = function (event) { - if (event.code == 1001) { - console.log('Navigated away'); - return; - } - console.log('You probably should refresh?'); - $('.ws-closed').show().find('a').click(function () { - window.location.reload(); - }); - }; return receiver; } From 0b2c410fe58d08419720e138249c4854214a2570 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 18 May 2022 14:10:23 -0500 Subject: [PATCH 061/959] Add debug to submission list --- templates/submission/list.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/templates/submission/list.html b/templates/submission/list.html index d5b25e4..48f0aac 100644 --- a/templates/submission/list.html +++ b/templates/submission/list.html @@ -231,6 +231,16 @@ } } ); + receiver.onwsclose = function (event) { + if (event.code == 1001) { + console.log('Navigated away'); + return; + } + console.log('You probably should refresh?'); + // $('.ws-closed').show().find('a').click(function () { + // window.location.reload(); + // }); + }; return receiver; } From f79f5a845517b8bbadf327d3b98d2a355509593b Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 21 May 2022 20:30:44 -0500 Subject: [PATCH 062/959] Add last unsolved --- judge/utils/problems.py | 16 +- judge/views/problem.py | 23 +- locale/vi/LC_MESSAGES/django.po | 1482 +++++++++++++------------ templates/problem/list.html | 34 +- templates/problem/recent-attempt.html | 20 + templates/problem/search-form.html | 10 + 6 files changed, 818 insertions(+), 767 deletions(-) create mode 100644 templates/problem/recent-attempt.html diff --git a/judge/utils/problems.py b/judge/utils/problems.py index 73d53d0..d20e542 100644 --- a/judge/utils/problems.py +++ b/judge/utils/problems.py @@ -88,11 +88,19 @@ def user_attempted_ids(profile): result = cache.get(key) if result is None: result = { - id: {"achieved_points": points, "max_points": max_points} - for id, max_points, points in ( + id: { + "achieved_points": points, + "max_points": max_points, + "last_submission": last_submission, + "code": problem_code, + "name": problem_name, + } + for id, max_points, problem_code, problem_name, points, last_submission in ( Submission.objects.filter(user=profile) - .values_list("problem__id", "problem__points") - .annotate(points=Max("points")) + .values_list( + "problem__id", "problem__points", "problem__code", "problem__name" + ) + .annotate(points=Max("points"), last_submission=Max("id")) .filter(points__lt=F("problem__points")) ) } diff --git a/judge/views/problem.py b/judge/views/problem.py index 08bcb54..1efca66 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -52,6 +52,7 @@ from judge.models import ( TranslatedProblemForeignKeyQuerySet, Organization, VolunteerProblemVote, + Profile, ) from judge.pdf_problems import DefaultPdfMaker, HAS_PDF from judge.utils.diggpaginator import DiggPaginator @@ -130,6 +131,15 @@ class SolvedProblemMixin(object): else: return user_attempted_ids(self.profile) if self.profile is not None else () + def get_latest_attempted_problems(self, limit=None): + if self.in_contest or not self.profile: + return () + result = list(user_attempted_ids(self.profile).values()) + result = sorted(result, key=lambda d: -d["last_submission"]) + if limit: + result = result[:limit] + return result + @cached_property def in_contest(self): return ( @@ -582,6 +592,8 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView Q(organizations__in=self.org_query) | Q(contests__contest__organizations__in=self.org_query) ) + if self.author_query: + queryset = queryset.filter(authors__in=self.author_query) if self.show_types: queryset = queryset.prefetch_related("types") if self.category is not None: @@ -639,6 +651,8 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView context["have_editorial"] = 0 if self.in_contest else int(self.have_editorial) context["organizations"] = Organization.objects.all() + all_authors_ids = set(Problem.objects.values_list("authors", flat=True)) + context["all_authors"] = Profile.objects.filter(id__in=all_authors_ids) context["category"] = self.category context["categories"] = ProblemGroup.objects.all() if self.show_types: @@ -646,9 +660,11 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView context["problem_types"] = ProblemType.objects.all() context["has_fts"] = settings.ENABLE_FTS context["org_query"] = self.org_query + context["author_query"] = self.author_query context["search_query"] = self.search_query context["completed_problem_ids"] = self.get_completed_problems() context["attempted_problems"] = self.get_attempted_problems() + context["last_attempted_problems"] = self.get_latest_attempted_problems(15) context["page_type"] = "list" context.update(self.get_sort_paginate_context()) if not self.in_contest: @@ -736,6 +752,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView self.search_query = None self.category = None self.org_query = [] + self.author_query = [] self.selected_types = [] # This actually copies into the instance dictionary... @@ -749,12 +766,16 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView self.selected_types = list(map(int, request.GET.getlist("type"))) except ValueError: pass - if "orgs" in request.GET: try: self.org_query = list(map(int, request.GET.getlist("orgs"))) except ValueError: pass + if "authors" in request.GET: + try: + self.author_query = list(map(int, request.GET.getlist("authors"))) + except ValueError: + pass self.point_start = safe_float_or_none(request.GET.get("point_start")) self.point_end = safe_float_or_none(request.GET.get("point_end")) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 1e853b8..bd1b0d1 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-05-03 09:54+0700\n" +"POT-Creation-Date: 2022-05-22 08:28+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -18,326 +18,326 @@ msgstr "" "X-Crowdin-Project-ID: 466004\n" "X-Crowdin-File-ID: 5\n" -#: chat_box/models.py:23 chat_box/models.py:41 chat_box/models.py:47 -#: judge/admin/interface.py:112 judge/models/contest.py:404 -#: judge/models/contest.py:529 judge/models/profile.py:220 +#: chat_box/models.py:31 chat_box/models.py:52 chat_box/models.py:63 +#: judge/admin/interface.py:150 judge/models/contest.py:621 +#: judge/models/contest.py:809 judge/models/profile.py:338 msgid "user" msgstr "người dùng" -#: chat_box/models.py:24 judge/models/comment.py:43 judge/models/comment.py:193 +#: chat_box/models.py:32 judge/models/comment.py:46 judge/models/comment.py:242 msgid "posted time" msgstr "thời gian đăng" -#: chat_box/models.py:25 judge/models/comment.py:47 +#: chat_box/models.py:33 judge/models/comment.py:54 msgid "body of comment" msgstr "nội dung bình luận" -#: chat_box/models.py:43 +#: chat_box/models.py:56 msgid "last seen" msgstr "xem lần cuối" -#: chat_box/views.py:29 templates/chat/chat.html:4 templates/chat/chat.html:548 +#: chat_box/views.py:45 templates/chat/chat.html:4 templates/chat/chat.html:553 msgid "Chat Box" msgstr "Chat Box" -#: dmoj/settings.py:360 +#: dmoj/settings.py:365 msgid "German" msgstr "" -#: dmoj/settings.py:361 +#: dmoj/settings.py:366 msgid "English" msgstr "" -#: dmoj/settings.py:362 +#: dmoj/settings.py:367 msgid "Spanish" msgstr "" -#: dmoj/settings.py:363 +#: dmoj/settings.py:368 msgid "French" msgstr "" -#: dmoj/settings.py:364 +#: dmoj/settings.py:369 msgid "Croatian" msgstr "" -#: dmoj/settings.py:365 +#: dmoj/settings.py:370 msgid "Hungarian" msgstr "" -#: dmoj/settings.py:366 +#: dmoj/settings.py:371 msgid "Japanese" msgstr "" -#: dmoj/settings.py:367 +#: dmoj/settings.py:372 msgid "Korean" msgstr "" -#: dmoj/settings.py:368 +#: dmoj/settings.py:373 msgid "Brazilian Portuguese" msgstr "" -#: dmoj/settings.py:369 +#: dmoj/settings.py:374 msgid "Romanian" msgstr "" -#: dmoj/settings.py:370 +#: dmoj/settings.py:375 msgid "Russian" msgstr "" -#: dmoj/settings.py:371 +#: dmoj/settings.py:376 msgid "Serbian (Latin)" msgstr "" -#: dmoj/settings.py:372 +#: dmoj/settings.py:377 msgid "Turkish" msgstr "" -#: dmoj/settings.py:373 +#: dmoj/settings.py:378 msgid "Vietnamese" msgstr "Tiếng Việt" -#: dmoj/settings.py:374 +#: dmoj/settings.py:379 msgid "Simplified Chinese" msgstr "" -#: dmoj/settings.py:375 +#: dmoj/settings.py:380 msgid "Traditional Chinese" msgstr "" -#: dmoj/urls.py:58 +#: dmoj/urls.py:130 msgid "Login" msgstr "Đăng nhập" -#: dmoj/urls.py:106 templates/base.html:213 +#: dmoj/urls.py:206 templates/base.html:213 msgid "Home" msgstr "Trang chủ" -#: judge/admin/comments.py:41 +#: judge/admin/comments.py:46 #, python-format msgid "%d comment successfully hidden." msgid_plural "%d comments successfully hidden." msgstr[0] "Đã ẩn %d bình luận." -#: judge/admin/comments.py:44 +#: judge/admin/comments.py:53 msgid "Hide comments" msgstr "Ẩn bình luận" -#: judge/admin/comments.py:48 +#: judge/admin/comments.py:60 #, python-format msgid "%d comment successfully unhidden." msgid_plural "%d comments successfully unhidden." msgstr[0] "Không ẩn được %d bình luận." -#: judge/admin/comments.py:51 +#: judge/admin/comments.py:67 msgid "Unhide comments" msgstr "Hiện bình luận" -#: judge/admin/comments.py:59 +#: judge/admin/comments.py:76 msgid "Associated page" msgstr "Trang liên kết" -#: judge/admin/contest.py:31 +#: judge/admin/contest.py:37 msgid "Included contests" msgstr "" -#: judge/admin/contest.py:67 judge/admin/volunteer.py:17 +#: judge/admin/contest.py:74 judge/admin/volunteer.py:33 #: templates/contest/clarification.html:42 templates/contest/contest.html:83 #: templates/contest/moss.html:43 templates/internal/base.html:29 -#: templates/internal/base.html:37 templates/problem/list.html:278 -#: templates/problem/list.html:294 templates/problem/list.html:412 +#: templates/internal/base.html:37 templates/problem/list.html:264 +#: templates/problem/list.html:279 templates/problem/list.html:395 #: templates/user/user-problems.html:56 templates/user/user-problems.html:98 msgid "Problem" msgstr "Bài tập" -#: judge/admin/contest.py:120 +#: judge/admin/contest.py:149 msgid "Settings" msgstr "Cài đặt" -#: judge/admin/contest.py:122 +#: judge/admin/contest.py:161 msgid "Scheduling" msgstr "" -#: judge/admin/contest.py:123 templates/organization/home.html:100 +#: judge/admin/contest.py:163 templates/organization/home.html:100 msgid "Details" msgstr "Chi tiết" -#: judge/admin/contest.py:124 +#: judge/admin/contest.py:175 msgid "Format" msgstr "Thể thức" -#: judge/admin/contest.py:125 templates/contest/ranking-table.html:7 +#: judge/admin/contest.py:179 templates/contest/ranking-table.html:7 #: templates/user/user-about.html:15 templates/user/user-about.html:45 msgid "Rating" msgstr "" -#: judge/admin/contest.py:126 +#: judge/admin/contest.py:191 msgid "Access" msgstr "Truy cập" -#: judge/admin/contest.py:128 judge/admin/problem.py:135 +#: judge/admin/contest.py:203 judge/admin/problem.py:192 msgid "Justice" msgstr "Xử phạt" -#: judge/admin/contest.py:208 +#: judge/admin/contest.py:313 #, python-format msgid "%d contest successfully marked as visible." msgid_plural "%d contests successfully marked as visible." msgstr[0] "%d kỳ thi đã được đánh dấu hiển thị." -#: judge/admin/contest.py:211 +#: judge/admin/contest.py:320 msgid "Mark contests as visible" msgstr "Đánh dấu hiển thị các kỳ thi" -#: judge/admin/contest.py:217 +#: judge/admin/contest.py:331 #, python-format msgid "%d contest successfully marked as hidden." msgid_plural "%d contests successfully marked as hidden." msgstr[0] "%d kỳ thi đã được đánh dấu ẩn." -#: judge/admin/contest.py:220 +#: judge/admin/contest.py:338 msgid "Mark contests as hidden" msgstr "Ẩn các kỳ thi" -#: judge/admin/contest.py:234 judge/admin/submission.py:164 +#: judge/admin/contest.py:359 judge/admin/submission.py:243 #, python-format msgid "%d submission was successfully scheduled for rejudging." msgid_plural "%d submissions were successfully scheduled for rejudging." msgstr[0] "%d bài nộp đã được lên lịch thành công để chấm lại." -#: judge/admin/contest.py:309 +#: judge/admin/contest.py:467 #, python-format msgid "%d participation recalculated." msgid_plural "%d participations recalculated." msgstr[0] "%d thí sinh đã được tính điểm lại." -#: judge/admin/contest.py:312 +#: judge/admin/contest.py:474 msgid "Recalculate results" msgstr "Tính toán lại kết quả" -#: judge/admin/contest.py:316 judge/admin/organization.py:65 +#: judge/admin/contest.py:479 judge/admin/organization.py:92 msgid "username" msgstr "tên đăng nhập" -#: judge/admin/contest.py:321 templates/base.html:304 +#: judge/admin/contest.py:485 templates/base.html:304 msgid "virtual" msgstr "ảo" -#: judge/admin/interface.py:30 judge/models/interface.py:46 +#: judge/admin/interface.py:35 judge/models/interface.py:46 msgid "link path" msgstr "đường dẫn" -#: judge/admin/interface.py:67 +#: judge/admin/interface.py:94 msgid "Content" msgstr "Nội dung" -#: judge/admin/interface.py:68 +#: judge/admin/interface.py:95 msgid "Summary" msgstr "Tổng kết" -#: judge/admin/interface.py:153 +#: judge/admin/interface.py:212 msgid "object" msgstr "" -#: judge/admin/interface.py:161 +#: judge/admin/interface.py:222 msgid "Diff" msgstr "" -#: judge/admin/interface.py:165 +#: judge/admin/interface.py:227 msgid "diff" msgstr "" -#: judge/admin/organization.py:34 judge/admin/problem.py:175 -#: judge/admin/profile.py:80 +#: judge/admin/organization.py:59 judge/admin/problem.py:254 +#: judge/admin/profile.py:116 msgid "View on site" msgstr "Xem trên trang" -#: judge/admin/problem.py:32 +#: judge/admin/problem.py:48 msgid "Describe the changes you made (optional)" msgstr "Mô tả các thay đổi (tùy chọn)" -#: judge/admin/problem.py:130 +#: judge/admin/problem.py:185 msgid "Social Media" msgstr "Mạng Xã Hội" -#: judge/admin/problem.py:131 +#: judge/admin/problem.py:188 msgid "Taxonomy" msgstr "" -#: judge/admin/problem.py:132 judge/admin/problem.py:286 -#: templates/contest/contest.html:84 templates/problem/data.html:474 -#: templates/problem/list.html:284 templates/problem/list.html:310 +#: judge/admin/problem.py:189 judge/admin/problem.py:407 +#: templates/contest/contest.html:84 templates/problem/data.html:476 +#: templates/problem/list.html:269 templates/problem/list.html:293 #: templates/user/base-users-table.html:10 templates/user/user-about.html:36 #: templates/user/user-about.html:52 templates/user/user-problems.html:58 msgid "Points" msgstr "Điểm" -#: judge/admin/problem.py:133 +#: judge/admin/problem.py:190 msgid "Limits" msgstr "Giới hạn" -#: judge/admin/problem.py:134 judge/admin/submission.py:232 +#: judge/admin/problem.py:191 judge/admin/submission.py:353 #: templates/stats/base.html:14 templates/submission/list.html:322 msgid "Language" msgstr "Ngôn ngữ" -#: judge/admin/problem.py:136 +#: judge/admin/problem.py:193 msgid "History" msgstr "Lịch sử" -#: judge/admin/problem.py:172 +#: judge/admin/problem.py:250 templates/problem/list.html:97 msgid "Authors" msgstr "Các tác giả" -#: judge/admin/problem.py:187 +#: judge/admin/problem.py:271 #, python-format msgid "%d problem successfully marked as public." msgid_plural "%d problems successfully marked as public." msgstr[0] "%d bài tập đã được đánh dấu công khai." -#: judge/admin/problem.py:191 +#: judge/admin/problem.py:278 msgid "Mark problems as public" msgstr "Công khai bài tập" -#: judge/admin/problem.py:197 +#: judge/admin/problem.py:287 #, python-format msgid "%d problem successfully marked as private." msgid_plural "%d problems successfully marked as private." msgstr[0] "%d bài tập đã được đánh dấu riêng tư." -#: judge/admin/problem.py:201 +#: judge/admin/problem.py:294 msgid "Mark problems as private" msgstr "Đánh dấu các bài tập là riêng tư" -#: judge/admin/problem.py:281 judge/admin/submission.py:200 -#: templates/problem/list.html:279 templates/problem/list.html:298 +#: judge/admin/problem.py:401 judge/admin/submission.py:316 +#: templates/problem/list.html:265 templates/problem/list.html:282 msgid "Problem code" msgstr "Mã bài" -#: judge/admin/problem.py:291 judge/admin/submission.py:205 +#: judge/admin/problem.py:413 judge/admin/submission.py:322 msgid "Problem name" msgstr "Tên bài" -#: judge/admin/problem.py:296 +#: judge/admin/problem.py:419 #, fuzzy #| msgid "contest rating" msgid "Voter rating" msgstr "rating kỳ thi" -#: judge/admin/problem.py:301 +#: judge/admin/problem.py:425 #, fuzzy #| msgid "Total points" msgid "Voter point" msgstr "Tổng điểm" -#: judge/admin/problem.py:306 +#: judge/admin/problem.py:431 msgid "Vote" msgstr "" -#: judge/admin/profile.py:34 +#: judge/admin/profile.py:40 msgid "timezone" msgstr "múi giờ" -#: judge/admin/profile.py:86 judge/admin/submission.py:211 +#: judge/admin/profile.py:125 judge/admin/submission.py:329 #: templates/notification/list.html:12 #: templates/organization/requests/log.html:9 #: templates/organization/requests/pending.html:12 @@ -345,28 +345,28 @@ msgstr "múi giờ" msgid "User" msgstr "Thành viên" -#: judge/admin/profile.py:91 templates/registration/registration_form.html:145 +#: judge/admin/profile.py:131 templates/registration/registration_form.html:145 #: templates/user/import/table_csv.html:8 msgid "Email" msgstr "Email" -#: judge/admin/profile.py:96 judge/views/register.py:29 +#: judge/admin/profile.py:137 judge/views/register.py:37 #: templates/registration/registration_form.html:173 #: templates/user/edit-profile.html:116 msgid "Timezone" msgstr "Múi giờ" -#: judge/admin/profile.py:101 +#: judge/admin/profile.py:143 msgid "date joined" msgstr "ngày tham gia" -#: judge/admin/profile.py:108 +#: judge/admin/profile.py:153 #, python-format msgid "%d user have scores recalculated." msgid_plural "%d users have scores recalculated." msgstr[0] "%d người dùng đã được tính điểm lại." -#: judge/admin/profile.py:111 +#: judge/admin/profile.py:160 msgid "Recalculate scores" msgstr "Tính điểm lại" @@ -378,87 +378,87 @@ msgstr "Các bài tập không được cho phép" msgid "These problems are NOT allowed to be submitted in this language" msgstr "Các bài này không cho phép sử dụng ngôn ngữ này" -#: judge/admin/runtime.py:83 templates/problem/list.html:414 +#: judge/admin/runtime.py:117 templates/problem/list.html:397 msgid "Description" msgstr "Mô tả" -#: judge/admin/runtime.py:84 +#: judge/admin/runtime.py:119 msgid "Information" msgstr "Thông tin" -#: judge/admin/runtime.py:85 +#: judge/admin/runtime.py:122 msgid "Capabilities" msgstr "Khả năng" -#: judge/admin/submission.py:23 judge/admin/submission.py:42 -#: judge/admin/submission.py:221 +#: judge/admin/submission.py:31 judge/admin/submission.py:53 +#: judge/admin/submission.py:340 msgid "None" msgstr "None" -#: judge/admin/submission.py:23 +#: judge/admin/submission.py:32 msgid "Not done" msgstr "Chưa xong" -#: judge/admin/submission.py:23 +#: judge/admin/submission.py:33 msgid "Exceptional" msgstr "Đặc biệt" -#: judge/admin/submission.py:42 +#: judge/admin/submission.py:53 msgid "Unaccepted" msgstr "" -#: judge/admin/submission.py:89 +#: judge/admin/submission.py:104 #, python-format msgctxt "contest problem" msgid "%(problem)s in %(contest)s" msgstr "%(problem)s trong %(contest)s" -#: judge/admin/submission.py:149 judge/admin/submission.py:171 +#: judge/admin/submission.py:215 judge/admin/submission.py:256 msgid "You do not have the permission to rejudge submissions." msgstr "Bạn không có quyền chấm lại bài." -#: judge/admin/submission.py:155 +#: judge/admin/submission.py:227 msgid "You do not have the permission to rejudge THAT many submissions." msgstr "Bạn không có quyền chấm lại nhiều bài nộp như vậy." -#: judge/admin/submission.py:167 +#: judge/admin/submission.py:250 msgid "Rejudge the selected submissions" msgstr "Chấm lại các bài nộp đã chọn" -#: judge/admin/submission.py:193 judge/views/problem_manage.py:159 +#: judge/admin/submission.py:304 judge/views/problem_manage.py:212 #, python-format msgid "%d submission were successfully rescored." msgid_plural "%d submissions were successfully rescored." msgstr[0] "%d bài nộp đã được tính điểm lại." -#: judge/admin/submission.py:196 +#: judge/admin/submission.py:311 msgid "Rescore the selected submissions" msgstr "Tính điểm lại cái bài nộp" -#: judge/admin/submission.py:215 templates/notification/list.html:15 +#: judge/admin/submission.py:334 templates/notification/list.html:15 #: templates/organization/requests/log.html:10 #: templates/organization/requests/pending.html:13 -#: templates/problem/list.html:413 +#: templates/problem/list.html:396 #: templates/submission/status-testcases.html:125 #: templates/submission/status-testcases.html:127 msgid "Time" msgstr "Thời gian" -#: judge/admin/submission.py:223 +#: judge/admin/submission.py:342 #, python-format msgid "%d KB" msgstr "%d KB" -#: judge/admin/submission.py:225 +#: judge/admin/submission.py:344 #, python-format msgid "%.2f MB" msgstr "" -#: judge/admin/submission.py:227 templates/submission/status-testcases.html:132 +#: judge/admin/submission.py:347 templates/submission/status-testcases.html:132 msgid "Memory" msgstr "Bộ nhớ" -#: judge/admin/taxon.py:11 judge/admin/taxon.py:34 +#: judge/admin/taxon.py:11 judge/admin/taxon.py:37 msgid "Included problems" msgstr "" @@ -466,7 +466,7 @@ msgstr "" msgid "These problems are included in this group of problems" msgstr "Các bài tập trong nhóm này" -#: judge/admin/taxon.py:37 +#: judge/admin/taxon.py:40 msgid "These problems are included in this type of problems" msgstr "Các bài tập dạng này" @@ -474,20 +474,20 @@ msgstr "Các bài tập dạng này" msgid "Online Judge" msgstr "" -#: judge/comments.py:60 +#: judge/comments.py:61 msgid "Comment body" msgstr "Nội dung bình luận" -#: judge/comments.py:66 judge/views/ticket.py:63 +#: judge/comments.py:67 judge/views/ticket.py:78 msgid "Your part is silent, little toad." msgstr "Bạn không được phép bình luận." -#: judge/comments.py:69 templates/comments/list.html:132 +#: judge/comments.py:76 templates/comments/list.html:132 msgid "" "You need to have solved at least one problem before your voice can be heard." msgstr "Bạn phải giải ít nhất một bài trước khi được phép bình luận." -#: judge/comments.py:115 +#: judge/comments.py:129 msgid "Posted comment" msgstr "Bình luận đã đăng" @@ -511,57 +511,57 @@ msgstr "" msgid "IOI" msgstr "" -#: judge/forms.py:27 +#: judge/forms.py:44 msgid "Subscribe to contest updates" msgstr "Đăng ký để nhận thông báo về các kỳ thi" -#: judge/forms.py:28 +#: judge/forms.py:47 msgid "Enable experimental features" msgstr "Sử dụng các tính năng thử nghiệm" -#: judge/forms.py:57 judge/views/organization.py:168 judge/views/register.py:49 +#: judge/forms.py:85 judge/views/organization.py:247 judge/views/register.py:69 #, python-brace-format msgid "You may not be part of more than {count} public organizations." msgstr "Bạn không thể tham gia nhiều hơn {count} tổ chức công khai." -#: judge/forms.py:82 +#: judge/forms.py:116 msgid "Any judge" msgstr "" -#: judge/forms.py:112 judge/views/register.py:26 +#: judge/forms.py:148 judge/views/register.py:31 #: templates/registration/registration_form.html:139 #: templates/user/base-users-table.html:5 #: templates/user/import/table_csv.html:4 msgid "Username" msgstr "Tên đăng nhập" -#: judge/forms.py:113 templates/registration/registration_form.html:151 +#: judge/forms.py:149 templates/registration/registration_form.html:151 #: templates/registration/registration_form.html:165 #: templates/user/import/table_csv.html:5 msgid "Password" msgstr "Mật khẩu" -#: judge/forms.py:135 +#: judge/forms.py:175 msgid "Two Factor Authentication tokens must be 6 decimal digits." msgstr "Two Factor Authentication phải chứa 6 chữ số." -#: judge/forms.py:144 templates/registration/totp_auth.html:32 +#: judge/forms.py:188 templates/registration/totp_auth.html:32 msgid "Invalid Two Factor Authentication token." msgstr "Token Two Factor Authentication không hợp lệ." -#: judge/forms.py:148 judge/models/problem.py:98 +#: judge/forms.py:195 judge/models/problem.py:151 msgid "Problem code must be ^[a-z0-9]+$" msgstr "Mã bài phải có dạng ^[a-z0-9]+$" -#: judge/forms.py:153 +#: judge/forms.py:202 msgid "Problem with code already exists." msgstr "Mã bài đã tồn tại." -#: judge/forms.py:158 judge/models/contest.py:61 +#: judge/forms.py:209 judge/models/contest.py:89 msgid "Contest id must be ^[a-z0-9]+$" msgstr "Mã kỳ thi phải có dạng ^[a-z0-9]+$" -#: judge/forms.py:163 +#: judge/forms.py:215 msgid "Contest with key already exists." msgstr "Mã kỳ thi đã tồn tại." @@ -604,52 +604,52 @@ msgstr "" msgid "Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$" msgstr "Mã trang phải có dạng ^[pcs]:[a-z0-9]+$|^b:\\d+$" -#: judge/models/comment.py:42 +#: judge/models/comment.py:45 msgid "commenter" msgstr "người bình luận" -#: judge/models/comment.py:44 judge/models/comment.py:179 +#: judge/models/comment.py:49 judge/models/comment.py:223 msgid "associated page" msgstr "trang tương ứng" -#: judge/models/comment.py:46 judge/models/problem.py:496 +#: judge/models/comment.py:53 judge/models/problem.py:719 msgid "votes" msgstr "bình chọn" -#: judge/models/comment.py:48 +#: judge/models/comment.py:55 msgid "hide the comment" msgstr "ẩn bình luận" -#: judge/models/comment.py:49 +#: judge/models/comment.py:58 msgid "parent" msgstr "" -#: judge/models/comment.py:54 judge/models/comment.py:194 +#: judge/models/comment.py:67 judge/models/comment.py:244 msgid "comment" msgstr "bình luận" -#: judge/models/comment.py:55 +#: judge/models/comment.py:68 msgid "comments" msgstr "" -#: judge/models/comment.py:139 judge/models/problem.py:466 +#: judge/models/comment.py:160 judge/models/problem.py:687 #, python-format msgid "Editorial for %s" msgstr "" -#: judge/models/comment.py:174 +#: judge/models/comment.py:216 msgid "comment vote" msgstr "" -#: judge/models/comment.py:175 +#: judge/models/comment.py:217 msgid "comment votes" msgstr "" -#: judge/models/comment.py:184 +#: judge/models/comment.py:229 msgid "Override comment lock" msgstr "" -#: judge/models/comment.py:192 +#: judge/models/comment.py:238 #: src/dmoj-wpadmin/test_project/apps/books/admin.py:24 #: src/dmoj-wpadmin/test_project/apps/books/models.py:30 #: src/dmoj-wpadmin/test_project/apps/cds/models.py:30 @@ -657,89 +657,89 @@ msgstr "" msgid "owner" msgstr "" -#: judge/models/comment.py:195 judge/models/message.py:16 +#: judge/models/comment.py:246 judge/models/message.py:28 msgid "read" msgstr "" -#: judge/models/comment.py:196 +#: judge/models/comment.py:247 #: src/dmoj-wpadmin/test_project/apps/books/models.py:28 #: src/dmoj-wpadmin/test_project/apps/cds/models.py:28 #: src/dmoj-wpadmin/test_project/apps/dvds/models.py:28 msgid "category" msgstr "" -#: judge/models/comment.py:197 +#: judge/models/comment.py:250 msgid "html link to comments, used for non-comments" msgstr "" -#: judge/models/comment.py:198 +#: judge/models/comment.py:256 msgid "who trigger, used for non-comment" msgstr "" -#: judge/models/contest.py:23 +#: judge/models/contest.py:36 msgid "Invalid colour." msgstr "" -#: judge/models/contest.py:25 +#: judge/models/contest.py:40 msgid "tag name" msgstr "" -#: judge/models/contest.py:26 +#: judge/models/contest.py:44 msgid "Lowercase letters and hyphens only." msgstr "" -#: judge/models/contest.py:27 +#: judge/models/contest.py:49 msgid "tag colour" msgstr "" -#: judge/models/contest.py:28 +#: judge/models/contest.py:51 msgid "tag description" msgstr "" -#: judge/models/contest.py:47 +#: judge/models/contest.py:72 msgid "contest tag" msgstr "" -#: judge/models/contest.py:48 judge/models/contest.py:119 +#: judge/models/contest.py:73 judge/models/contest.py:233 msgid "contest tags" msgstr "nhãn kỳ thi" -#: judge/models/contest.py:56 +#: judge/models/contest.py:81 msgid "Visible" msgstr "Hiển thị" -#: judge/models/contest.py:57 +#: judge/models/contest.py:82 msgid "Hidden for duration of contest" msgstr "Ẩn trong thời gian kỳ thi" -#: judge/models/contest.py:58 +#: judge/models/contest.py:83 msgid "Hidden for duration of participation" msgstr "Ẩn trong thời gian tham gia" -#: judge/models/contest.py:60 +#: judge/models/contest.py:87 msgid "contest id" msgstr "ID kỳ thi" -#: judge/models/contest.py:62 +#: judge/models/contest.py:92 msgid "contest name" msgstr "tên kỳ thi" -#: judge/models/contest.py:63 +#: judge/models/contest.py:96 msgid "These users will be able to edit the contest." msgstr "Những người dùng này có quyền chỉnh sửa kỳ thi." -#: judge/models/contest.py:65 +#: judge/models/contest.py:102 msgid "" "These users will be able to edit the contest, but will not be listed as " "authors." msgstr "Những người dùng này là tác giả và có quyền chỉnh sửa kỳ thi." -#: judge/models/contest.py:68 +#: judge/models/contest.py:111 msgid "These users will be able to view the contest, but not edit it." msgstr "" "Những người dùng này có thể thấy kỳ thi nhưng không có quyền chỉnh sửa." -#: judge/models/contest.py:71 judge/models/runtime.py:136 +#: judge/models/contest.py:116 judge/models/runtime.py:211 #: src/dmoj-wpadmin/test_project/apps/books/admin.py:20 #: src/dmoj-wpadmin/test_project/apps/books/models.py:13 #: src/dmoj-wpadmin/test_project/apps/books/models.py:27 @@ -750,36 +750,36 @@ msgstr "" msgid "description" msgstr "mô tả" -#: judge/models/contest.py:72 judge/models/problem.py:413 -#: judge/models/runtime.py:138 +#: judge/models/contest.py:118 judge/models/problem.py:600 +#: judge/models/runtime.py:216 msgid "problems" msgstr "bài tập" -#: judge/models/contest.py:73 judge/models/contest.py:405 +#: judge/models/contest.py:120 judge/models/contest.py:626 msgid "start time" msgstr "thời gian bắt đầu" -#: judge/models/contest.py:74 +#: judge/models/contest.py:121 msgid "end time" msgstr "thời gian kết thúc" -#: judge/models/contest.py:75 judge/models/problem.py:119 -#: judge/models/problem.py:437 +#: judge/models/contest.py:123 judge/models/problem.py:204 +#: judge/models/problem.py:645 msgid "time limit" msgstr "giới hạn thời gian" -#: judge/models/contest.py:76 +#: judge/models/contest.py:127 msgid "" "Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00" msgstr "" "Định dạng hh:mm:ss (giờ:phút:giây). Ví dụ, nếu muốn tạo kỳ thi dài 2h, hãy " "nhập 02:00:00" -#: judge/models/contest.py:77 judge/models/problem.py:137 +#: judge/models/contest.py:131 judge/models/problem.py:243 msgid "publicly visible" msgstr "công khai" -#: judge/models/contest.py:78 +#: judge/models/contest.py:134 msgid "" "Should be set even for organization-private contests, where it determines " "whether the contest is visible to members of the specified organizations." @@ -787,84 +787,84 @@ msgstr "" "Đánh dấu ngay cả với các kỳ thi riêng tư của tổ chức, quyết định việc kỳ thi " "có được hiển thị với các thành viên hay không." -#: judge/models/contest.py:81 +#: judge/models/contest.py:140 msgid "contest rated" msgstr "kỳ thi được xếp hạng" -#: judge/models/contest.py:81 +#: judge/models/contest.py:141 msgid "Whether this contest can be rated." msgstr "Quyết định kỳ thi có được xếp hạng không." -#: judge/models/contest.py:83 +#: judge/models/contest.py:145 msgid "scoreboard visibility" msgstr "khả năng hiển thị của bảng điểm" -#: judge/models/contest.py:84 +#: judge/models/contest.py:148 msgid "Scoreboard visibility through the duration of the contest" msgstr "Khả năng hiển thị của bảng điểm trong thời gian kỳ thi" -#: judge/models/contest.py:86 +#: judge/models/contest.py:153 msgid "view contest scoreboard" msgstr "xem bảng điểm kỳ thi" -#: judge/models/contest.py:88 +#: judge/models/contest.py:156 msgid "These users will be able to view the scoreboard." msgstr "Những người dùng này được phép xem bảng điểm." -#: judge/models/contest.py:89 +#: judge/models/contest.py:159 msgid "no comments" msgstr "không bình luận" -#: judge/models/contest.py:90 +#: judge/models/contest.py:160 msgid "Use clarification system instead of comments." msgstr "Dùng hệ thống thông báo thay vì bình luận." -#: judge/models/contest.py:92 +#: judge/models/contest.py:165 msgid "Rating floor for contest" msgstr "Cận dưới rating được xếp hạng trong kỳ thi" -#: judge/models/contest.py:94 +#: judge/models/contest.py:171 msgid "Rating ceiling for contest" msgstr "Cận trên rating được xếp hạng trong kỳ thi" -#: judge/models/contest.py:96 +#: judge/models/contest.py:176 msgid "rate all" msgstr "xếp hạng tất cả" -#: judge/models/contest.py:96 +#: judge/models/contest.py:177 msgid "Rate all users who joined." msgstr "Xếp hạng tất cả người dùng đã tham gia (kể cả không nộp)." -#: judge/models/contest.py:97 +#: judge/models/contest.py:182 msgid "exclude from ratings" msgstr "không xếp hạng" -#: judge/models/contest.py:99 +#: judge/models/contest.py:187 msgid "private to specific users" msgstr "riêng tư với các người dùng này" -#: judge/models/contest.py:100 +#: judge/models/contest.py:192 msgid "private contestants" msgstr "thí sinh riêng tư" -#: judge/models/contest.py:101 +#: judge/models/contest.py:193 msgid "If private, only these users may see the contest" msgstr "Nếu riêng tư, chỉ những người dùng này mới thấy kỳ thi" -#: judge/models/contest.py:103 +#: judge/models/contest.py:197 msgid "hide problem tags" msgstr "ẩn nhãn kỳ thi" -#: judge/models/contest.py:104 +#: judge/models/contest.py:198 msgid "Whether problem tags should be hidden by default." msgstr "" "Quyết định việc nhãn bài tập (DP, Tham lam, ...) được ẩn trong kỳ thi không." -#: judge/models/contest.py:106 +#: judge/models/contest.py:202 msgid "run pretests only" msgstr "chỉ chạy pretests" -#: judge/models/contest.py:107 +#: judge/models/contest.py:204 msgid "" "Whether judges should grade pretests only, versus all testcases. Commonly " "set during a contest, then unset prior to rejudging user submissions when " @@ -873,50 +873,50 @@ msgstr "" "Quyết định việc các máy chấm chỉ chấm pretests thay vì tất cả các test. Sau " "kỳ thi, hãy bỏ đánh dấu ô này và chấm lại tất cả các bài." -#: judge/models/contest.py:111 judge/models/interface.py:77 -#: judge/models/problem.py:158 +#: judge/models/contest.py:211 judge/models/interface.py:92 +#: judge/models/problem.py:300 msgid "private to organizations" msgstr "riêng tư với các tổ chức" -#: judge/models/contest.py:112 judge/models/interface.py:75 -#: judge/models/problem.py:156 judge/models/profile.py:77 +#: judge/models/contest.py:216 judge/models/interface.py:88 +#: judge/models/problem.py:296 judge/models/profile.py:116 msgid "organizations" msgstr "tổ chức" -#: judge/models/contest.py:113 +#: judge/models/contest.py:217 msgid "If private, only these organizations may see the contest" msgstr "Nếu riêng tư, chỉ những tổ chức này thấy được kỳ thi" -#: judge/models/contest.py:114 judge/models/problem.py:146 +#: judge/models/contest.py:220 judge/models/problem.py:274 msgid "OpenGraph image" msgstr "Hình ảnh OpenGraph" -#: judge/models/contest.py:115 judge/models/profile.py:48 +#: judge/models/contest.py:223 judge/models/profile.py:80 msgid "Logo override image" msgstr "Hình ảnh ghi đè logo" -#: judge/models/contest.py:117 +#: judge/models/contest.py:228 msgid "" "This image will replace the default site logo for users inside the contest." msgstr "Ảnh này sẽ thay thế cho logo mặc định trong kỳ thi." -#: judge/models/contest.py:120 +#: judge/models/contest.py:236 msgid "the amount of live participants" msgstr "số lượng thí sinh thi trực tiếp" -#: judge/models/contest.py:121 +#: judge/models/contest.py:240 msgid "contest summary" msgstr "tổng kết kỳ thi" -#: judge/models/contest.py:122 judge/models/problem.py:148 +#: judge/models/contest.py:242 judge/models/problem.py:280 msgid "Plain-text, shown in meta description tag, e.g. for social media." msgstr "" -#: judge/models/contest.py:123 judge/models/profile.py:47 +#: judge/models/contest.py:246 judge/models/profile.py:75 msgid "access code" msgstr "mật khẩu truy cập" -#: judge/models/contest.py:124 +#: judge/models/contest.py:251 msgid "" "An optional code to prompt contestants before they are allowed to join the " "contest. Leave it blank to disable." @@ -924,251 +924,251 @@ msgstr "" "Mật khẩu truy cập cho các thí sinh muốn tham gia kỳ thi. Để trống nếu không " "dùng." -#: judge/models/contest.py:126 judge/models/problem.py:142 +#: judge/models/contest.py:257 judge/models/problem.py:262 msgid "personae non gratae" msgstr "" -#: judge/models/contest.py:127 +#: judge/models/contest.py:259 msgid "Bans the selected users from joining this contest." msgstr "Cấm những người dùng được chọn tham gia kỳ thi." -#: judge/models/contest.py:128 +#: judge/models/contest.py:262 msgid "contest format" msgstr "format kỳ thi" -#: judge/models/contest.py:129 +#: judge/models/contest.py:266 msgid "The contest format module to use." msgstr "" -#: judge/models/contest.py:130 +#: judge/models/contest.py:269 msgid "contest format configuration" msgstr "" -#: judge/models/contest.py:131 +#: judge/models/contest.py:273 msgid "" "A JSON object to serve as the configuration for the chosen contest format " "module. Leave empty to use None. Exact format depends on the contest format " "selected." msgstr "" -#: judge/models/contest.py:138 +#: judge/models/contest.py:286 msgid "precision points" msgstr "" -#: judge/models/contest.py:140 +#: judge/models/contest.py:289 msgid "Number of digits to round points to." msgstr "" -#: judge/models/contest.py:384 +#: judge/models/contest.py:594 msgid "See private contests" msgstr "" -#: judge/models/contest.py:385 +#: judge/models/contest.py:595 msgid "Edit own contests" msgstr "" -#: judge/models/contest.py:386 +#: judge/models/contest.py:596 msgid "Edit all contests" msgstr "" -#: judge/models/contest.py:387 +#: judge/models/contest.py:597 msgid "Clone contest" msgstr "" -#: judge/models/contest.py:388 templates/contest/moss.html:74 +#: judge/models/contest.py:598 templates/contest/moss.html:74 msgid "MOSS contest" msgstr "" -#: judge/models/contest.py:389 +#: judge/models/contest.py:599 msgid "Rate contests" msgstr "" -#: judge/models/contest.py:390 +#: judge/models/contest.py:600 msgid "Contest access codes" msgstr "" -#: judge/models/contest.py:391 +#: judge/models/contest.py:601 msgid "Create private contests" msgstr "" -#: judge/models/contest.py:392 +#: judge/models/contest.py:602 msgid "Change contest visibility" msgstr "" -#: judge/models/contest.py:393 +#: judge/models/contest.py:603 msgid "Edit contest problem label script" msgstr "" -#: judge/models/contest.py:395 judge/models/contest.py:493 -#: judge/models/contest.py:530 judge/models/contest.py:554 -#: judge/models/submission.py:84 +#: judge/models/contest.py:605 judge/models/contest.py:743 +#: judge/models/contest.py:812 judge/models/contest.py:842 +#: judge/models/submission.py:116 msgid "contest" msgstr "kỳ thi" -#: judge/models/contest.py:396 +#: judge/models/contest.py:606 msgid "contests" msgstr "kỳ thi" -#: judge/models/contest.py:403 +#: judge/models/contest.py:615 msgid "associated contest" msgstr "" -#: judge/models/contest.py:406 +#: judge/models/contest.py:628 msgid "score" msgstr "điểm" -#: judge/models/contest.py:407 +#: judge/models/contest.py:629 msgid "cumulative time" msgstr "tổng thời gian" -#: judge/models/contest.py:408 +#: judge/models/contest.py:631 msgid "is disqualified" msgstr "đã bị loại" -#: judge/models/contest.py:409 +#: judge/models/contest.py:633 msgid "Whether this participation is disqualified." msgstr "Quyết định thí sinh có bị loại không." -#: judge/models/contest.py:410 +#: judge/models/contest.py:635 msgid "tie-breaking field" msgstr "" -#: judge/models/contest.py:411 +#: judge/models/contest.py:637 msgid "virtual participation id" msgstr "id lần tham gia ảo" -#: judge/models/contest.py:412 +#: judge/models/contest.py:639 msgid "0 means non-virtual, otherwise the n-th virtual participation." msgstr "0 nghĩa là tham gia chính thức, ngược lại là lần tham gia ảo thứ n." -#: judge/models/contest.py:413 +#: judge/models/contest.py:642 msgid "contest format specific data" msgstr "" -#: judge/models/contest.py:479 +#: judge/models/contest.py:718 #, python-format msgid "%s spectating in %s" msgstr "%s đang theo dõi trong %s" -#: judge/models/contest.py:481 +#: judge/models/contest.py:723 #, python-format msgid "%s in %s, v%d" msgstr "%s trong %s, v%d" -#: judge/models/contest.py:482 +#: judge/models/contest.py:728 #, python-format msgid "%s in %s" msgstr "%s trong %s" -#: judge/models/contest.py:485 +#: judge/models/contest.py:731 msgid "contest participation" msgstr "lần tham gia kỳ thi" -#: judge/models/contest.py:486 +#: judge/models/contest.py:732 msgid "contest participations" msgstr "lần tham gia kỳ thi" -#: judge/models/contest.py:492 judge/models/contest.py:514 -#: judge/models/contest.py:555 judge/models/problem.py:412 -#: judge/models/problem.py:417 judge/models/problem.py:435 -#: judge/models/problem_data.py:41 +#: judge/models/contest.py:739 judge/models/contest.py:783 +#: judge/models/contest.py:845 judge/models/problem.py:599 +#: judge/models/problem.py:606 judge/models/problem.py:637 +#: judge/models/problem_data.py:49 msgid "problem" msgstr "bài tập" -#: judge/models/contest.py:494 judge/models/contest.py:518 -#: judge/models/problem.py:130 +#: judge/models/contest.py:747 judge/models/contest.py:795 +#: judge/models/problem.py:227 msgid "points" msgstr "điểm" -#: judge/models/contest.py:495 +#: judge/models/contest.py:748 msgid "partial" msgstr "thành phần" -#: judge/models/contest.py:496 judge/models/contest.py:519 +#: judge/models/contest.py:749 judge/models/contest.py:797 msgid "is pretested" msgstr "dùng pretest" -#: judge/models/contest.py:497 judge/models/interface.py:43 +#: judge/models/contest.py:750 judge/models/interface.py:43 msgid "order" msgstr "thứ tự" -#: judge/models/contest.py:498 +#: judge/models/contest.py:752 msgid "0 to not show testcases, 1 to show" msgstr "0 để ẩn test, 1 để hiện" -#: judge/models/contest.py:499 +#: judge/models/contest.py:753 msgid "visible testcases" msgstr "hiển thị test" -#: judge/models/contest.py:500 +#: judge/models/contest.py:760 msgid "Maximum number of submissions for this problem, or 0 for no limit." msgstr "Số lần nộp tối đa, đặt là 0 nếu không có giới hạn." -#: judge/models/contest.py:502 +#: judge/models/contest.py:764 msgid "Why include a problem you can't submit to?" msgstr "" -#: judge/models/contest.py:507 +#: judge/models/contest.py:770 msgid "contest problem" msgstr "bài trong kỳ thi" -#: judge/models/contest.py:508 +#: judge/models/contest.py:771 msgid "contest problems" msgstr "bài trong kỳ thi" -#: judge/models/contest.py:512 judge/models/submission.py:183 +#: judge/models/contest.py:777 judge/models/submission.py:234 msgid "submission" msgstr "bài nộp" -#: judge/models/contest.py:516 judge/models/contest.py:531 +#: judge/models/contest.py:790 judge/models/contest.py:816 msgid "participation" msgstr "lần tham gia" -#: judge/models/contest.py:520 +#: judge/models/contest.py:798 msgid "Whether this submission was ran only on pretests." msgstr "Quyết định bài nộp chỉ được chạy trên pretest không." -#: judge/models/contest.py:524 +#: judge/models/contest.py:803 msgid "contest submission" msgstr "bài nộp kỳ thi" -#: judge/models/contest.py:525 +#: judge/models/contest.py:804 msgid "contest submissions" msgstr "bài nộp kỳ thi" -#: judge/models/contest.py:533 +#: judge/models/contest.py:820 msgid "rank" msgstr "rank" -#: judge/models/contest.py:534 +#: judge/models/contest.py:821 msgid "rating" msgstr "rating" -#: judge/models/contest.py:535 +#: judge/models/contest.py:822 msgid "raw rating" msgstr "rating thật" -#: judge/models/contest.py:536 +#: judge/models/contest.py:823 msgid "contest performance" msgstr "" -#: judge/models/contest.py:537 +#: judge/models/contest.py:824 msgid "last rated" msgstr "lần cuối được xếp hạng" -#: judge/models/contest.py:541 +#: judge/models/contest.py:828 msgid "contest rating" msgstr "rating kỳ thi" -#: judge/models/contest.py:542 +#: judge/models/contest.py:829 msgid "contest ratings" msgstr "rating kỳ thi" -#: judge/models/contest.py:562 +#: judge/models/contest.py:853 msgid "contest moss result" msgstr "kết quả MOSS kỳ thi" -#: judge/models/contest.py:563 +#: judge/models/contest.py:854 msgid "contest moss results" msgstr "kết quả MOSS kỳ thi" @@ -1196,63 +1196,63 @@ msgstr "" msgid "label" msgstr "nhãn" -#: judge/models/interface.py:47 +#: judge/models/interface.py:48 msgid "highlight regex" msgstr "" -#: judge/models/interface.py:48 +#: judge/models/interface.py:52 msgid "parent item" msgstr "mục cha" -#: judge/models/interface.py:66 +#: judge/models/interface.py:74 msgid "post title" msgstr "tiêu đề bài đăng" -#: judge/models/interface.py:67 judge/models/problem.py:455 +#: judge/models/interface.py:75 judge/models/problem.py:676 msgid "authors" msgstr "tác giả" -#: judge/models/interface.py:68 +#: judge/models/interface.py:76 msgid "slug" msgstr "slug" -#: judge/models/interface.py:69 judge/models/problem.py:453 +#: judge/models/interface.py:77 judge/models/problem.py:674 msgid "public visibility" msgstr "khả năng hiển thị công khai" -#: judge/models/interface.py:70 +#: judge/models/interface.py:78 msgid "sticky" msgstr "nổi lên đầu" -#: judge/models/interface.py:71 +#: judge/models/interface.py:79 msgid "publish after" msgstr "đăng sau khi" -#: judge/models/interface.py:72 +#: judge/models/interface.py:80 msgid "post content" msgstr "đăng nội dung" -#: judge/models/interface.py:73 +#: judge/models/interface.py:81 msgid "post summary" msgstr "đăng tổng kết" -#: judge/models/interface.py:74 +#: judge/models/interface.py:83 msgid "openGraph image" msgstr "hình ảnh openGraph" -#: judge/models/interface.py:76 +#: judge/models/interface.py:89 msgid "If private, only these organizations may see the blog post." msgstr "Nếu riêng tư, chỉ những tổ chức này thấy được bài đăng." -#: judge/models/interface.py:105 +#: judge/models/interface.py:129 msgid "Edit all posts" msgstr "Chỉnh sửa tất cả bài đăng" -#: judge/models/interface.py:107 +#: judge/models/interface.py:130 msgid "blog post" msgstr "bài đăng" -#: judge/models/interface.py:108 +#: judge/models/interface.py:131 msgid "blog posts" msgstr "bài đăng" @@ -1260,440 +1260,440 @@ msgstr "bài đăng" msgid "message title" msgstr "tiêu đề tin nhắn" -#: judge/models/message.py:12 judge/models/ticket.py:29 +#: judge/models/message.py:12 judge/models/ticket.py:48 msgid "message body" msgstr "nội dung tin nhắn" -#: judge/models/message.py:13 +#: judge/models/message.py:15 msgid "sender" msgstr "người gửi" -#: judge/models/message.py:14 +#: judge/models/message.py:21 msgid "target" msgstr "người nhận" -#: judge/models/message.py:15 +#: judge/models/message.py:26 msgid "message timestamp" msgstr "thời gian gửi" -#: judge/models/message.py:20 +#: judge/models/message.py:33 msgid "messages in the thread" msgstr "tin nhắn trong chuỗi" -#: judge/models/problem.py:27 +#: judge/models/problem.py:37 msgid "problem category ID" msgstr "mã của nhóm bài" -#: judge/models/problem.py:28 +#: judge/models/problem.py:40 msgid "problem category name" msgstr "tên nhóm bài" -#: judge/models/problem.py:35 +#: judge/models/problem.py:48 msgid "problem type" msgstr "dạng bài" -#: judge/models/problem.py:36 judge/models/problem.py:114 -#: judge/models/volunteer.py:17 +#: judge/models/problem.py:49 judge/models/problem.py:194 +#: judge/models/volunteer.py:28 msgid "problem types" msgstr "dạng bài" -#: judge/models/problem.py:40 +#: judge/models/problem.py:54 msgid "problem group ID" msgstr "mã của nhóm bài" -#: judge/models/problem.py:41 +#: judge/models/problem.py:56 msgid "problem group name" msgstr "tên nhóm bài" -#: judge/models/problem.py:48 judge/models/problem.py:117 +#: judge/models/problem.py:63 judge/models/problem.py:199 msgid "problem group" msgstr "nhóm bài" -#: judge/models/problem.py:49 +#: judge/models/problem.py:64 msgid "problem groups" msgstr "nhóm bài" -#: judge/models/problem.py:53 +#: judge/models/problem.py:71 msgid "key" msgstr "" -#: judge/models/problem.py:55 +#: judge/models/problem.py:74 msgid "link" msgstr "đường dẫn" -#: judge/models/problem.py:56 +#: judge/models/problem.py:75 msgid "full name" msgstr "tên đầy đủ" -#: judge/models/problem.py:57 judge/models/profile.py:33 -#: judge/models/runtime.py:24 +#: judge/models/problem.py:79 judge/models/profile.py:38 +#: judge/models/runtime.py:34 msgid "short name" msgstr "tên ngắn" -#: judge/models/problem.py:58 +#: judge/models/problem.py:80 msgid "Displayed on pages under this license" msgstr "Được hiển thị trên các trang theo giấy phép này" -#: judge/models/problem.py:59 +#: judge/models/problem.py:85 msgid "icon" msgstr "icon" -#: judge/models/problem.py:59 +#: judge/models/problem.py:86 msgid "URL to the icon" msgstr "Đường dẫn icon" -#: judge/models/problem.py:60 +#: judge/models/problem.py:88 msgid "license text" msgstr "văn bản giấy phép" -#: judge/models/problem.py:69 +#: judge/models/problem.py:97 msgid "license" msgstr "" -#: judge/models/problem.py:70 +#: judge/models/problem.py:98 msgid "licenses" msgstr "" -#: judge/models/problem.py:97 +#: judge/models/problem.py:148 msgid "problem code" msgstr "" -#: judge/models/problem.py:99 +#: judge/models/problem.py:154 msgid "A short, unique code for the problem, used in the url after /problem/" msgstr "" -#: judge/models/problem.py:101 +#: judge/models/problem.py:159 msgid "problem name" msgstr "" -#: judge/models/problem.py:102 +#: judge/models/problem.py:161 msgid "The full name of the problem, as shown in the problem list." msgstr "" -#: judge/models/problem.py:104 +#: judge/models/problem.py:163 msgid "problem body" msgstr "" -#: judge/models/problem.py:105 +#: judge/models/problem.py:166 msgid "creators" msgstr "" -#: judge/models/problem.py:106 +#: judge/models/problem.py:170 msgid "These users will be able to edit the problem, and be listed as authors." msgstr "" -#: judge/models/problem.py:108 +#: judge/models/problem.py:175 msgid "curators" msgstr "" -#: judge/models/problem.py:109 +#: judge/models/problem.py:179 msgid "" "These users will be able to edit the problem, but not be listed as authors." msgstr "" -#: judge/models/problem.py:111 +#: judge/models/problem.py:185 msgid "testers" msgstr "" -#: judge/models/problem.py:113 +#: judge/models/problem.py:189 msgid "These users will be able to view the private problem, but not edit it." msgstr "" -#: judge/models/problem.py:115 judge/models/volunteer.py:18 +#: judge/models/problem.py:195 judge/models/volunteer.py:29 msgid "The type of problem, as shown on the problem's page." msgstr "" -#: judge/models/problem.py:118 +#: judge/models/problem.py:201 msgid "The group of problem, shown under Category in the problem list." msgstr "" -#: judge/models/problem.py:120 +#: judge/models/problem.py:206 msgid "" "The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) " "are supported." msgstr "" -#: judge/models/problem.py:124 judge/models/problem.py:440 +#: judge/models/problem.py:215 judge/models/problem.py:652 msgid "memory limit" msgstr "" -#: judge/models/problem.py:125 +#: judge/models/problem.py:217 msgid "" "The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 " "kilobytes)." msgstr "" -#: judge/models/problem.py:131 +#: judge/models/problem.py:229 msgid "" "Points awarded for problem completion. Points are displayed with a 'p' " "suffix if partial." msgstr "" -#: judge/models/problem.py:134 +#: judge/models/problem.py:235 msgid "allows partial points" msgstr "" -#: judge/models/problem.py:135 +#: judge/models/problem.py:239 msgid "allowed languages" msgstr "" -#: judge/models/problem.py:136 +#: judge/models/problem.py:240 msgid "List of allowed submission languages." msgstr "" -#: judge/models/problem.py:138 +#: judge/models/problem.py:246 msgid "manually managed" msgstr "" -#: judge/models/problem.py:139 +#: judge/models/problem.py:249 msgid "Whether judges should be allowed to manage data or not." msgstr "" -#: judge/models/problem.py:140 +#: judge/models/problem.py:252 msgid "date of publishing" msgstr "" -#: judge/models/problem.py:141 +#: judge/models/problem.py:257 msgid "" "Doesn't have magic ability to auto-publish due to backward compatibility" msgstr "" -#: judge/models/problem.py:143 +#: judge/models/problem.py:264 msgid "Bans the selected users from submitting to this problem." msgstr "" -#: judge/models/problem.py:145 +#: judge/models/problem.py:271 msgid "The license under which this problem is published." msgstr "" -#: judge/models/problem.py:147 +#: judge/models/problem.py:278 msgid "problem summary" msgstr "" -#: judge/models/problem.py:149 +#: judge/models/problem.py:284 msgid "number of users" msgstr "" -#: judge/models/problem.py:150 +#: judge/models/problem.py:286 msgid "The number of users who solved the problem." msgstr "" -#: judge/models/problem.py:151 +#: judge/models/problem.py:288 msgid "solve rate" msgstr "" -#: judge/models/problem.py:157 +#: judge/models/problem.py:297 msgid "If private, only these organizations may see the problem." msgstr "" -#: judge/models/problem.py:418 judge/models/problem.py:436 -#: judge/models/runtime.py:111 +#: judge/models/problem.py:611 judge/models/problem.py:642 +#: judge/models/runtime.py:161 msgid "language" msgstr "" -#: judge/models/problem.py:419 +#: judge/models/problem.py:614 msgid "translated name" msgstr "" -#: judge/models/problem.py:420 +#: judge/models/problem.py:616 msgid "translated description" msgstr "" -#: judge/models/problem.py:424 +#: judge/models/problem.py:620 msgid "problem translation" msgstr "" -#: judge/models/problem.py:425 +#: judge/models/problem.py:621 msgid "problem translations" msgstr "" -#: judge/models/problem.py:429 +#: judge/models/problem.py:626 msgid "clarified problem" msgstr "" -#: judge/models/problem.py:430 +#: judge/models/problem.py:628 msgid "clarification body" msgstr "" -#: judge/models/problem.py:431 +#: judge/models/problem.py:630 msgid "clarification timestamp" msgstr "" -#: judge/models/problem.py:446 +#: judge/models/problem.py:661 msgid "language-specific resource limit" msgstr "" -#: judge/models/problem.py:447 +#: judge/models/problem.py:662 msgid "language-specific resource limits" msgstr "" -#: judge/models/problem.py:451 +#: judge/models/problem.py:669 msgid "associated problem" msgstr "" -#: judge/models/problem.py:454 +#: judge/models/problem.py:675 msgid "publish date" msgstr "" -#: judge/models/problem.py:456 +#: judge/models/problem.py:677 msgid "editorial content" msgstr "nội dung lời giải" -#: judge/models/problem.py:472 +#: judge/models/problem.py:691 msgid "solution" msgstr "lời giải" -#: judge/models/problem.py:473 +#: judge/models/problem.py:692 msgid "solutions" msgstr "lời giải" -#: judge/models/problem.py:478 +#: judge/models/problem.py:697 #, fuzzy #| msgid "point value" msgid "proposed point value" msgstr "điểm" -#: judge/models/problem.py:479 +#: judge/models/problem.py:698 msgid "The amount of points you think this problem deserves." msgstr "" -#: judge/models/problem.py:489 +#: judge/models/problem.py:712 msgid "The time this vote was cast" msgstr "" -#: judge/models/problem.py:495 +#: judge/models/problem.py:718 msgid "vote" msgstr "" -#: judge/models/problem_data.py:26 +#: judge/models/problem_data.py:32 msgid "Standard" msgstr "Tiêu chuẩn" -#: judge/models/problem_data.py:27 +#: judge/models/problem_data.py:33 msgid "Floats" msgstr "Số thực" -#: judge/models/problem_data.py:28 +#: judge/models/problem_data.py:34 msgid "Floats (absolute)" msgstr "Số thực (chênh lệch tuyệt đối)" -#: judge/models/problem_data.py:29 +#: judge/models/problem_data.py:35 msgid "Floats (relative)" msgstr "Số thực (chênh lệch tương đối)" -#: judge/models/problem_data.py:30 +#: judge/models/problem_data.py:36 msgid "Non-trailing spaces" msgstr "Không cho phép dấu cách cuối dòng" -#: judge/models/problem_data.py:31 +#: judge/models/problem_data.py:37 msgid "Unordered" msgstr "Không thứ tự" -#: judge/models/problem_data.py:32 +#: judge/models/problem_data.py:38 msgid "Byte identical" msgstr "Giống từng byte" -#: judge/models/problem_data.py:33 +#: judge/models/problem_data.py:39 msgid "Line-by-line" msgstr "Chấm theo dòng (điểm = số dòng đúng)" -#: judge/models/problem_data.py:34 +#: judge/models/problem_data.py:40 msgid "Custom checker (PY)" msgstr "Trình chấm tự viết (Python)" -#: judge/models/problem_data.py:35 +#: judge/models/problem_data.py:41 msgid "Custom validator (CPP)" msgstr "Trình chấm tự viết (C++)" -#: judge/models/problem_data.py:36 +#: judge/models/problem_data.py:42 msgid "Interactive" msgstr "" -#: judge/models/problem_data.py:43 +#: judge/models/problem_data.py:54 msgid "data zip file" msgstr "file zip chứa test" -#: judge/models/problem_data.py:45 +#: judge/models/problem_data.py:61 msgid "generator file" msgstr "file tạo test" -#: judge/models/problem_data.py:47 judge/models/problem_data.py:129 +#: judge/models/problem_data.py:68 judge/models/problem_data.py:190 msgid "output prefix length" msgstr "độ dài hiển thị output" -#: judge/models/problem_data.py:48 judge/models/problem_data.py:130 +#: judge/models/problem_data.py:71 judge/models/problem_data.py:193 msgid "output limit length" msgstr "giới hạn hiển thị output" -#: judge/models/problem_data.py:49 +#: judge/models/problem_data.py:74 msgid "init.yml generation feedback" msgstr "phản hồi của quá trình tạo file init.yml" -#: judge/models/problem_data.py:50 judge/models/problem_data.py:131 +#: judge/models/problem_data.py:77 judge/models/problem_data.py:196 msgid "checker" msgstr "trình chấm" -#: judge/models/problem_data.py:51 judge/models/problem_data.py:132 +#: judge/models/problem_data.py:80 judge/models/problem_data.py:199 msgid "checker arguments" msgstr "các biến trong trình chấm" -#: judge/models/problem_data.py:52 judge/models/problem_data.py:133 +#: judge/models/problem_data.py:82 judge/models/problem_data.py:201 msgid "checker arguments as a JSON object" msgstr "các biến trong trình chấm theo dạng JSON" -#: judge/models/problem_data.py:53 +#: judge/models/problem_data.py:85 msgid "custom checker file" msgstr "file trình chấm" -#: judge/models/problem_data.py:59 +#: judge/models/problem_data.py:93 msgid "custom validator file" msgstr "file trình chấm" -#: judge/models/problem_data.py:65 +#: judge/models/problem_data.py:101 msgid "interactive judge" msgstr "" -#: judge/models/problem_data.py:116 +#: judge/models/problem_data.py:165 msgid "problem data set" msgstr "tập hợp dữ liệu bài" -#: judge/models/problem_data.py:118 +#: judge/models/problem_data.py:169 msgid "case position" msgstr "vị trí test" -#: judge/models/problem_data.py:119 +#: judge/models/problem_data.py:172 msgid "case type" msgstr "loại test" -#: judge/models/problem_data.py:120 +#: judge/models/problem_data.py:174 msgid "Normal case" msgstr "Test bình thường" -#: judge/models/problem_data.py:121 +#: judge/models/problem_data.py:175 msgid "Batch start" msgstr "Bắt đầu nhóm" -#: judge/models/problem_data.py:122 +#: judge/models/problem_data.py:176 msgid "Batch end" msgstr "Kết thúc nhóm" -#: judge/models/problem_data.py:124 +#: judge/models/problem_data.py:181 msgid "input file name" msgstr "tên file input" -#: judge/models/problem_data.py:125 +#: judge/models/problem_data.py:184 msgid "output file name" msgstr "tên file output" -#: judge/models/problem_data.py:126 +#: judge/models/problem_data.py:186 msgid "generator arguments" msgstr "biến trong file sinh test" -#: judge/models/problem_data.py:127 +#: judge/models/problem_data.py:187 msgid "point value" msgstr "điểm" -#: judge/models/problem_data.py:128 +#: judge/models/problem_data.py:188 msgid "case is pretest?" msgstr "test là pretest?" @@ -1701,372 +1701,372 @@ msgstr "test là pretest?" msgid "organization title" msgstr "tiêu đề tổ chức" -#: judge/models/profile.py:31 +#: judge/models/profile.py:33 msgid "organization slug" msgstr "tên ngắn tổ chức" -#: judge/models/profile.py:32 +#: judge/models/profile.py:34 msgid "Organization name shown in URL" msgstr "Tên được hiển thị trong đường dẫn" -#: judge/models/profile.py:34 +#: judge/models/profile.py:39 msgid "Displayed beside user name during contests" msgstr "Hiển thị bên cạnh tên người dùng trong kỳ thi" -#: judge/models/profile.py:35 +#: judge/models/profile.py:41 msgid "organization description" msgstr "mô tả tổ chức" -#: judge/models/profile.py:36 +#: judge/models/profile.py:44 msgid "registrant" msgstr "người tạo" -#: judge/models/profile.py:37 +#: judge/models/profile.py:47 msgid "User who registered this organization" msgstr "Người tạo tổ chức" -#: judge/models/profile.py:38 +#: judge/models/profile.py:51 msgid "administrators" msgstr "người quản lý" -#: judge/models/profile.py:39 +#: judge/models/profile.py:53 msgid "Those who can edit this organization" msgstr "Những người có thể chỉnh sửa tổ chức" -#: judge/models/profile.py:40 +#: judge/models/profile.py:56 msgid "creation date" msgstr "ngày tạo" -#: judge/models/profile.py:41 +#: judge/models/profile.py:59 msgid "is open organization?" msgstr "tổ chức mở?" -#: judge/models/profile.py:42 +#: judge/models/profile.py:60 msgid "Allow joining organization" msgstr "Cho phép tham gia tổ chức" -#: judge/models/profile.py:43 +#: judge/models/profile.py:64 msgid "maximum size" msgstr "số lượng thành viên tối đa" -#: judge/models/profile.py:44 +#: judge/models/profile.py:68 msgid "" "Maximum amount of users in this organization, only applicable to private " "organizations" msgstr "Số người tối đa trong tổ chức, chỉ áp dụng với tổ chức riêng tư" -#: judge/models/profile.py:46 +#: judge/models/profile.py:74 msgid "Student access code" msgstr "Mã truy cập cho học sinh" -#: judge/models/profile.py:50 +#: judge/models/profile.py:85 msgid "" "This image will replace the default site logo for users viewing the " "organization." msgstr "Ảnh này sẽ thay thế logo mặc định khi ở trong tổ chức." -#: judge/models/profile.py:76 judge/models/profile.py:93 -#: judge/models/profile.py:221 +#: judge/models/profile.py:115 judge/models/profile.py:144 +#: judge/models/profile.py:344 msgid "organization" msgstr "" -#: judge/models/profile.py:81 +#: judge/models/profile.py:121 msgid "user associated" msgstr "" -#: judge/models/profile.py:82 +#: judge/models/profile.py:123 msgid "self-description" msgstr "" -#: judge/models/profile.py:83 +#: judge/models/profile.py:126 msgid "location" msgstr "" -#: judge/models/profile.py:85 +#: judge/models/profile.py:132 msgid "preferred language" msgstr "" -#: judge/models/profile.py:91 +#: judge/models/profile.py:140 msgid "last access time" msgstr "" -#: judge/models/profile.py:92 +#: judge/models/profile.py:141 msgid "last IP" msgstr "" -#: judge/models/profile.py:95 +#: judge/models/profile.py:152 msgid "display rank" msgstr "" -#: judge/models/profile.py:97 +#: judge/models/profile.py:160 msgid "comment mute" msgstr "" -#: judge/models/profile.py:97 +#: judge/models/profile.py:161 msgid "Some users are at their best when silent." msgstr "" -#: judge/models/profile.py:99 +#: judge/models/profile.py:165 msgid "unlisted user" msgstr "" -#: judge/models/profile.py:99 +#: judge/models/profile.py:166 msgid "User will not be ranked." msgstr "" -#: judge/models/profile.py:102 +#: judge/models/profile.py:170 #, fuzzy #| msgid "Banned from joining" msgid "banned from voting" msgstr "Bị cấm tham gia" -#: judge/models/profile.py:103 +#: judge/models/profile.py:171 msgid "User will not be able to vote on problems' point values." msgstr "" -#: judge/models/profile.py:107 +#: judge/models/profile.py:176 msgid "user script" msgstr "" -#: judge/models/profile.py:108 +#: judge/models/profile.py:180 msgid "User-defined JavaScript for site customization." msgstr "" -#: judge/models/profile.py:109 +#: judge/models/profile.py:184 msgid "current contest" msgstr "kỳ thi hiện tại" -#: judge/models/profile.py:111 +#: judge/models/profile.py:191 msgid "math engine" msgstr "" -#: judge/models/profile.py:113 +#: judge/models/profile.py:195 msgid "the rendering engine used to render math" msgstr "" -#: judge/models/profile.py:114 +#: judge/models/profile.py:198 msgid "2FA enabled" msgstr "" -#: judge/models/profile.py:115 +#: judge/models/profile.py:200 msgid "check to enable TOTP-based two factor authentication" msgstr "đánh dấu để sử dụng TOTP-based two factor authentication" -#: judge/models/profile.py:116 +#: judge/models/profile.py:206 msgid "TOTP key" msgstr "mã TOTP" -#: judge/models/profile.py:117 +#: judge/models/profile.py:207 msgid "32 character base32-encoded key for TOTP" msgstr "" -#: judge/models/profile.py:119 +#: judge/models/profile.py:209 msgid "TOTP key must be empty or base32" msgstr "" -#: judge/models/profile.py:120 +#: judge/models/profile.py:213 msgid "internal notes" msgstr "ghi chú nội bộ" -#: judge/models/profile.py:121 +#: judge/models/profile.py:216 msgid "Notes for administrators regarding this user." msgstr "Ghi chú riêng cho quản trị viên." -#: judge/models/profile.py:215 +#: judge/models/profile.py:331 msgid "user profile" msgstr "thông tin người dùng" -#: judge/models/profile.py:216 +#: judge/models/profile.py:332 msgid "user profiles" msgstr "thông tin người dùng" -#: judge/models/profile.py:223 +#: judge/models/profile.py:348 msgid "request time" msgstr "thời gian đăng ký" -#: judge/models/profile.py:224 +#: judge/models/profile.py:351 msgid "state" msgstr "trạng thái" -#: judge/models/profile.py:229 +#: judge/models/profile.py:358 msgid "reason" msgstr "lý do" -#: judge/models/profile.py:232 +#: judge/models/profile.py:361 msgid "organization join request" msgstr "đơn đăng ký tham gia" -#: judge/models/profile.py:233 +#: judge/models/profile.py:362 msgid "organization join requests" msgstr "đơn đăng ký tham gia" -#: judge/models/runtime.py:19 +#: judge/models/runtime.py:21 msgid "short identifier" msgstr "tên ngắn" -#: judge/models/runtime.py:20 +#: judge/models/runtime.py:23 msgid "" "The identifier for this language; the same as its executor id for judges." msgstr "" -#: judge/models/runtime.py:22 +#: judge/models/runtime.py:29 msgid "long name" msgstr "tên dài" -#: judge/models/runtime.py:23 +#: judge/models/runtime.py:30 msgid "Longer name for the language, e.g. \"Python 2\" or \"C++11\"." msgstr "Tên dài, ví dụ \"Python 2\" or \"C++11\"." -#: judge/models/runtime.py:25 +#: judge/models/runtime.py:36 msgid "" "More readable, but short, name to display publicly; e.g. \"PY2\" or \"C+" "+11\". If left blank, it will default to the short identifier." msgstr "" -#: judge/models/runtime.py:29 +#: judge/models/runtime.py:45 msgid "common name" msgstr "" -#: judge/models/runtime.py:30 +#: judge/models/runtime.py:47 msgid "" "Common name for the language. For example, the common name for C++03, C++11, " "and C++14 would be \"C++\"" msgstr "" -#: judge/models/runtime.py:32 +#: judge/models/runtime.py:53 msgid "ace mode name" msgstr "" -#: judge/models/runtime.py:33 +#: judge/models/runtime.py:55 msgid "" "Language ID for Ace.js editor highlighting, appended to \"mode-\" to " "determine the Ace JavaScript file to use, e.g., \"python\"." msgstr "" -#: judge/models/runtime.py:35 +#: judge/models/runtime.py:61 msgid "pygments name" msgstr "" -#: judge/models/runtime.py:36 +#: judge/models/runtime.py:62 msgid "Language ID for Pygments highlighting in source windows." msgstr "" -#: judge/models/runtime.py:37 +#: judge/models/runtime.py:65 msgid "code template" msgstr "" -#: judge/models/runtime.py:38 +#: judge/models/runtime.py:66 msgid "Code template to display in submission editor." msgstr "" -#: judge/models/runtime.py:39 +#: judge/models/runtime.py:71 msgid "runtime info override" msgstr "" -#: judge/models/runtime.py:40 +#: judge/models/runtime.py:74 msgid "" "Do not set this unless you know what you're doing! It will override the " "usually more specific, judge-provided runtime info!" msgstr "" -#: judge/models/runtime.py:42 +#: judge/models/runtime.py:79 msgid "language description" msgstr "" -#: judge/models/runtime.py:43 +#: judge/models/runtime.py:81 msgid "" "Use this field to inform users of quirks with your environment, additional " "restrictions, etc." msgstr "" -#: judge/models/runtime.py:45 +#: judge/models/runtime.py:88 msgid "extension" msgstr "" -#: judge/models/runtime.py:46 +#: judge/models/runtime.py:89 msgid "The extension of source files, e.g., \"py\" or \"cpp\"." msgstr "" -#: judge/models/runtime.py:112 +#: judge/models/runtime.py:162 msgid "languages" msgstr "ngôn ngữ" -#: judge/models/runtime.py:116 +#: judge/models/runtime.py:168 msgid "language to which this runtime belongs" msgstr "" -#: judge/models/runtime.py:117 +#: judge/models/runtime.py:172 msgid "judge on which this runtime exists" msgstr "" -#: judge/models/runtime.py:118 +#: judge/models/runtime.py:174 msgid "runtime name" msgstr "" -#: judge/models/runtime.py:119 +#: judge/models/runtime.py:176 msgid "runtime version" msgstr "" -#: judge/models/runtime.py:120 +#: judge/models/runtime.py:179 msgid "order in which to display this runtime" msgstr "" -#: judge/models/runtime.py:124 +#: judge/models/runtime.py:185 msgid "Server name, hostname-style" msgstr "Tên web" -#: judge/models/runtime.py:125 +#: judge/models/runtime.py:188 msgid "time of creation" msgstr "ngày tạo" -#: judge/models/runtime.py:126 +#: judge/models/runtime.py:192 msgid "A key to authenticate this judge" msgstr "Chìa khóa xác thực" -#: judge/models/runtime.py:127 +#: judge/models/runtime.py:193 msgid "authentication key" msgstr "mã xác thực" -#: judge/models/runtime.py:128 +#: judge/models/runtime.py:196 msgid "block judge" msgstr "chặn máy chấm" -#: judge/models/runtime.py:129 +#: judge/models/runtime.py:199 msgid "" "Whether this judge should be blocked from connecting, even if its key is " "correct." msgstr "Quyết định có chặn máy chấm, ngay cả khi mã xác thực đúng." -#: judge/models/runtime.py:131 +#: judge/models/runtime.py:203 msgid "judge online status" msgstr "trạng thái online của máy chấm" -#: judge/models/runtime.py:132 +#: judge/models/runtime.py:204 msgid "judge start time" msgstr "thời gian khởi đầu máy chấm" -#: judge/models/runtime.py:133 +#: judge/models/runtime.py:205 msgid "response time" msgstr "thời gian trả lời" -#: judge/models/runtime.py:134 +#: judge/models/runtime.py:207 msgid "system load" msgstr "lưu lượng xử lý" -#: judge/models/runtime.py:135 +#: judge/models/runtime.py:209 msgid "Load for the last minute, divided by processors to be fair." msgstr "Lưu lượng được chia đều." -#: judge/models/runtime.py:139 judge/models/runtime.py:179 +#: judge/models/runtime.py:219 judge/models/runtime.py:261 msgid "judges" msgstr "máy chấm" -#: judge/models/runtime.py:178 +#: judge/models/runtime.py:260 msgid "judge" msgstr "máy chấm" #: judge/models/submission.py:20 judge/models/submission.py:47 -#: judge/utils/problems.py:79 +#: judge/utils/problems.py:116 msgid "Accepted" msgstr "Accepted" @@ -2095,7 +2095,7 @@ msgid "Runtime Error" msgstr "Runtime Error" #: judge/models/submission.py:27 judge/models/submission.py:41 -#: judge/models/submission.py:55 judge/utils/problems.py:81 +#: judge/models/submission.py:55 judge/utils/problems.py:120 msgid "Compile Error" msgstr "Compile Error" @@ -2132,111 +2132,111 @@ msgstr "Chấm xong" msgid "Internal Error (judging server error)" msgstr "Lỗi máy chấm" -#: judge/models/submission.py:66 +#: judge/models/submission.py:67 msgid "submission time" msgstr "thời gian bài nộp" -#: judge/models/submission.py:67 judge/models/submission.py:203 +#: judge/models/submission.py:69 judge/models/submission.py:264 msgid "execution time" msgstr "thời gian chạy" -#: judge/models/submission.py:68 judge/models/submission.py:204 +#: judge/models/submission.py:70 judge/models/submission.py:265 msgid "memory usage" msgstr "bộ nhớ sử dụng" -#: judge/models/submission.py:69 judge/models/submission.py:205 +#: judge/models/submission.py:72 judge/models/submission.py:266 msgid "points granted" msgstr "điểm" -#: judge/models/submission.py:70 +#: judge/models/submission.py:75 msgid "submission language" msgstr "ngôn ngữ bài nộp" -#: judge/models/submission.py:71 +#: judge/models/submission.py:78 msgid "status" msgstr "trạng thái" -#: judge/models/submission.py:72 +#: judge/models/submission.py:85 msgid "result" msgstr "kết quả" -#: judge/models/submission.py:74 +#: judge/models/submission.py:93 msgid "compile errors" msgstr "biên dịch lỗi" -#: judge/models/submission.py:76 +#: judge/models/submission.py:95 msgid "batched cases" msgstr "nhóm test" -#: judge/models/submission.py:77 +#: judge/models/submission.py:96 msgid "test case points" msgstr "điểm test" -#: judge/models/submission.py:78 +#: judge/models/submission.py:97 msgid "test case total points" msgstr "tổng điểm các test" -#: judge/models/submission.py:79 +#: judge/models/submission.py:100 msgid "judged on" msgstr "chấm trên" -#: judge/models/submission.py:81 +#: judge/models/submission.py:106 msgid "submission judge time" msgstr "thời điểm được chấm" -#: judge/models/submission.py:82 +#: judge/models/submission.py:109 msgid "was rejudged by admin" msgstr "được chấm lại bởi admin" -#: judge/models/submission.py:83 +#: judge/models/submission.py:112 msgid "was ran on pretests only" msgstr "chỉ chấm pretest" -#: judge/models/submission.py:184 templates/contest/moss.html:58 +#: judge/models/submission.py:235 templates/contest/moss.html:58 msgid "submissions" msgstr "bài nộp" -#: judge/models/submission.py:188 judge/models/submission.py:199 +#: judge/models/submission.py:242 judge/models/submission.py:256 msgid "associated submission" msgstr "bài nộp tương ứng" -#: judge/models/submission.py:190 +#: judge/models/submission.py:245 msgid "source code" msgstr "mã nguồn" -#: judge/models/submission.py:201 +#: judge/models/submission.py:260 msgid "test case ID" msgstr "test case ID" -#: judge/models/submission.py:202 +#: judge/models/submission.py:262 msgid "status flag" msgstr "" -#: judge/models/submission.py:206 +#: judge/models/submission.py:267 msgid "points possible" msgstr "" -#: judge/models/submission.py:207 +#: judge/models/submission.py:268 msgid "batch number" msgstr "số thứ tự của nhóm" -#: judge/models/submission.py:208 +#: judge/models/submission.py:270 msgid "judging feedback" msgstr "phản hồi từ máy chấm" -#: judge/models/submission.py:209 +#: judge/models/submission.py:273 msgid "extended judging feedback" msgstr "phản hồi thêm từ máy chấm" -#: judge/models/submission.py:210 +#: judge/models/submission.py:275 msgid "program output" msgstr "output chương trình" -#: judge/models/submission.py:218 +#: judge/models/submission.py:283 msgid "submission test case" msgstr "cái testcase trong bài nộp" -#: judge/models/submission.py:219 +#: judge/models/submission.py:284 msgid "submission test cases" msgstr "cái testcase trong bài nộp" @@ -2244,394 +2244,394 @@ msgstr "cái testcase trong bài nộp" msgid "ticket title" msgstr "tiêu đề báo cáo" -#: judge/models/ticket.py:11 +#: judge/models/ticket.py:13 msgid "ticket creator" msgstr "người báo cáo" -#: judge/models/ticket.py:13 +#: judge/models/ticket.py:17 msgid "creation time" msgstr "thời gian tạo" -#: judge/models/ticket.py:14 +#: judge/models/ticket.py:19 msgid "assignees" msgstr "người được ủy thác" -#: judge/models/ticket.py:15 +#: judge/models/ticket.py:22 msgid "quick notes" msgstr "" -#: judge/models/ticket.py:16 +#: judge/models/ticket.py:24 msgid "Staff notes for this issue to aid in processing." msgstr "" -#: judge/models/ticket.py:17 +#: judge/models/ticket.py:27 msgid "linked item type" msgstr "" -#: judge/models/ticket.py:19 +#: judge/models/ticket.py:29 msgid "linked item ID" msgstr "" -#: judge/models/ticket.py:21 +#: judge/models/ticket.py:31 msgid "is ticket open?" msgstr "" -#: judge/models/ticket.py:25 +#: judge/models/ticket.py:37 msgid "ticket" msgstr "" -#: judge/models/ticket.py:27 +#: judge/models/ticket.py:44 msgid "poster" msgstr "" -#: judge/models/ticket.py:30 +#: judge/models/ticket.py:49 msgid "message time" msgstr "" -#: judge/models/volunteer.py:13 +#: judge/models/volunteer.py:19 msgid "knowledge points" msgstr "Độ khó kiến thức" -#: judge/models/volunteer.py:14 +#: judge/models/volunteer.py:20 msgid "Points awarded by knowledge difficulty" msgstr "" -#: judge/models/volunteer.py:15 +#: judge/models/volunteer.py:23 msgid "thinking points" msgstr "Độ khó nghĩ" -#: judge/models/volunteer.py:16 +#: judge/models/volunteer.py:24 msgid "Points awarded by thinking difficulty" msgstr "" -#: judge/models/volunteer.py:20 +#: judge/models/volunteer.py:31 msgid "feedback" msgstr "phản hồi" -#: judge/models/volunteer.py:23 +#: judge/models/volunteer.py:34 msgid "volunteer vote" msgstr "vote từ TNV" -#: judge/models/volunteer.py:24 +#: judge/models/volunteer.py:35 msgid "volunteer votes" msgstr "vote từ TNV" -#: judge/pdf_problems.py:147 judge/pdf_problems.py:199 -#: judge/pdf_problems.py:259 +#: judge/pdf_problems.py:161 judge/pdf_problems.py:221 +#: judge/pdf_problems.py:294 msgid "Page [page] of [topage]" msgstr "Trang [page]/[topage]" -#: judge/pdf_problems.py:280 +#: judge/pdf_problems.py:324 #, python-format msgid "Page %s of %s" msgstr "Trang %s/%s" -#: judge/tasks/contest.py:19 +#: judge/tasks/contest.py:20 msgid "Recalculating contest scores" msgstr "Tính lại điểm kỳ thi" -#: judge/tasks/contest.py:40 +#: judge/tasks/contest.py:42 msgid "Running MOSS" msgstr "Đang chạy MOSS" -#: judge/tasks/submission.py:43 +#: judge/tasks/submission.py:45 msgid "Modifying submissions" msgstr "Chỉnh sửa bài nộp" -#: judge/tasks/submission.py:56 +#: judge/tasks/submission.py:65 msgid "Recalculating user points" msgstr "Tính lại điểm người dùng" -#: judge/utils/problem_data.py:70 +#: judge/utils/problem_data.py:75 msgid "Empty batches not allowed." msgstr "Nhóm test trống là không hợp lệ." -#: judge/utils/problem_data.py:78 judge/utils/problem_data.py:99 +#: judge/utils/problem_data.py:84 judge/utils/problem_data.py:107 msgid "How did you corrupt the custom checker path?" msgstr "How did you corrupt the custom checker path?" -#: judge/utils/problem_data.py:120 +#: judge/utils/problem_data.py:130 #, python-format msgid "Points must be defined for non-batch case #%d." msgstr "Ô điểm số cho test #%d phải được điền." -#: judge/utils/problem_data.py:125 +#: judge/utils/problem_data.py:137 #, python-format msgid "Input file for case %d does not exist: %s" msgstr "File input cho test %d không tồn tại: %s" -#: judge/utils/problem_data.py:128 +#: judge/utils/problem_data.py:142 #, python-format msgid "Output file for case %d does not exist: %s" msgstr "File output cho test %d không tồn tại: %s" -#: judge/utils/problem_data.py:153 +#: judge/utils/problem_data.py:169 #, python-format msgid "Batch start case #%d requires points." msgstr "Nhóm test #%d cần được điền điểm số." -#: judge/utils/problem_data.py:174 +#: judge/utils/problem_data.py:192 #, python-format msgid "Attempt to end batch outside of one in case #%d" msgstr "Nhóm test #%d kết thúc không hợp lệ" -#: judge/utils/problem_data.py:192 +#: judge/utils/problem_data.py:211 msgid "How did you corrupt the zip path?" msgstr "" -#: judge/utils/problem_data.py:198 +#: judge/utils/problem_data.py:217 msgid "How did you corrupt the generator path?" msgstr "" -#: judge/utils/problems.py:80 +#: judge/utils/problems.py:117 msgid "Wrong" msgstr "Sai" -#: judge/utils/problems.py:82 +#: judge/utils/problems.py:123 msgid "Timeout" msgstr "Quá thời gian" -#: judge/utils/problems.py:83 +#: judge/utils/problems.py:126 msgid "Error" msgstr "Lỗi" -#: judge/utils/problems.py:94 +#: judge/utils/problems.py:143 msgid "Can't pass both queryset and keyword filters" msgstr "" -#: judge/utils/pwned.py:101 +#: judge/utils/pwned.py:102 msgid "Your password can't be a commonly used password." msgstr "Mật khẩu không được quá phổ biến." -#: judge/utils/pwned.py:102 +#: judge/utils/pwned.py:103 msgid "This password is too common." msgstr "Mật khẩu này quá phổ biến." -#: judge/utils/timedelta.py:49 +#: judge/utils/timedelta.py:59 msgctxt "time format with day" msgid "%d day %h:%m:%s" msgid_plural "%d days %h:%m:%s" msgstr[0] "%d ngày %h:%m:%s" -#: judge/utils/timedelta.py:53 +#: judge/utils/timedelta.py:68 msgctxt "time format without day" msgid "%h:%m:%s" msgstr "%h:%m:%s" -#: judge/utils/timedelta.py:59 +#: judge/utils/timedelta.py:80 msgctxt "time format no seconds with day" msgid "%d day %h:%m" msgid_plural "%d days %h:%m" msgstr[0] "%d ngày %h:%m" -#: judge/utils/timedelta.py:61 +#: judge/utils/timedelta.py:88 #, python-format msgid "%d day" msgid_plural "%d days" msgstr[0] "%d ngày" -#: judge/utils/timedelta.py:63 +#: judge/utils/timedelta.py:91 msgctxt "hours and minutes" msgid "%h:%m" msgstr "%h:%m" -#: judge/views/about.py:7 templates/organization/home.html:105 +#: judge/views/about.py:10 templates/organization/home.html:105 #: templates/user/user-about.html:83 templates/user/user-tabs.html:4 #: templates/user/users-table.html:32 msgid "About" msgstr "Giới thiệu" -#: judge/views/about.py:13 +#: judge/views/about.py:20 msgid "Custom Checker Sample" msgstr "Hướng dẫn viết trình chấm" -#: judge/views/blog.py:93 +#: judge/views/blog.py:128 #, python-format msgid "Page %d of Posts" msgstr "Trang %d" -#: judge/views/blog.py:134 +#: judge/views/blog.py:183 msgid "Ticket feed" msgstr "Báo cáo" -#: judge/views/blog.py:152 +#: judge/views/blog.py:201 msgid "Comment feed" msgstr "Bình luận" -#: judge/views/comment.py:28 +#: judge/views/comment.py:39 msgid "Messing around, are we?" msgstr "Messing around, are we?" -#: judge/views/comment.py:37 +#: judge/views/comment.py:55 msgid "You must solve at least one problem before you can vote." msgstr "Bạn phải giải ít nhất 1 bài trước khi được vote." -#: judge/views/comment.py:64 +#: judge/views/comment.py:86 msgid "You already voted." msgstr "Bạn đã vote." -#: judge/views/comment.py:126 judge/views/organization.py:344 +#: judge/views/comment.py:153 judge/views/organization.py:472 msgid "Edited from site" msgstr "Chỉnh sửa từ web" -#: judge/views/comment.py:147 +#: judge/views/comment.py:174 msgid "Editing comment" msgstr "Chỉnh sửa bình luận" -#: judge/views/contests.py:58 judge/views/contests.py:250 -#: judge/views/contests.py:253 judge/views/contests.py:427 +#: judge/views/contests.py:115 judge/views/contests.py:356 +#: judge/views/contests.py:361 judge/views/contests.py:602 msgid "No such contest" msgstr "Không có contest nào như vậy" -#: judge/views/contests.py:59 judge/views/contests.py:251 +#: judge/views/contests.py:116 judge/views/contests.py:357 #, python-format msgid "Could not find a contest with the key \"%s\"." msgstr "Không tìm thấy kỳ thi với mã \"%s\"." -#: judge/views/contests.py:72 +#: judge/views/contests.py:135 msgid "Contests" msgstr "Kỳ thi" -#: judge/views/contests.py:254 +#: judge/views/contests.py:361 msgid "Could not find such contest." msgstr "Không tìm thấy kỳ thi nào như vậy." -#: judge/views/contests.py:257 +#: judge/views/contests.py:369 #, python-format msgid "Access to contest \"%s\" denied" msgstr "Truy cập tới kỳ thi \"%s\" bị từ chối" -#: judge/views/contests.py:281 +#: judge/views/contests.py:407 msgid "Clone Contest" msgstr "Nhân bản kỳ thi" -#: judge/views/contests.py:346 +#: judge/views/contests.py:476 msgid "Contest not ongoing" msgstr "Kỳ thi đang không diễn ra" -#: judge/views/contests.py:347 +#: judge/views/contests.py:477 #, python-format msgid "\"%s\" is not currently ongoing." msgstr "\"%s\" kỳ thi đang không diễn ra." -#: judge/views/contests.py:351 +#: judge/views/contests.py:484 msgid "Already in contest" msgstr "Đã ở trong kỳ thi" -#: judge/views/contests.py:352 +#: judge/views/contests.py:485 #, python-format msgid "You are already in a contest: \"%s\"." msgstr "Bạn đã ở trong kỳ thi: \"%s\"." -#: judge/views/contests.py:355 +#: judge/views/contests.py:495 msgid "Banned from joining" msgstr "Bị cấm tham gia" -#: judge/views/contests.py:356 +#: judge/views/contests.py:497 msgid "" "You have been declared persona non grata for this contest. You are " "permanently barred from joining this contest." msgstr "Bạn không được phép tham gia kỳ thi này." -#: judge/views/contests.py:417 +#: judge/views/contests.py:586 #, python-format msgid "Enter access code for \"%s\"" msgstr "Nhập mật khẩu truy cập cho \"%s\"" -#: judge/views/contests.py:428 +#: judge/views/contests.py:603 #, python-format msgid "You are not in contest \"%s\"." msgstr "Bạn không ở trong kỳ thi \"%s\"." -#: judge/views/contests.py:448 +#: judge/views/contests.py:626 msgid "ContestCalendar requires integer year and month" msgstr "Lịch thi yêu cầu giá trị cho năm và tháng là số nguyên" -#: judge/views/contests.py:488 +#: judge/views/contests.py:684 #, python-format msgid "Contests in %(month)s" msgstr "Các kỳ thi trong %(month)s" -#: judge/views/contests.py:488 +#: judge/views/contests.py:685 msgid "F Y" msgstr "F Y" -#: judge/views/contests.py:536 +#: judge/views/contests.py:745 #, python-format msgid "%s Statistics" msgstr "%s Thống kê" -#: judge/views/contests.py:731 +#: judge/views/contests.py:1002 #, python-format msgid "%s Rankings" msgstr "%s Bảng điểm" -#: judge/views/contests.py:739 +#: judge/views/contests.py:1013 msgid "???" msgstr "???" -#: judge/views/contests.py:755 +#: judge/views/contests.py:1029 #, python-format msgid "Your participation in %s" msgstr "Lần tham gia trong %s" -#: judge/views/contests.py:756 +#: judge/views/contests.py:1030 #, python-format msgid "%s's participation in %s" msgstr "Lần tham gia của %s trong %s" -#: judge/views/contests.py:763 +#: judge/views/contests.py:1044 msgid "Live" msgstr "Trực tiếp" -#: judge/views/contests.py:775 templates/contest/contest-tabs.html:13 +#: judge/views/contests.py:1063 templates/contest/contest-tabs.html:13 msgid "Participation" msgstr "Lần tham gia" -#: judge/views/contests.py:822 +#: judge/views/contests.py:1112 #, python-format msgid "%s MOSS Results" msgstr "%s Kết quả MOSS" -#: judge/views/contests.py:849 +#: judge/views/contests.py:1148 #, python-format msgid "Running MOSS for %s..." msgstr "Đang chạy MOSS cho %s..." -#: judge/views/contests.py:872 +#: judge/views/contests.py:1171 #, python-format msgid "Contest tag: %s" msgstr "Nhãn kỳ thi: %s" -#: judge/views/contests.py:882 judge/views/ticket.py:57 +#: judge/views/contests.py:1186 judge/views/ticket.py:72 msgid "Issue description" msgstr "Mô tả vấn đề" -#: judge/views/contests.py:925 +#: judge/views/contests.py:1232 #, python-format msgid "New clarification for %s" msgstr "Thông báo mới cho %s" -#: judge/views/error.py:14 +#: judge/views/error.py:17 msgid "404 error" msgstr "Lỗi 404" -#: judge/views/error.py:15 +#: judge/views/error.py:18 #, python-format msgid "Could not find page \"%s\"" msgstr "Không thể tìm thấy trang \"%s\"" -#: judge/views/error.py:22 +#: judge/views/error.py:29 #, python-format msgid "no permission for %s" msgstr "không có quyền cho %s" -#: judge/views/error.py:30 +#: judge/views/error.py:41 #, python-format msgid "corrupt page %s" msgstr "trang bị sập %s" -#: judge/views/internal.py:11 +#: judge/views/internal.py:12 #, fuzzy #| msgid "contest problems" msgid "Internal problems" @@ -2642,72 +2642,72 @@ msgstr "bài trong kỳ thi" msgid "Runtimes" msgstr "Runtimes" -#: judge/views/notification.py:40 +#: judge/views/notification.py:43 #, python-format msgid "Notifications (%d unseen)" msgstr "Thông báo (%d chưa xem)" -#: judge/views/organization.py:59 judge/views/organization.py:62 +#: judge/views/organization.py:93 judge/views/organization.py:99 msgid "No such organization" msgstr "Không có tổ chức như vậy" -#: judge/views/organization.py:60 +#: judge/views/organization.py:94 #, python-format msgid "Could not find an organization with the key \"%s\"." msgstr "Không tìm thấy tổ chức với mã \"%s\"." -#: judge/views/organization.py:63 +#: judge/views/organization.py:100 msgid "Could not find such organization." msgstr "" -#: judge/views/organization.py:79 judge/views/register.py:34 +#: judge/views/organization.py:119 judge/views/register.py:49 #: templates/organization/list.html:32 templates/user/import/table_csv.html:9 #: templates/user/user-list-tabs.html:6 msgid "Organizations" msgstr "Tổ chức" -#: judge/views/organization.py:130 +#: judge/views/organization.py:187 #, python-format msgid "%s Members" msgstr "%s Thành viên" -#: judge/views/organization.py:159 judge/views/organization.py:162 -#: judge/views/organization.py:167 +#: judge/views/organization.py:232 judge/views/organization.py:238 +#: judge/views/organization.py:245 msgid "Joining organization" msgstr "Tham gia tổ chức" -#: judge/views/organization.py:159 +#: judge/views/organization.py:233 msgid "You are already in the organization." msgstr "Bạn đã ở trong tổ chức." -#: judge/views/organization.py:162 +#: judge/views/organization.py:238 msgid "This organization is not open." msgstr "Tổ chức này không phải tổ chức mở." -#: judge/views/organization.py:179 +#: judge/views/organization.py:261 msgid "Leaving organization" msgstr "Rời tổ chức" -#: judge/views/organization.py:179 +#: judge/views/organization.py:262 #, python-format msgid "You are not in \"%s\"." msgstr "Bạn không ở trong \"%s\"." -#: judge/views/organization.py:203 +#: judge/views/organization.py:287 #, python-format msgid "Request to join %s" msgstr "Đăng ký tham gia %s" -#: judge/views/organization.py:221 +#: judge/views/organization.py:312 msgid "Join request detail" msgstr "Chi tiết đơn đăng ký" -#: judge/views/organization.py:254 +#: judge/views/organization.py:360 #, python-format msgid "Managing join requests for %s" msgstr "Quản lý đơn đăng ký cho %s" -#: judge/views/organization.py:285 +#: judge/views/organization.py:400 #, python-format msgid "" "Your organization can only receive %d more members. You cannot approve %d " @@ -2716,207 +2716,207 @@ msgstr "" "Tổ chức chỉ có thể chứa %d thành viên. Bạn không thể chấp thuận nhiều hơn %d " "người." -#: judge/views/organization.py:297 +#: judge/views/organization.py:418 #, python-format msgid "Approved %d user." msgid_plural "Approved %d users." msgstr[0] "Đã chấp thuận %d người." -#: judge/views/organization.py:298 +#: judge/views/organization.py:421 #, python-format msgid "Rejected %d user." msgid_plural "Rejected %d users." msgstr[0] "Đã từ chối %d người." -#: judge/views/organization.py:328 +#: judge/views/organization.py:455 #, python-format msgid "Editing %s" msgstr "Đang chỉnh sửa %s" -#: judge/views/organization.py:352 judge/views/organization.py:360 +#: judge/views/organization.py:482 judge/views/organization.py:496 msgid "Can't edit organization" msgstr "Không thể chỉnh sửa tổ chức" -#: judge/views/organization.py:353 +#: judge/views/organization.py:483 msgid "You are not allowed to edit this organization." msgstr "Bạn không được phép chỉnh sửa tổ chức này." -#: judge/views/organization.py:361 +#: judge/views/organization.py:497 msgid "You are not allowed to kick people from this organization." msgstr "Bạn không được phép đuổi người." -#: judge/views/organization.py:366 judge/views/organization.py:370 +#: judge/views/organization.py:506 judge/views/organization.py:514 msgid "Can't kick user" msgstr "Không thể đuổi" -#: judge/views/organization.py:367 +#: judge/views/organization.py:507 msgid "The user you are trying to kick does not exist!" msgstr "" -#: judge/views/organization.py:371 +#: judge/views/organization.py:515 #, python-format msgid "The user you are trying to kick is not in organization: %s." msgstr "" -#: judge/views/problem.py:70 +#: judge/views/problem.py:108 msgid "No such problem" msgstr "Không có bài nào như vậy" -#: judge/views/problem.py:71 +#: judge/views/problem.py:109 #, python-format msgid "Could not find a problem with the code \"%s\"." msgstr "Không tìm thấy bài tập với mã bài \"%s\"." -#: judge/views/problem.py:115 +#: judge/views/problem.py:169 #, python-brace-format msgid "Editorial for {0}" msgstr "Hướng dẫn cho {0}" -#: judge/views/problem.py:118 +#: judge/views/problem.py:173 #, python-brace-format msgid "Editorial for {0}" msgstr "Hướng dẫn cho {0}" -#: judge/views/problem.py:343 templates/contest/contest.html:79 +#: judge/views/problem.py:463 templates/contest/contest.html:79 #: templates/user/user-about.html:28 templates/user/user-tabs.html:5 #: templates/user/users-table.html:29 msgid "Problems" msgstr "Bài tập" -#: judge/views/problem.py:602 +#: judge/views/problem.py:815 msgid "Problem feed" msgstr "Bài tập" -#: judge/views/problem.py:756 +#: judge/views/problem.py:1027 msgid "Banned from submitting" msgstr "Bị cấm nộp bài" -#: judge/views/problem.py:757 +#: judge/views/problem.py:1029 msgid "" "You have been declared persona non grata for this problem. You are " "permanently barred from submitting this problem." msgstr "Bạn đã bị cấm nộp bài này." -#: judge/views/problem.py:771 +#: judge/views/problem.py:1052 msgid "Too many submissions" msgstr "Quá nhiều lần nộp" -#: judge/views/problem.py:772 +#: judge/views/problem.py:1054 msgid "You have exceeded the submission limit for this problem." msgstr "Bạn đã vượt quá số lần nộp cho bài này." -#: judge/views/problem.py:832 judge/views/problem.py:835 +#: judge/views/problem.py:1133 judge/views/problem.py:1138 #, python-format msgid "Submit to %(problem)s" msgstr "Nộp bài cho %(problem)s" -#: judge/views/problem.py:850 +#: judge/views/problem.py:1160 msgid "Clone Problem" msgstr "Nhân bản bài tập" -#: judge/views/problem_data.py:48 +#: judge/views/problem_data.py:68 msgid "Checker arguments must be a JSON object" msgstr "" -#: judge/views/problem_data.py:50 +#: judge/views/problem_data.py:70 msgid "Checker arguments is invalid JSON" msgstr "" -#: judge/views/problem_data.py:57 +#: judge/views/problem_data.py:77 msgid "Your zip file is invalid!" msgstr "File Zip không hợp lệ!" -#: judge/views/problem_data.py:120 +#: judge/views/problem_data.py:157 #, python-brace-format msgid "Comparing submissions for {0}" msgstr "So sánh các bài nộp cho {0}" -#: judge/views/problem_data.py:123 +#: judge/views/problem_data.py:161 #, python-brace-format msgid "Comparing submissions for {0}" msgstr "So sánh các bài nộp cho {0}" -#: judge/views/problem_data.py:158 +#: judge/views/problem_data.py:198 #, python-brace-format msgid "Editing data for {0}" msgstr "Chỉnh sửa dữ liệu cho {0}" -#: judge/views/problem_data.py:161 +#: judge/views/problem_data.py:202 #, python-format msgid "Editing data for %s" msgstr "Chỉnh sửa dữ liệu cho %s" -#: judge/views/problem_data.py:253 judge/views/problem_data.py:254 +#: judge/views/problem_data.py:332 judge/views/problem_data.py:334 #, python-format msgid "Generated init.yml for %s" msgstr "File init.yml cho %s" -#: judge/views/problem_manage.py:52 judge/views/problem_manage.py:55 +#: judge/views/problem_manage.py:57 judge/views/problem_manage.py:61 #, python-format msgid "Managing submissions for %s" msgstr "Quản lý bài nộp cho %s" -#: judge/views/problem_manage.py:97 +#: judge/views/problem_manage.py:120 #, python-format msgid "Rejudging selected submissions for %s..." msgstr "Đang chấm lại các bài nộp cho %s..." -#: judge/views/problem_manage.py:141 +#: judge/views/problem_manage.py:181 #, python-format msgid "Rescoring all submissions for %s..." msgstr "Đang tính điểm lại các bài nộp cho %s..." -#: judge/views/problem_manage.py:150 +#: judge/views/problem_manage.py:196 #, python-format msgid "Successfully scheduled %d submission for rejudging." msgid_plural "Successfully scheduled %d submissions for rejudging." msgstr[0] "Đã lên lịch chấm lại cho %d bài nộp." -#: judge/views/ranked_submission.py:58 +#: judge/views/ranked_submission.py:67 #, python-format msgid "Best solutions for %s" msgstr "Các bài nộp tốt nhất cho %s" -#: judge/views/ranked_submission.py:61 +#: judge/views/ranked_submission.py:71 #, python-brace-format msgid "Best solutions for {0}" msgstr "Các bài nộp tốt nhất cho {0}" -#: judge/views/ranked_submission.py:71 +#: judge/views/ranked_submission.py:83 #, python-format msgid "Best solutions for %(problem)s in %(contest)s" msgstr "Các bài nộp tốt nhất cho %(problem)s trong %(contest)s" -#: judge/views/ranked_submission.py:74 +#: judge/views/ranked_submission.py:87 #, python-format msgid "Best solutions for problem %(number)s in %(contest)s" msgstr "Các bài nộp tốt nhất cho bài %(number)s trong %(contest)s" -#: judge/views/ranked_submission.py:80 +#: judge/views/ranked_submission.py:95 #, python-brace-format msgid "Best solutions for {0} in {2}" msgstr "" "Các bài nộp tốt nhất cho {0} trong {2}" -#: judge/views/ranked_submission.py:83 +#: judge/views/ranked_submission.py:102 #, python-brace-format msgid "Best solutions for problem {0} in {1}" msgstr "Các bài nộp tốt nhất cho bài {0} trong {1}" -#: judge/views/register.py:27 +#: judge/views/register.py:33 msgid "A username must contain letters, numbers, or underscores" msgstr "Tên đăng nhập phải chứa ký tự, chữ số, hoặc dấu gạch dưới" -#: judge/views/register.py:31 templates/user/edit-profile.html:120 +#: judge/views/register.py:43 templates/user/edit-profile.html:120 msgid "Preferred language" msgstr "Ngôn ngữ ưa thích" -#: judge/views/register.py:38 +#: judge/views/register.py:56 msgid "Subscribe to newsletter?" msgstr "Đăng ký để nhận thông báo?" -#: judge/views/register.py:55 +#: judge/views/register.py:79 #, python-format msgid "" "The email address \"%s\" is already taken. Only one registration is allowed " @@ -2924,7 +2924,7 @@ msgid "" msgstr "" "Email \"%s\" đã được sử dụng. Mỗi email chỉ có thể đăng ký một tài khoản." -#: judge/views/register.py:61 +#: judge/views/register.py:91 msgid "" "Your email provider is not allowed due to history of abuse. Please use a " "reputable email provider." @@ -2932,69 +2932,69 @@ msgstr "" "Your email provider is not allowed due to history of abuse. Please use a " "reputable email provider." -#: judge/views/register.py:67 judge/views/register.py:105 +#: judge/views/register.py:99 judge/views/register.py:140 msgid "Registration" msgstr "Đăng ký" -#: judge/views/register.py:116 +#: judge/views/register.py:154 msgid "Authentication failure" msgstr "Xác thực thất bại" -#: judge/views/stats.py:67 +#: judge/views/stats.py:99 msgid "Language statistics" msgstr "Thống kê ngôn ngữ" -#: judge/views/status.py:24 templates/submission/list.html:313 +#: judge/views/status.py:27 templates/submission/list.html:313 msgid "Status" msgstr "Kết quả chấm" -#: judge/views/status.py:107 +#: judge/views/status.py:120 msgid "Version matrix" msgstr "Ma trận phiên bản" -#: judge/views/submission.py:88 judge/views/submission.py:95 +#: judge/views/submission.py:112 judge/views/submission.py:120 #, python-format msgid "Submission of %(problem)s by %(user)s" msgstr "Bài nộp của %(user)s cho bài %(problem)s" -#: judge/views/submission.py:248 judge/views/submission.py:249 +#: judge/views/submission.py:285 judge/views/submission.py:286 #: templates/problem/problem.html:194 msgid "All submissions" msgstr "Tất cả bài nộp" -#: judge/views/submission.py:410 +#: judge/views/submission.py:475 msgid "All my submissions" msgstr "Tất cả bài nộp của tôi" -#: judge/views/submission.py:411 +#: judge/views/submission.py:476 #, python-format msgid "All submissions by %s" msgstr "Tất cả bài nộp của %s" -#: judge/views/submission.py:442 +#: judge/views/submission.py:521 #, python-format msgid "All submissions for %s" msgstr "Tất cả bài nộp cho %s" -#: judge/views/submission.py:461 +#: judge/views/submission.py:543 msgid "Must pass a problem" msgstr "Phải làm được một bài" -#: judge/views/submission.py:507 +#: judge/views/submission.py:601 #, python-format msgid "My submissions for %(problem)s" msgstr "Bài nộp của tôi cho %(problem)s" -#: judge/views/submission.py:508 +#: judge/views/submission.py:602 #, python-format msgid "%(user)s's submissions for %(problem)s" msgstr "Các bài nộp của %(user)s cho %(problem)s" -#: judge/views/submission.py:609 +#: judge/views/submission.py:732 msgid "Must pass a contest" msgstr "Phải qua một kỳ thi" -#: judge/views/submission.py:628 +#: judge/views/submission.py:760 #, python-brace-format msgid "" "{0}'s submissions for {2} in {0} cho {2} trong {4}" -#: judge/views/submission.py:635 +#: judge/views/submission.py:772 #, python-brace-format msgid "" "{0}'s submissions for problem {2} in {3}" @@ -3012,44 +3012,44 @@ msgstr "" "Các bài nộp của {0} cho bài {2} trong {3}" "" -#: judge/views/ticket.py:50 judge/views/ticket.py:56 +#: judge/views/ticket.py:65 judge/views/ticket.py:71 msgid "Ticket title" msgstr "Tiêu đề báo cáo" -#: judge/views/ticket.py:107 judge/views/ticket.py:110 +#: judge/views/ticket.py:129 judge/views/ticket.py:133 #, python-format msgid "New ticket for %s" msgstr "Báo cáo mới cho %s" -#: judge/views/ticket.py:170 +#: judge/views/ticket.py:210 #, python-format msgid "%(title)s - Ticket %(id)d" msgstr "%(title)s - Báo cáo %(id)d" -#: judge/views/ticket.py:279 +#: judge/views/ticket.py:339 #, python-format msgid "Tickets - Page %(number)d of %(total)d" msgstr "Báo cáo - Trang %(number)d trên %(total)d" -#: judge/views/ticket.py:328 +#: judge/views/ticket.py:405 #, python-format msgid "New Ticket: %s" msgstr "Báo cáo mới: %s" -#: judge/views/ticket.py:329 +#: judge/views/ticket.py:408 #, python-format msgid "#%(id)d, assigned to: %(users)s" msgstr "#%(id)d, được phân cho: %(users)s" -#: judge/views/ticket.py:331 +#: judge/views/ticket.py:412 msgid ", " msgstr ", " -#: judge/views/ticket.py:331 +#: judge/views/ticket.py:417 msgid "no one" msgstr "không người nào" -#: judge/views/ticket.py:351 +#: judge/views/ticket.py:444 #, python-format msgid "New Ticket Message For: %s" msgstr "Tin nhắn báo cáo mới cho: %s" @@ -3058,64 +3058,64 @@ msgstr "Tin nhắn báo cáo mới cho: %s" msgid "Enable Two Factor Authentication" msgstr "Kích hoạt Two Factor Authentication" -#: judge/views/totp.py:89 templates/registration/totp_disable.html:48 +#: judge/views/totp.py:93 templates/registration/totp_disable.html:48 msgid "Disable Two Factor Authentication" msgstr "Hủy kích hoạt Two Factor Authentication" -#: judge/views/totp.py:105 +#: judge/views/totp.py:109 msgid "Perform Two Factor Authentication" msgstr "Thực hiện Two Factor Authentication" -#: judge/views/user.py:74 +#: judge/views/user.py:89 msgid "No such user" msgstr "Không người dùng nào như vậy" -#: judge/views/user.py:74 +#: judge/views/user.py:90 #, python-format msgid "No user handle \"%s\"." msgstr "Không tồn tại tên người dùng \"%s\"." -#: judge/views/user.py:78 +#: judge/views/user.py:95 msgid "My account" msgstr "Tài khoản của tôi" -#: judge/views/user.py:79 +#: judge/views/user.py:97 #, python-format msgid "User %s" msgstr "Thành viên %s" -#: judge/views/user.py:149 +#: judge/views/user.py:195 msgid "M j, Y" msgstr "j M, Y" -#: judge/views/user.py:172 +#: judge/views/user.py:230 msgid "M j, Y, G:i" msgstr "j M, Y, G:i" -#: judge/views/user.py:291 +#: judge/views/user.py:392 msgid "Updated on site" msgstr "Được cập nhật trên web" -#: judge/views/user.py:326 templates/admin/auth/user/change_form.html:14 +#: judge/views/user.py:446 templates/admin/auth/user/change_form.html:14 #: templates/admin/auth/user/change_form.html:17 templates/base.html:264 #: templates/user/user-tabs.html:10 msgid "Edit profile" msgstr "Chỉnh sửa thông tin" -#: judge/views/user.py:335 templates/user/user-list-tabs.html:4 +#: judge/views/user.py:457 templates/user/user-list-tabs.html:4 msgid "Leaderboard" msgstr "Bảng xếp hạng" -#: judge/views/user.py:410 +#: judge/views/user.py:553 msgid "Import Users" msgstr "" -#: judge/views/widgets.py:48 judge/views/widgets.py:58 +#: judge/views/widgets.py:69 judge/views/widgets.py:85 #, python-format msgid "Invalid upstream data: %s" msgstr "Dữ liệu không hợp lệ: %s" -#: judge/views/widgets.py:68 +#: judge/views/widgets.py:99 msgid "Bad latitude or longitude" msgstr "Kinh độ / Vĩ độ không hợp lệ" @@ -3307,7 +3307,7 @@ msgstr "Chỉnh sửa thông tin" msgid "Rejudge" msgstr "Chấm lại" -#: templates/base.html:230 templates/chat/chat.html:573 +#: templates/base.html:230 templates/chat/chat.html:578 msgid "Chat" msgstr "Chat" @@ -3323,7 +3323,7 @@ msgstr "Xin chào, %(username)s." #: templates/base.html:259 templates/chat/chat.html:20 #: templates/comments/list.html:89 templates/contest/contest-list-tabs.html:24 #: templates/contest/ranking-table.html:53 templates/internal/base.html:59 -#: templates/problem/list.html:263 templates/problem/problem-list-tabs.html:6 +#: templates/problem/list.html:249 templates/problem/problem-list-tabs.html:6 #: templates/submission/info-base.html:12 #: templates/submission/submission-list-tabs.html:15 msgid "Admin" @@ -3412,7 +3412,7 @@ msgstr "Sự kiện" msgid "You have no ticket" msgstr "Bạn không có báo cáo" -#: templates/blog/list.html:94 templates/problem/list.html:409 +#: templates/blog/list.html:94 templates/problem/list.html:392 #: templates/problem/problem.html:407 msgid "Clarifications" msgstr "Thông báo" @@ -3421,7 +3421,7 @@ msgstr "Thông báo" msgid "Add" msgstr "Thêm mới" -#: templates/blog/list.html:119 templates/problem/list.html:431 +#: templates/blog/list.html:119 templates/problem/list.html:414 #: templates/problem/problem.html:418 msgid "No clarifications have been made at this time." msgstr "Không có thông báo nào." @@ -3458,28 +3458,28 @@ msgstr "Bạn bè" msgid "Other" msgstr "Thành viên khác" -#: templates/chat/chat.html:166 +#: templates/chat/chat.html:168 msgid "New message(s)" msgstr "Tin nhắn mới" -#: templates/chat/chat.html:514 templates/chat/chat.html:595 +#: templates/chat/chat.html:519 templates/chat/chat.html:600 #: templates/user/base-users.html:14 templates/user/base-users.html:80 msgid "Search by handle..." msgstr "Tìm kiếm theo tên..." -#: templates/chat/chat.html:575 templates/chat/chat.html:582 +#: templates/chat/chat.html:580 templates/chat/chat.html:587 msgid "Online Users" msgstr "Trực tuyến" -#: templates/chat/chat.html:583 +#: templates/chat/chat.html:588 msgid "Refresh" msgstr "Làm mới" -#: templates/chat/chat.html:616 +#: templates/chat/chat.html:621 msgid "Emoji" msgstr "" -#: templates/chat/chat.html:617 +#: templates/chat/chat.html:622 msgid "Enter your message" msgstr "Nhập tin nhắn" @@ -3659,7 +3659,7 @@ msgstr "Hôm nay" msgid "Next" msgstr "Tiếp" -#: templates/contest/contest-list-tabs.html:21 templates/problem/list.html:262 +#: templates/contest/contest-list-tabs.html:21 templates/problem/list.html:248 #: templates/problem/problem-list-tabs.html:5 msgid "List" msgstr "Danh sách" @@ -3766,8 +3766,8 @@ msgid "" "%(time_limit)s window between %(start_time)s and " "%(end_time)s" msgstr "" -"Kéo dài %(time_limit)s từ %(start_time)s đến " -"%(end_time)s" +"Kéo dài %(time_limit)s từ %(start_time)s đến %(end_time)s" #: templates/contest/contest.html:63 #, python-format @@ -3780,12 +3780,12 @@ msgstr "Tỷ lệ AC" #: templates/contest/contest.html:86 templates/contest/list.html:236 #: templates/contest/list.html:290 templates/contest/list.html:370 -#: templates/problem/list.html:285 templates/problem/list.html:316 +#: templates/problem/list.html:270 templates/problem/list.html:299 msgid "Users" msgstr "Số lượng" -#: templates/contest/contest.html:111 templates/problem/list.html:320 -#: templates/problem/list.html:392 +#: templates/contest/contest.html:111 templates/problem/list.html:303 +#: templates/problem/list.html:375 msgid "Editorial" msgstr "Hướng dẫn" @@ -3805,7 +3805,7 @@ msgstr "" msgid "Show" msgstr "Hiển thị" -#: templates/contest/list.html:102 templates/problem/list.html:111 +#: templates/contest/list.html:102 templates/problem/list.html:95 msgid "Organizations..." msgstr "Tổ chức..." @@ -4049,8 +4049,8 @@ msgstr "" msgid "Thinking" msgstr "Bảng xếp hạng" -#: templates/internal/base.html:78 templates/problem/list.html:282 -#: templates/problem/list.html:306 +#: templates/internal/base.html:78 templates/problem/list.html:267 +#: templates/problem/list.html:289 msgid "Types" msgstr "Dạng" @@ -4274,7 +4274,7 @@ msgid "There are no requests to approve." msgstr "Không có đơn đăng ký." #: templates/organization/requests/pending.html:17 -#: templates/problem/data.html:477 +#: templates/problem/data.html:479 msgid "Delete?" msgstr "Xóa?" @@ -4310,35 +4310,35 @@ msgstr "Đuổi" msgid "Enter a new code for the cloned problem:" msgstr "Nhập mã bài mới cho bài tập được nhân bản:" -#: templates/problem/data.html:147 +#: templates/problem/data.html:149 msgid "Instruction" msgstr "Hướng dẫn" -#: templates/problem/data.html:435 +#: templates/problem/data.html:437 msgid "View YAML" msgstr "Xem YAML" -#: templates/problem/data.html:466 templates/problem/data.html:517 +#: templates/problem/data.html:468 templates/problem/data.html:519 msgid "Apply!" msgstr "Lưu!" -#: templates/problem/data.html:471 +#: templates/problem/data.html:473 msgid "Type" msgstr "Kiểu" -#: templates/problem/data.html:472 +#: templates/problem/data.html:474 msgid "Input file" msgstr "File Input" -#: templates/problem/data.html:473 +#: templates/problem/data.html:475 msgid "Output file" msgstr "File Output" -#: templates/problem/data.html:475 +#: templates/problem/data.html:477 msgid "Pretest?" msgstr "Pretest?" -#: templates/problem/data.html:518 +#: templates/problem/data.html:520 msgid "Add new case" msgstr "Thêm test mới" @@ -4375,7 +4375,7 @@ msgstr "Tổng điểm" msgid "Thinking point" msgstr "Độ khó nghĩ" -#: templates/problem/feed.html:70 templates/problem/search-form.html:63 +#: templates/problem/feed.html:70 templates/problem/search-form.html:73 msgid "Problem types" msgstr "Dạng bài" @@ -4383,49 +4383,49 @@ msgstr "Dạng bài" msgid "Any additional note here" msgstr "Lưu ý thêm cho admin" -#: templates/problem/list.html:109 +#: templates/problem/list.html:93 msgid "Filter by type..." msgstr "Lọc theo dạng..." -#: templates/problem/list.html:166 templates/problem/list.html:192 +#: templates/problem/list.html:152 templates/problem/list.html:178 msgid "Add types..." msgstr "Thêm dạng" -#: templates/problem/list.html:208 +#: templates/problem/list.html:194 msgid "Fail to vote!" msgstr "Hệ thống lỗi!" -#: templates/problem/list.html:211 +#: templates/problem/list.html:197 msgid "Successful vote! Thank you!" msgstr "Đã gửi thành công! Cảm ơn bạn!" -#: templates/problem/list.html:261 +#: templates/problem/list.html:247 msgid "Feed" msgstr "Gợi ý" -#: templates/problem/list.html:280 templates/problem/list.html:302 -#: templates/problem/search-form.html:46 templates/user/user-problems.html:57 +#: templates/problem/list.html:285 templates/problem/search-form.html:56 +#: templates/user/user-problems.html:57 msgid "Category" msgstr "Nhóm" -#: templates/problem/list.html:313 +#: templates/problem/list.html:296 #, python-format msgid "AC %%" msgstr "AC %%" -#: templates/problem/list.html:404 +#: templates/problem/list.html:387 msgid "Add clarifications" msgstr "Thêm thông báo" -#: templates/problem/list.html:440 +#: templates/problem/list.html:423 msgid "FOR YOU" msgstr "DÀNH CHO BẠN" -#: templates/problem/list.html:443 +#: templates/problem/list.html:426 msgid "NEW" msgstr "MỚI NHẤT" -#: templates/problem/list.html:447 +#: templates/problem/list.html:430 msgid "VOLUNTEER" msgstr "TÌNH NGUYỆN" @@ -4637,6 +4637,10 @@ msgstr "Giới hạn thời gian:" msgid "Memory Limit:" msgstr "Giới hạn bộ nhớ:" +#: templates/problem/recent-attempt.html:2 +msgid "Last unsolved" +msgstr "Nộp gần đây" + #: templates/problem/search-form.html:2 msgid "Problem search" msgstr "Tìm kiếm bài tập" @@ -4661,21 +4665,27 @@ msgstr "Hiển thị hướng dẫn" msgid "Have editorial" msgstr "Có hướng dẫn" -#: templates/problem/search-form.html:49 templates/problem/search-form.html:51 +#: templates/problem/search-form.html:46 +#, fuzzy +#| msgid "Authors" +msgid "Author" +msgstr "Các tác giả" + +#: templates/problem/search-form.html:59 templates/problem/search-form.html:61 #: templates/submission/submission-list-tabs.html:4 msgid "All" msgstr "Tất cả" -#: templates/problem/search-form.html:74 +#: templates/problem/search-form.html:84 msgid "Point range" msgstr "Mốc điểm" -#: templates/problem/search-form.html:80 templates/submission/list.html:331 +#: templates/problem/search-form.html:90 templates/submission/list.html:331 #: templates/ticket/list.html:248 msgid "Go" msgstr "Lọc" -#: templates/problem/search-form.html:81 +#: templates/problem/search-form.html:91 msgid "Random" msgstr "Ngẫu nhiên" diff --git a/templates/problem/list.html b/templates/problem/list.html index 316d466..ba5ba04 100644 --- a/templates/problem/list.html +++ b/templates/problem/list.html @@ -10,17 +10,15 @@ {% if not request.in_contest_mode %} {% endblock %} -{% block js_media %} +{% block three_col_js %} {% endblock %} -{% block title_ruler %}{% endblock %} - -{% block title_row %} - {% set tab = 'list' %} - {% set title = 'Contests' %} - {% include "contest/contest-list-tabs.html" %} +{% block left_sidebar %} + {% endblock %} {% macro contest_head(contest) %} @@ -212,19 +219,20 @@ {% endif %} {% endmacro %} -{% block body %} +{% block middle_content %}
- - + {% if organizations %} + + {% endif %}
{% if active_participations %} diff --git a/templates/contests-countdown.html b/templates/contests-countdown.html new file mode 100644 index 0000000..9cd2310 --- /dev/null +++ b/templates/contests-countdown.html @@ -0,0 +1,35 @@ +{% if current_contests %} + +{% endif %} + +{% if future_contests %} + +{% endif %} diff --git a/templates/organization/contests.html b/templates/organization/contests.html new file mode 100644 index 0000000..14aed86 --- /dev/null +++ b/templates/organization/contests.html @@ -0,0 +1,4 @@ +{% extends "contest/list.html" %} +{% block left_sidebar %} + {% include "organization/org-left-sidebar.html" %} +{% endblock %} \ No newline at end of file diff --git a/templates/organization/home.html b/templates/organization/home.html index 6007abe..2be494a 100644 --- a/templates/organization/home.html +++ b/templates/organization/home.html @@ -1,12 +1,6 @@ -{% extends "base.html" %} - -{% block media %} +{% extends "three-column-content.html" %} +{% block three_col_media %} {% endblock %} -{% block js_media %} +{% block three_col_js %} {% endblock %} -{% block title_row %} +{% block left_sidebar %} + {% include "organization/org-left-sidebar.html" %} +{% endblock %} + +{% block title_ruler %}{% endblock %} +{% block middle_content %} + {% block before_posts %}{% endblock %}

{{title}}

@@ -85,81 +72,68 @@ class="unselectable button">{{ _('Request membership') }} {% endif %} {% endif %} -
-{% endblock %} -{% block title_ruler %} {% endblock %} -{% block body %} - {% block before_posts %}{% endblock %} - -
- - {% if page_type == 'list' %} + {% if has_show_editorial_option %}
- {% elif page_type == 'feed' %} + {% endif %} + {% if has_have_editorial_option %}
{% endif %} -
- - -
+ {% if organizations %} +
+ + +
+ {% endif %}
-
{% if show_types %}
diff --git a/templates/top-users.html b/templates/top-users.html new file mode 100644 index 0000000..fac55ec --- /dev/null +++ b/templates/top-users.html @@ -0,0 +1,37 @@ +{% if top_rated %} + +{% endif %} + +{% if top_scorer %} + +{% endif %} \ No newline at end of file diff --git a/templates/user/base-users-three-col.html b/templates/user/base-users-three-col.html new file mode 100644 index 0000000..c7fbb46 --- /dev/null +++ b/templates/user/base-users-three-col.html @@ -0,0 +1,102 @@ +{% extends "three-column-content.html" %} + +{% block three_col_js %} + {% block users_js_media %}{% endblock %} + +{% endblock %} + +{% block three_col_media %} + + + {% block users_media %}{% endblock %} +{% endblock %} + +{% block middle_content %} +
+
+ {% if page_obj and page_obj.num_pages > 1 %} +
+ {% include "list-pages.html" %} + + + +
+ {% endif %} + + + {% block users_table %}{% endblock %} +
+ + {% if page_obj and page_obj.num_pages > 1 %} +
{% include "list-pages.html" %}
+ {% endif %} +
+
+{% endblock %} From cae65de1d583da215f499a526e2d6a7064e26ad2 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 27 May 2022 23:33:00 -0500 Subject: [PATCH 066/959] Add permission to org pages --- judge/views/organization.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/judge/views/organization.py b/judge/views/organization.py index dc96bdc..289118b 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -83,6 +83,13 @@ class OrganizationBase(object): self.request.profile in org if self.request.user.is_authenticated else False ) + def can_access(self, org): + if self.request.user.is_superuser: + return True + if org is None: + org = self.object + return self.is_member(org) or self.can_edit_organization(org) + class OrganizationMixin(OrganizationBase): context_object_name = "organization" @@ -287,6 +294,8 @@ class OrganizationProblems(ProblemList, OrganizationExternalMixin): ret = super().get_organization_from_url(request, *args, **kwargs) if ret: return ret + if not self.can_access(self.organization): + return HttpResponseBadRequest() self.setup_problem_list(request) return super().get(request, *args, **kwargs) @@ -308,6 +317,8 @@ class OrganizationContests(ContestList, OrganizationExternalMixin): ret = super().get_organization_from_url(request, *args, **kwargs) if ret: return ret + if not self.can_access(self.organization): + return HttpResponseBadRequest() return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): From b1a52cc8722ac75ab5c8cbf2508a6bd5f8c7170b Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 27 May 2022 23:41:14 -0500 Subject: [PATCH 067/959] Make wmd bar scrollbar hidden --- resources/pagedown_widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/pagedown_widget.css b/resources/pagedown_widget.css index 7d00ff7..3d2326a 100644 --- a/resources/pagedown_widget.css +++ b/resources/pagedown_widget.css @@ -27,7 +27,7 @@ margin: 10px 5px 5px; padding: 0; height: 20px; - overflow-x: auto; + overflow-x: none; } .wmd-spacer { From b8876ba0231d1e36258e214b9a99c0b799adf795 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 28 May 2022 02:29:25 -0500 Subject: [PATCH 068/959] Make admin members --- judge/models/profile.py | 4 ++++ judge/views/organization.py | 7 ++----- templates/top-users.html | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/judge/models/profile.py b/judge/models/profile.py index fe8ce17..251101d 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -112,6 +112,10 @@ class Organization(models.Model): def get_contests_url(self): return reverse("organization_contests", args=(self.id, self.slug)) + def save(self, *args, **kwargs): + self.members.add(*self.admins.all()) + super(Organization, self).save(*args, **kwargs) + class Meta: ordering = ["name"] permissions = ( diff --git a/judge/views/organization.py b/judge/views/organization.py index 289118b..42426f2 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -151,11 +151,8 @@ class OrganizationList(TitleMixin, ListView, OrganizationBase): def get_context_data(self, **kwargs): context = super(OrganizationList, self).get_context_data(**kwargs) - context["my_organizations"] = set() - - for organization in context["organizations"]: - if self.can_edit_organization(organization) or self.is_member(organization): - context["my_organizations"].add(organization) + if self.request.profile: + context["my_organizations"] = self.request.profile.organizations.all() return context diff --git a/templates/top-users.html b/templates/top-users.html index fac55ec..35197ee 100644 --- a/templates/top-users.html +++ b/templates/top-users.html @@ -7,7 +7,7 @@ {% for user in top_rated %} {{loop.index}} - {{link_user(user)}} + {{link_user(user)}} {{user.rating}} {% endfor %} From 982c975a20f487e1ba8af06e6a946a8b0814f084 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 28 May 2022 02:33:37 -0500 Subject: [PATCH 069/959] Add line break to top users --- templates/top-users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/top-users.html b/templates/top-users.html index 35197ee..23e2f10 100644 --- a/templates/top-users.html +++ b/templates/top-users.html @@ -26,7 +26,7 @@ {% for user in top_scorer %} {{loop.index}} - {{link_user(user)}} + {{link_user(user)}} {{ user.performance_points|floatformat(0) }} {% endfor %} From 1b3b27f1d945f1dd32438829bcc1e1ff7fe5b8b7 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 28 May 2022 02:52:41 -0500 Subject: [PATCH 070/959] Fix org user --- templates/organization/users-table.html | 4 ++-- templates/organization/users.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/organization/users-table.html b/templates/organization/users-table.html index 873e985..0f59bdc 100644 --- a/templates/organization/users-table.html +++ b/templates/organization/users-table.html @@ -1,13 +1,13 @@ {% extends "user/users-table.html" %} {% block before_point_head %} - {% if is_admin %} + {% if can_edit %} {% endif %} {% endblock %} {% block before_point %} - {% if is_admin %} + {% if can_edit %}
{% csrf_token %} diff --git a/templates/organization/users.html b/templates/organization/users.html index ab383a3..f681f15 100644 --- a/templates/organization/users.html +++ b/templates/organization/users.html @@ -17,7 +17,7 @@ } - {% if is_admin %} + {% if can_edit %} +{% endblock %} + +{% block middle_content %} + {% include "organization/form.html" %} +{% endblock %} \ No newline at end of file diff --git a/templates/organization/blog/pending.html b/templates/organization/blog/pending.html new file mode 100644 index 0000000..4df3608 --- /dev/null +++ b/templates/organization/blog/pending.html @@ -0,0 +1,28 @@ +{% extends "organization/home-base.html" %} + +{% block middle_content %} + + + + + + + + + + {% for blog in blogs %} + + + + + + {% endfor %} + +
+ {{_('Blog')}} + + {{_('Author')}} + + {{_('Post time')}} +
{{blog.title}}{{link_users(blog.authors.all())}}{{- blog.publish_on|date(_("N j, Y, g:i a")) -}}
+{% endblock %} \ No newline at end of file diff --git a/templates/organization/edit.html b/templates/organization/edit.html index 996c2b9..9df98b0 100644 --- a/templates/organization/edit.html +++ b/templates/organization/edit.html @@ -1,6 +1,6 @@ -{% extends "base.html" %} +{% extends "organization/home-base.html" %} -{% block js_media %} +{% block org_js %} {{ form.media.js }} {% endblock %} -{% block media %} +{% block three_col_media %} {{ form.media.css }} - - - {% endblock %} -{% block body %} - - {% csrf_token %} - {{ form.as_table() }}
- -
-{% endblock %} \ No newline at end of file +{% block middle_content %} + {% include "organization/form.html" %} +{% endblock %} diff --git a/templates/organization/form.html b/templates/organization/form.html new file mode 100644 index 0000000..332e4ad --- /dev/null +++ b/templates/organization/form.html @@ -0,0 +1,24 @@ +
+ {% csrf_token %} + {% if form.errors %} +
+ x + {{ form.non_field_errors() }} +
+ {% endif %} + {% for field in form %} + {% if not field.is_hidden %} +
+ {{ field.errors }} + +
+ {{ field }} +
+ {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} +
+ {% endif %} + {% endfor %} + +
\ No newline at end of file diff --git a/templates/organization/home-base.html b/templates/organization/home-base.html new file mode 100644 index 0000000..0291acd --- /dev/null +++ b/templates/organization/home-base.html @@ -0,0 +1,21 @@ +{% extends "three-column-content.html" %} + +{% block three_col_js %} + {% include "organization/home-js.html" %} + {% block org_js %}{% endblock %} +{% endblock %} + +{% block left_sidebar %} + {% include "organization/org-left-sidebar.html" %} +{% endblock %} + +{% block right_sidebar %} + {% include "organization/org-right-sidebar.html" %} +{% endblock %} + +{% block middle_title %} + {% if title %} +

{{ title }}

+
+ {% endif %} +{% endblock %} diff --git a/templates/organization/home-js.html b/templates/organization/home-js.html new file mode 100644 index 0000000..9725bdb --- /dev/null +++ b/templates/organization/home-js.html @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/templates/organization/home.html b/templates/organization/home.html index 2be494a..6e1cc30 100644 --- a/templates/organization/home.html +++ b/templates/organization/home.html @@ -1,79 +1,31 @@ -{% extends "three-column-content.html" %} -{% block three_col_media %} - -{% endblock %} - -{% block three_col_js %} - -{% endblock %} - -{% block left_sidebar %} - {% include "organization/org-left-sidebar.html" %} -{% endblock %} +{% extends "organization/home-base.html" %} {% block title_ruler %}{% endblock %} -{% block middle_content %} - {% block before_posts %}{% endblock %} + +{% block middle_title %}
-

{{title}}

- - - {% if request.user.is_authenticated %} - {% if is_member or can_edit %} - - {% elif organization.is_open or can_edit %} -
- {% csrf_token %} - -
- {% else %} - {{ _('Request membership') }} - {% endif %} +

{{title}}

+ + + {% if request.user.is_authenticated %} + {% if is_member or can_edit %} + {% elif organization.is_open or can_edit %} +
+ {% csrf_token %} + +
+ {% else %} + {{ _('Request membership') }} {% endif %} -
+ {% endif %}
+
+{% endblock %} + +{% block middle_content %} + {% block before_posts %}{% endblock %} {% if is_member or can_edit %} {% for post in posts %} {% include "blog/content.html" %} @@ -95,56 +47,3 @@ {% endif %} {% block after_posts %}{% endblock %} {% endblock %} - -{% block right_sidebar %} - -{% endblock %} \ No newline at end of file diff --git a/templates/organization/list.html b/templates/organization/list.html index 364f821..8d6feb7 100644 --- a/templates/organization/list.html +++ b/templates/organization/list.html @@ -22,14 +22,13 @@ {% endif %} }); - {% endblock %} {% block title_ruler %}{% endblock %} {% block title_row %} {% set tab = 'organizations' %} - {% set title = _('Organizations') %} + {% set title = _('Group') %} {% include "user/user-list-tabs.html" %} {% endblock %} @@ -37,7 +36,7 @@ {% if request.user.is_authenticated %}
- +
{% endif %} diff --git a/templates/organization/org-right-sidebar.html b/templates/organization/org-right-sidebar.html new file mode 100644 index 0000000..55cecae --- /dev/null +++ b/templates/organization/org-right-sidebar.html @@ -0,0 +1,76 @@ + \ No newline at end of file diff --git a/templates/organization/requests/detail.html b/templates/organization/requests/detail.html index 562be13..8e18e20 100644 --- a/templates/organization/requests/detail.html +++ b/templates/organization/requests/detail.html @@ -1,14 +1,7 @@ -{% extends "base.html" %} -{% block media %} - -{% endblock %} +{% extends "organization/home-base.html" %} -{% block body %} - +{% block middle_content %} +
@@ -26,10 +19,8 @@ - - - - + +
{{ _('User:') }} {{ link_user(object.user) }}{{ object.time|date(_("N j, Y, g:i a")) }}
{{ _('Reason:') }}
{{ object.reason }}{{ _('Reason:') }}{{ object.reason }}
{% endblock %} \ No newline at end of file diff --git a/templates/organization/requests/log.html b/templates/organization/requests/log.html index 73a7b8c..a70f2ab 100644 --- a/templates/organization/requests/log.html +++ b/templates/organization/requests/log.html @@ -1,6 +1,6 @@ -{% extends "base.html" %} +{% extends "organization/home-base.html" %} -{% block body %} +{% block middle_content %} {% include "organization/requests/tabs.html" %} {% if requests %} @@ -16,7 +16,7 @@ {{ link_user(r.user) }} - {{- r.time|date(_("N j, Y, H:i")) -}} + {{- r.time|date(_("N j, Y, g:i a")) -}} {{ r.state }} diff --git a/templates/organization/requests/pending.html b/templates/organization/requests/pending.html index 3d300a4..c542fad 100644 --- a/templates/organization/requests/pending.html +++ b/templates/organization/requests/pending.html @@ -1,5 +1,5 @@ -{% extends "base.html" %} -{% block body %} +{% extends "organization/home-base.html" %} +{% block middle_content %} {% include "messages.html" %} {% include "organization/requests/tabs.html" %} @@ -21,7 +21,7 @@ {{ form.id }}{{ link_user(form.instance.user) }} - {{ form.instance.time|date(_("N j, Y, H:i")) }} + {{ form.instance.time|date(_("N j, Y, g:i a")) }} {{ form.state }} {{ form.instance.reason|truncatechars(50) }} diff --git a/templates/organization/requests/request.html b/templates/organization/requests/request.html index dff6e8d..b3666e8 100644 --- a/templates/organization/requests/request.html +++ b/templates/organization/requests/request.html @@ -1,6 +1,6 @@ -{% extends "base.html" %} +{% extends "organization/home-base.html" %} -{% block js_media %} +{% block org_js %} {% endblock %} -{% block body %} +{% block middle_content %}
{% csrf_token %}

diff --git a/templates/problem/list-base.html b/templates/problem/list-base.html index 69ea5ec..2eabfe6 100644 --- a/templates/problem/list-base.html +++ b/templates/problem/list-base.html @@ -92,7 +92,7 @@ $category.select2().css({'visibility': 'visible'}).change(clean_submit); $('#types').select2({multiple: 1, placeholder: '{{ _('Filter by type...') }}'}) .css({'visibility': 'visible'}); - $('#search-org').select2({multiple: 1, placeholder: '{{ _('Organizations...') }}'}) + $('#search-org').select2({multiple: 1, placeholder: '{{ _('Groups') }}...'}) .css({'visibility': 'visible'}); $('#search-author').select2({multiple: 1, placeholder: '{{ _('Authors') }}...'}) .css({'visibility': 'visible'}); diff --git a/templates/problem/search-form.html b/templates/problem/search-form.html index 827735d..9f0c910 100644 --- a/templates/problem/search-form.html +++ b/templates/problem/search-form.html @@ -35,7 +35,7 @@ {% endif %} {% if organizations %}
- + + {% else %} Date: Tue, 31 May 2022 00:41:57 -0500 Subject: [PATCH 084/959] Redesign UI --- resources/problem.scss | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/resources/problem.scss b/resources/problem.scss index 060d796..63d4a55 100644 --- a/resources/problem.scss +++ b/resources/problem.scss @@ -297,16 +297,15 @@ ul.problem-list { .problem-feed-option { width: 90%; - margin-left: auto; - margin-right: auto; - padding: 1em; + margin-left: 2.5%; + padding-bottom: 1em; border-radius: 5px; - margin-bottom: 1em + margin-bottom: 1em; } .problem-feed-option-item { padding: 10px 15px; - border-radius: 2em; + border-radius: 1em; font-weight: bold; background: lightgray; margin-right: 1em; From 9b1724cdad11db08cf2b1c53d7928b361e5ef69d Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 1 Jun 2022 00:28:56 -0500 Subject: [PATCH 085/959] Change UI ranking --- dmoj/urls.py | 10 +- judge/contest_format/default.py | 4 +- judge/management/commands/makedmojmessages.py | 3 +- judge/models/problem.py | 23 -- judge/views/organization.py | 1 + judge/views/problem.py | 69 ------ judge/views/submission.py | 36 ++- resources/submission.scss | 6 + templates/contest/media-js.html | 201 ++++++++++++++++ templates/contest/ranking-table.html | 2 +- templates/contest/ranking.html | 217 +----------------- templates/problem/problem.html | 6 - templates/problem/voting-controls.html | 75 ------ templates/problem/voting-form.html | 93 -------- templates/problem/voting-stats.html | 146 ------------ templates/submission/user-ajax.html | 30 +++ 16 files changed, 291 insertions(+), 631 deletions(-) delete mode 100644 templates/problem/voting-controls.html delete mode 100644 templates/problem/voting-form.html delete mode 100644 templates/problem/voting-stats.html create mode 100644 templates/submission/user-ajax.html diff --git a/dmoj/urls.py b/dmoj/urls.py index a791537..f696a1e 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -310,7 +310,6 @@ urlpatterns = [ ticket.NewProblemTicketView.as_view(), name="new_problem_ticket", ), - url(r"^/vote$", problem.Vote.as_view(), name="vote"), url( r"^/manage/submission", include( @@ -529,11 +528,18 @@ urlpatterns = [ ), ), url( - r"^/submissions/(?P\w+)/(?P\w+)/", + r"^/submissions/(?P\w+)/(?P\w+)", paged_list_view( submission.UserContestSubmissions, "contest_user_submissions" ), ), + url( + r"^/submissions/(?P\w+)/(?P\w+)/ajax", + paged_list_view( + submission.UserContestSubmissionsAjax, + "contest_user_submissions_ajax", + ), + ), url( r"^/participations$", contests.ContestParticipationList.as_view(), diff --git a/judge/contest_format/default.py b/judge/contest_format/default.py index 11c8f37..0c053e0 100644 --- a/judge/contest_format/default.py +++ b/judge/contest_format/default.py @@ -55,7 +55,7 @@ class DefaultContestFormat(BaseContestFormat): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: return format_html( - '{points}
{time}
', + '{points}
{time}
', state=( ( "pretest-" @@ -68,7 +68,7 @@ class DefaultContestFormat(BaseContestFormat): ) ), url=reverse( - "contest_user_submissions", + "contest_user_submissions_ajax", args=[ self.contest.key, participation.user.user.username, diff --git a/judge/management/commands/makedmojmessages.py b/judge/management/commands/makedmojmessages.py index 44324e8..5c2a9ba 100644 --- a/judge/management/commands/makedmojmessages.py +++ b/judge/management/commands/makedmojmessages.py @@ -171,7 +171,8 @@ msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\\n" - """) + """ + ) if self.verbosity > 1: self.stdout.write("processing navigation bar") for label in NavigationBar.objects.values_list("label", flat=True): diff --git a/judge/models/problem.py b/judge/models/problem.py index bb99ae3..7211a69 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -561,29 +561,6 @@ class Problem(models.Model): save.alters_data = True - def can_vote(self, request): - return False - user = request.user - if not user.is_authenticated: - return False - - # If the user is in contest, nothing should be shown. - if request.in_contest_mode: - return False - - # If the user is not allowed to vote - if user.profile.is_unlisted or user.profile.is_banned_problem_voting: - return False - - # If the user is banned from submitting to the problem. - if self.banned_users.filter(pk=user.pk).exists(): - return False - - # If the user has a full AC submission to the problem (solved the problem). - return self.submission_set.filter( - user=user.profile, result="AC", points=F("problem__points") - ).exists() - class Meta: permissions = ( ("see_private_problem", "See hidden problems"), diff --git a/judge/views/organization.py b/judge/views/organization.py index 248f32c..9a5b52b 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -644,6 +644,7 @@ class KickUserWidgetView( LoginRequiredMixin, AdminOrganizationMixin, SingleObjectMixin, View ): model = Organization + def post(self, request, *args, **kwargs): organization = self.get_object() try: diff --git a/judge/views/problem.py b/judge/views/problem.py index 4c76f92..050f8fa 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -306,78 +306,9 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView): context["meta_description"] = self.object.summary or metadata[0] context["og_image"] = self.object.og_image or metadata[1] - context["can_vote"] = self.object.can_vote(self.request) - if context["can_vote"]: - try: - context["vote"] = ProblemPointsVote.objects.get( - voter=user.profile, problem=self.object - ) - except ObjectDoesNotExist: - context["vote"] = None - else: - context["vote"] = None - - context["has_votes"] = False - if user.is_superuser: - all_votes = list( - self.object.problem_points_votes.order_by("points").values_list( - "points", flat=True - ) - ) - context["all_votes"] = all_votes - context["has_votes"] = len(all_votes) > 0 - context["max_possible_vote"] = 600 - context["min_possible_vote"] = 100 return context -class DeleteVote(ProblemMixin, SingleObjectMixin, View): - def get(self, request, *args, **kwargs): - return HttpResponseForbidden(status=405, content_type="text/plain") - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - if not request.user.is_authenticated: - return HttpResponseForbidden("Not signed in.", content_type="text/plain") - elif self.object.can_vote(request.user): - ProblemPointsVote.objects.filter( - voter=request.profile, problem=self.object - ).delete() - return HttpResponse("success", content_type="text/plain") - else: - return HttpResponseForbidden( - "Not allowed to delete votes on this problem.", - content_type="text/plain", - ) - - -class Vote(ProblemMixin, SingleObjectMixin, View): - def get(self, request, *args, **kwargs): - return HttpResponseForbidden(status=405, content_type="text/plain") - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - if not self.object.can_vote(request): # Not allowed to vote for some reason. - return HttpResponseForbidden( - "Not allowed to vote on this problem.", content_type="text/plain" - ) - - form = ProblemPointsVoteForm(request.POST) - if form.is_valid(): - with transaction.atomic(): - # Delete any pre existing votes. - ProblemPointsVote.objects.filter( - voter=request.profile, problem=self.object - ).delete() - vote = form.save(commit=False) - vote.voter = request.profile - vote.problem = self.object - vote.save() - return JsonResponse({"points": vote.points}) - else: - return JsonResponse(form.errors, status=400) - - class LatexError(Exception): pass diff --git a/judge/views/submission.py b/judge/views/submission.py index d8e1473..3e284b3 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -18,6 +18,7 @@ from django.http import HttpResponseRedirect from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.shortcuts import render +from django.template.defaultfilters import floatformat from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property @@ -47,6 +48,7 @@ from judge.utils.problem_data import get_problem_case from judge.utils.raw_sql import join_sql_subquery, use_straight_join from judge.utils.views import DiggPaginatorMixin from judge.utils.views import TitleMixin +from judge.utils.timedelta import nice_repr def submission_related(queryset): @@ -358,7 +360,8 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): ) if self.selected_statuses: queryset = queryset.filter( - Q(result__in=self.selected_statuses) | Q(status__in=self.selected_statuses) + Q(result__in=self.selected_statuses) + | Q(status__in=self.selected_statuses) ) return queryset @@ -392,9 +395,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): hidden_codes = ["SC", "D", "G"] if not self.request.user.is_superuser and not self.request.user.is_staff: hidden_codes += ["IE"] - return [ - (key, value) for key, value in all_statuses if key not in hidden_codes - ] + return [(key, value) for key, value in all_statuses if key not in hidden_codes] def get_context_data(self, **kwargs): context = super(SubmissionsListBase, self).get_context_data(**kwargs) @@ -782,3 +783,30 @@ class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): self.contest.name, reverse("contest_view", args=[self.contest.key]), ) + + +class UserContestSubmissionsAjax(UserContestSubmissions): + template_name = "submission/user-ajax.html" + + def contest_time(self, s): + if s.contest.participation.live: + return s.date - s.contest.participation.real_start + return None + + def get_context_data(self, **kwargs): + context = super(UserContestSubmissionsAjax, self).get_context_data(**kwargs) + context["contest"] = self.contest + context["problem"] = self.problem + context["profile"] = self.profile + + contest_problem = self.contest.contest_problems.get(problem=self.problem) + for s in context["submissions"]: + contest_time = self.contest_time(s) + if contest_time: + s.contest_time = nice_repr(contest_time, "noday") + else: + s.contest_time = None + points = floatformat(s.contest.points, -self.contest.points_precision) + total = floatformat(contest_problem.points, -self.contest.points_precision) + s.display_point = f"{points} / {total}" + return context diff --git a/resources/submission.scss b/resources/submission.scss index c66a718..afb1f2e 100644 --- a/resources/submission.scss +++ b/resources/submission.scss @@ -362,3 +362,9 @@ label[for="language"], label[for="status"] { color: gray; } } + +.lightbox-submissions { + td { + padding-right: 0.2em; + } +} \ No newline at end of file diff --git a/templates/contest/media-js.html b/templates/contest/media-js.html index 3e0ac48..108e976 100644 --- a/templates/contest/media-js.html +++ b/templates/contest/media-js.html @@ -1,4 +1,142 @@ + \ No newline at end of file diff --git a/templates/contest/ranking-table.html b/templates/contest/ranking-table.html index d9d674e..8a97818 100644 --- a/templates/contest/ranking-table.html +++ b/templates/contest/ranking-table.html @@ -58,7 +58,7 @@ {% block before_point_head %} {% for problem in problems %} - + {{- contest.get_label_for_problem(loop.index0) }}
{{ problem.points }}
diff --git a/templates/contest/ranking.html b/templates/contest/ranking.html index c0d4e04..b9788fa 100644 --- a/templates/contest/ranking.html +++ b/templates/contest/ranking.html @@ -8,6 +8,8 @@ {% endblock %} {% block users_media %} + + {% if has_rating %} @@ -227,215 +235,6 @@ }); {% endif %} - - {% include "contest/media-js.html" %} {% endblock %} diff --git a/templates/problem/problem.html b/templates/problem/problem.html index 64e4b3d..c4ef5da 100644 --- a/templates/problem/problem.html +++ b/templates/problem/problem.html @@ -346,9 +346,6 @@ {% endblock %} {% block description %} - {% if can_vote and not vote %} - {% include 'problem/voting-form.html' %} - {% endif %} {% if contest_problem and contest_problem.contest.use_clarifications and has_clarifications %}
@@ -374,9 +371,6 @@ {% endblock %} {% block post_description_end %} - {% if can_vote or request.user.is_superuser %} - {% include 'problem/voting-controls.html' %} - {% endif %} {% if request.user.is_authenticated and not request.profile.mute %} diff --git a/templates/problem/voting-controls.html b/templates/problem/voting-controls.html deleted file mode 100644 index 1f93193..0000000 --- a/templates/problem/voting-controls.html +++ /dev/null @@ -1,75 +0,0 @@ - - -{% if can_vote or request.user.is_superuser %} - - - {% if can_vote %} - - {% endif %} - {% if request.user.is_superuser %} - - {% include 'problem/voting-stats.html' %} - {% endif %} - - - - -{% endif %} \ No newline at end of file diff --git a/templates/problem/voting-form.html b/templates/problem/voting-form.html deleted file mode 100644 index 0dc2fd3..0000000 --- a/templates/problem/voting-form.html +++ /dev/null @@ -1,93 +0,0 @@ - -
- {% csrf_token %} - - - - - - - - - - - - -
- {{_('How difficult is this problem?')}} - -
- - {% for i in range(1, (max_possible_vote - min_possible_vote) // 100 + 2) %} - - {% endfor %} - -
-
- {{_('This helps us improve the site')}} - - {{min_possible_vote}} - {{max_possible_vote}} -
-
- -
-
- -
-
- \ No newline at end of file diff --git a/templates/problem/voting-stats.html b/templates/problem/voting-stats.html deleted file mode 100644 index c37896e..0000000 --- a/templates/problem/voting-stats.html +++ /dev/null @@ -1,146 +0,0 @@ - -{{ _('Statistics') }} - - - \ No newline at end of file diff --git a/templates/submission/user-ajax.html b/templates/submission/user-ajax.html new file mode 100644 index 0000000..1b825e0 --- /dev/null +++ b/templates/submission/user-ajax.html @@ -0,0 +1,30 @@ +

+ {{_('Contest submissions of')}} {{link_user(profile)}} # +

+
+ +{% for submission in submissions %} + + {% set can_view = submission_layout(submission, profile_id, request.user, editable_problem_ids, completed_problem_ids) %} + + + + + {% if can_view %} + + {% endif %} + +{% endfor %} + \ No newline at end of file From 3fb78e714d8be7c41028ed77e8cc5862de9bbd61 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 1 Jun 2022 00:47:22 -0500 Subject: [PATCH 086/959] Fix small bugs --- judge/views/submission.py | 4 +++- resources/submission.scss | 1 + templates/contest/ranking.html | 8 ++++++++ templates/submission/user-ajax.html | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/judge/views/submission.py b/judge/views/submission.py index 3e284b3..c196445 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -790,7 +790,9 @@ class UserContestSubmissionsAjax(UserContestSubmissions): def contest_time(self, s): if s.contest.participation.live: - return s.date - s.contest.participation.real_start + if self.contest.time_limit: + return s.date - s.contest.participation.real_start + return s.date - self.contest.start_time return None def get_context_data(self, **kwargs): diff --git a/resources/submission.scss b/resources/submission.scss index afb1f2e..2846273 100644 --- a/resources/submission.scss +++ b/resources/submission.scss @@ -366,5 +366,6 @@ label[for="language"], label[for="status"] { .lightbox-submissions { td { padding-right: 0.2em; + font-size: 1.2em; } } \ No newline at end of file diff --git a/templates/contest/ranking.html b/templates/contest/ranking.html index b9788fa..4820192 100644 --- a/templates/contest/ranking.html +++ b/templates/contest/ranking.html @@ -137,6 +137,11 @@ border-radius: 10px; height: 80%; width: 60%; + overflow: auto; + } + + .featherlight { + /*position: re;*/ } @@ -183,6 +188,9 @@ width: 200px; height: 2.3em; } + html.with-featherlight { + overflow: unset; + } {% endblock %} diff --git a/templates/submission/user-ajax.html b/templates/submission/user-ajax.html index 1b825e0..de5e1a7 100644 --- a/templates/submission/user-ajax.html +++ b/templates/submission/user-ajax.html @@ -10,7 +10,7 @@ {% if submission.contest_time %} {{submission.contest_time}} {% else %} - {% trans time=submission.date|date(_("N j, Y, g:i a")) %} + {% trans time=submission.date|date(_("g:i a d/m/Y")) %} {{ time }} {% endtrans %} {% endif %} From 752ba6c6051c499981761d533b3ab1cfbf23b1df Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 1 Jun 2022 00:53:15 -0500 Subject: [PATCH 087/959] Add trans --- locale/vi/LC_MESSAGES/django.po | 282 +++++++++++++++------------- templates/submission/user-ajax.html | 6 +- 2 files changed, 157 insertions(+), 131 deletions(-) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 12d24d3..8ef9593 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-05-31 05:19+0700\n" +"POT-Creation-Date: 2022-06-01 12:50+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -280,7 +280,7 @@ msgid "Limits" msgstr "Giới hạn" #: judge/admin/problem.py:191 judge/admin/submission.py:353 -#: templates/stats/base.html:14 templates/submission/list.html:322 +#: templates/stats/base.html:14 templates/submission/list.html:321 msgid "Language" msgstr "Ngôn ngữ" @@ -631,7 +631,7 @@ msgstr "người bình luận" msgid "associated page" msgstr "trang tương ứng" -#: judge/models/comment.py:53 judge/models/problem.py:719 +#: judge/models/comment.py:53 judge/models/problem.py:696 msgid "votes" msgstr "bình chọn" @@ -651,7 +651,7 @@ msgstr "bình luận" msgid "comments" msgstr "" -#: judge/models/comment.py:160 judge/models/problem.py:687 +#: judge/models/comment.py:160 judge/models/problem.py:664 #, python-format msgid "Editorial for %s" msgstr "" @@ -769,7 +769,7 @@ msgstr "" msgid "description" msgstr "mô tả" -#: judge/models/contest.py:118 judge/models/problem.py:600 +#: judge/models/contest.py:118 judge/models/problem.py:577 #: judge/models/runtime.py:216 msgid "problems" msgstr "bài tập" @@ -783,7 +783,7 @@ msgid "end time" msgstr "thời gian kết thúc" #: judge/models/contest.py:123 judge/models/problem.py:204 -#: judge/models/problem.py:645 +#: judge/models/problem.py:622 msgid "time limit" msgstr "giới hạn thời gian" @@ -1088,8 +1088,8 @@ msgid "contest participations" msgstr "lần tham gia kỳ thi" #: judge/models/contest.py:739 judge/models/contest.py:783 -#: judge/models/contest.py:845 judge/models/problem.py:599 -#: judge/models/problem.py:606 judge/models/problem.py:637 +#: judge/models/contest.py:845 judge/models/problem.py:576 +#: judge/models/problem.py:583 judge/models/problem.py:614 #: judge/models/problem_data.py:49 msgid "problem" msgstr "bài tập" @@ -1227,7 +1227,7 @@ msgstr "mục cha" msgid "post title" msgstr "tiêu đề bài đăng" -#: judge/models/interface.py:75 judge/models/problem.py:676 +#: judge/models/interface.py:75 judge/models/problem.py:653 msgid "authors" msgstr "tác giả" @@ -1235,7 +1235,7 @@ msgstr "tác giả" msgid "slug" msgstr "slug" -#: judge/models/interface.py:77 judge/models/problem.py:674 +#: judge/models/interface.py:77 judge/models/problem.py:651 msgid "public visibility" msgstr "khả năng hiển thị công khai" @@ -1432,7 +1432,7 @@ msgid "" "are supported." msgstr "" -#: judge/models/problem.py:215 judge/models/problem.py:652 +#: judge/models/problem.py:215 judge/models/problem.py:629 msgid "memory limit" msgstr "" @@ -1505,82 +1505,82 @@ msgstr "" msgid "If private, only these organizations may see the problem." msgstr "" -#: judge/models/problem.py:611 judge/models/problem.py:642 +#: judge/models/problem.py:588 judge/models/problem.py:619 #: judge/models/runtime.py:161 msgid "language" msgstr "" -#: judge/models/problem.py:614 +#: judge/models/problem.py:591 msgid "translated name" msgstr "" -#: judge/models/problem.py:616 +#: judge/models/problem.py:593 msgid "translated description" msgstr "" -#: judge/models/problem.py:620 +#: judge/models/problem.py:597 msgid "problem translation" msgstr "" -#: judge/models/problem.py:621 +#: judge/models/problem.py:598 msgid "problem translations" msgstr "" -#: judge/models/problem.py:626 +#: judge/models/problem.py:603 msgid "clarified problem" msgstr "" -#: judge/models/problem.py:628 +#: judge/models/problem.py:605 msgid "clarification body" msgstr "" -#: judge/models/problem.py:630 +#: judge/models/problem.py:607 msgid "clarification timestamp" msgstr "" -#: judge/models/problem.py:661 +#: judge/models/problem.py:638 msgid "language-specific resource limit" msgstr "" -#: judge/models/problem.py:662 +#: judge/models/problem.py:639 msgid "language-specific resource limits" msgstr "" -#: judge/models/problem.py:669 +#: judge/models/problem.py:646 msgid "associated problem" msgstr "" -#: judge/models/problem.py:675 +#: judge/models/problem.py:652 msgid "publish date" msgstr "" -#: judge/models/problem.py:677 +#: judge/models/problem.py:654 msgid "editorial content" msgstr "nội dung lời giải" -#: judge/models/problem.py:691 +#: judge/models/problem.py:668 msgid "solution" msgstr "lời giải" -#: judge/models/problem.py:692 +#: judge/models/problem.py:669 msgid "solutions" msgstr "lời giải" -#: judge/models/problem.py:697 +#: judge/models/problem.py:674 #, fuzzy #| msgid "point value" msgid "proposed point value" msgstr "điểm" -#: judge/models/problem.py:698 +#: judge/models/problem.py:675 msgid "The amount of points you think this problem deserves." msgstr "" -#: judge/models/problem.py:712 +#: judge/models/problem.py:689 msgid "The time this vote was cast" msgstr "" -#: judge/models/problem.py:718 +#: judge/models/problem.py:695 msgid "vote" msgstr "" @@ -2465,16 +2465,16 @@ msgstr "Giới thiệu" msgid "Custom Checker Sample" msgstr "Hướng dẫn viết trình chấm" -#: judge/views/blog.py:112 +#: judge/views/blog.py:111 #, python-format msgid "Page %d of Posts" msgstr "Trang %d" -#: judge/views/blog.py:167 +#: judge/views/blog.py:166 msgid "Ticket feed" msgstr "Báo cáo" -#: judge/views/blog.py:185 +#: judge/views/blog.py:184 msgid "Comment feed" msgstr "Bình luận" @@ -2490,8 +2490,8 @@ msgstr "Bạn phải giải ít nhất 1 bài trước khi được vote." msgid "You already voted." msgstr "Bạn đã vote." -#: judge/views/comment.py:153 judge/views/organization.py:701 -#: judge/views/organization.py:788 +#: judge/views/comment.py:153 judge/views/organization.py:702 +#: judge/views/organization.py:794 msgid "Edited from site" msgstr "Chỉnh sửa từ web" @@ -2778,48 +2778,48 @@ msgstr "Thêm thành viên cho %s" msgid "Added members from site" msgstr "Chỉnh sửa từ web" -#: judge/views/organization.py:654 judge/views/organization.py:662 +#: judge/views/organization.py:655 judge/views/organization.py:663 msgid "Can't kick user" msgstr "Không thể đuổi" -#: judge/views/organization.py:655 +#: judge/views/organization.py:656 msgid "The user you are trying to kick does not exist!" msgstr "" -#: judge/views/organization.py:663 +#: judge/views/organization.py:664 #, python-format msgid "The user you are trying to kick is not in organization: %s." msgstr "" -#: judge/views/organization.py:684 +#: judge/views/organization.py:685 #, fuzzy, python-format #| msgid "Editing %s" msgid "Edit %s" msgstr "Đang chỉnh sửa %s" -#: judge/views/organization.py:718 +#: judge/views/organization.py:724 #, python-format msgid "Add blog for %s" msgstr "Thêm bài đăng cho %s" -#: judge/views/organization.py:729 +#: judge/views/organization.py:735 msgid "Added from site" msgstr "Thêm từ web" -#: judge/views/organization.py:763 +#: judge/views/organization.py:769 msgid "Permission denied" msgstr "Truy cập bị từ chối" -#: judge/views/organization.py:764 +#: judge/views/organization.py:770 msgid "Not allowed to edit this blog" msgstr "Bạn không được phép chỉnh sửa bài đăng này." -#: judge/views/organization.py:783 +#: judge/views/organization.py:789 #, python-format msgid "Edit blog %s" msgstr "Chỉnh sửa %s" -#: judge/views/organization.py:813 +#: judge/views/organization.py:819 #, python-format msgid "Pending blogs in %s" msgstr "Bài đang đợi duyệt trong %s" @@ -2843,41 +2843,41 @@ msgstr "Hướng dẫn cho {0}" msgid "Editorial for {0}" msgstr "Hướng dẫn cho {0}" -#: judge/views/problem.py:463 templates/contest/contest.html:79 -#: templates/extra_js.html:3 templates/organization/org-left-sidebar.html:4 +#: judge/views/problem.py:394 templates/contest/contest.html:79 +#: templates/organization/org-left-sidebar.html:4 #: templates/user/user-about.html:28 templates/user/user-tabs.html:5 #: templates/user/users-table.html:29 msgid "Problems" msgstr "Bài tập" -#: judge/views/problem.py:827 +#: judge/views/problem.py:758 msgid "Problem feed" msgstr "Bài tập" -#: judge/views/problem.py:1040 +#: judge/views/problem.py:971 msgid "Banned from submitting" msgstr "Bị cấm nộp bài" -#: judge/views/problem.py:1042 +#: judge/views/problem.py:973 msgid "" "You have been declared persona non grata for this problem. You are " "permanently barred from submitting this problem." msgstr "Bạn đã bị cấm nộp bài này." -#: judge/views/problem.py:1065 +#: judge/views/problem.py:996 msgid "Too many submissions" msgstr "Quá nhiều lần nộp" -#: judge/views/problem.py:1067 +#: judge/views/problem.py:998 msgid "You have exceeded the submission limit for this problem." msgstr "Bạn đã vượt quá số lần nộp cho bài này." -#: judge/views/problem.py:1146 judge/views/problem.py:1151 +#: judge/views/problem.py:1077 judge/views/problem.py:1082 #, python-format msgid "Submit to %(problem)s" msgstr "Nộp bài cho %(problem)s" -#: judge/views/problem.py:1173 +#: judge/views/problem.py:1104 msgid "Clone Problem" msgstr "Nhân bản bài tập" @@ -3011,7 +3011,7 @@ msgstr "Xác thực thất bại" msgid "Language statistics" msgstr "Thống kê ngôn ngữ" -#: judge/views/status.py:27 templates/submission/list.html:313 +#: judge/views/status.py:27 templates/submission/list.html:312 msgid "Status" msgstr "Kết quả chấm" @@ -3019,49 +3019,49 @@ msgstr "Kết quả chấm" msgid "Version matrix" msgstr "Ma trận phiên bản" -#: judge/views/submission.py:112 judge/views/submission.py:120 +#: judge/views/submission.py:114 judge/views/submission.py:122 #, python-format msgid "Submission of %(problem)s by %(user)s" msgstr "Bài nộp của %(user)s cho bài %(problem)s" -#: judge/views/submission.py:285 judge/views/submission.py:286 +#: judge/views/submission.py:287 judge/views/submission.py:288 #: templates/problem/problem.html:194 msgid "All submissions" msgstr "Tất cả bài nộp" -#: judge/views/submission.py:475 +#: judge/views/submission.py:480 msgid "All my submissions" msgstr "Tất cả bài nộp của tôi" -#: judge/views/submission.py:476 +#: judge/views/submission.py:481 #, python-format msgid "All submissions by %s" msgstr "Tất cả bài nộp của %s" -#: judge/views/submission.py:521 +#: judge/views/submission.py:526 #, python-format msgid "All submissions for %s" msgstr "Tất cả bài nộp cho %s" -#: judge/views/submission.py:543 +#: judge/views/submission.py:548 msgid "Must pass a problem" msgstr "Phải làm được một bài" -#: judge/views/submission.py:601 +#: judge/views/submission.py:606 #, python-format msgid "My submissions for %(problem)s" msgstr "Bài nộp của tôi cho %(problem)s" -#: judge/views/submission.py:602 +#: judge/views/submission.py:607 #, python-format msgid "%(user)s's submissions for %(problem)s" msgstr "Các bài nộp của %(user)s cho %(problem)s" -#: judge/views/submission.py:732 +#: judge/views/submission.py:737 msgid "Must pass a contest" msgstr "Phải qua một kỳ thi" -#: judge/views/submission.py:760 +#: judge/views/submission.py:765 #, python-brace-format msgid "" "{0}'s submissions for {2} in {0} cho {2} trong {4}" -#: judge/views/submission.py:772 +#: judge/views/submission.py:777 #, python-brace-format msgid "" "{0}'s submissions for problem {2} in {3}" @@ -3483,7 +3483,7 @@ msgid "You have no ticket" msgstr "Bạn không có báo cáo" #: templates/blog/list.html:94 templates/problem/list-base.html:392 -#: templates/problem/list.html:143 templates/problem/problem.html:407 +#: templates/problem/list.html:143 templates/problem/problem.html:401 msgid "Clarifications" msgstr "Thông báo" @@ -3492,7 +3492,7 @@ msgid "Add" msgstr "Thêm mới" #: templates/blog/list.html:119 templates/problem/list-base.html:414 -#: templates/problem/list.html:165 templates/problem/problem.html:418 +#: templates/problem/list.html:165 templates/problem/problem.html:412 msgid "No clarifications have been made at this time." msgstr "Không có thông báo nào." @@ -3725,8 +3725,8 @@ msgstr "Lịch" msgid "Info" msgstr "Thông tin" -#: templates/contest/contest-tabs.html:6 templates/problem/voting-stats.html:26 -#: templates/stats/base.html:9 templates/submission/list.html:339 +#: templates/contest/contest-tabs.html:6 templates/stats/base.html:9 +#: templates/submission/list.html:338 msgid "Statistics" msgstr "Thống kê" @@ -3844,7 +3844,7 @@ msgstr "Số lượng" msgid "Editorial" msgstr "Hướng dẫn" -#: templates/contest/list.html:90 templates/contest/media-js.html:9 +#: templates/contest/list.html:90 templates/contest/media-js.html:147 msgid "Are you sure you want to join?" msgstr "Bạn có chắc tham gia?" @@ -3933,11 +3933,11 @@ msgstr "Không có kỳ thi nào được lên lịch hiện tại." msgid "Past Contests" msgstr "Kỳ thi trong quá khứ" -#: templates/contest/media-js.html:4 +#: templates/contest/media-js.html:142 msgid "Are you sure you want to leave?" msgstr "Bạn có chắc muốn rời?" -#: templates/contest/media-js.html:5 +#: templates/contest/media-js.html:143 msgid "" "You cannot come back to a virtual participation. You will have to start a " "new one." @@ -3945,7 +3945,7 @@ msgstr "" "Bạn không thể quay lại lần tham gia ảo này. Bạn sẽ phải tham gia ảo lại từ " "đầu." -#: templates/contest/media-js.html:10 +#: templates/contest/media-js.html:148 msgid "" "Joining a contest starts your timer, after which it becomes unstoppable." msgstr "Tham gia kỳ thi sẽ khởi động đồng hồ đếm ngược, và không thể dừng lại." @@ -3998,35 +3998,35 @@ msgstr "Khôi phục kết quả" msgid "Disqualify" msgstr "Hủy kết quả" -#: templates/contest/ranking.html:187 +#: templates/contest/ranking.html:203 msgid "Are you sure you want to disqualify this participation?" msgstr "Bạn có chắc muốn hủy kết quả này?" -#: templates/contest/ranking.html:192 +#: templates/contest/ranking.html:208 msgid "Are you sure you want to un-disqualify this participation?" msgstr "Bạn có chắc muốn khôi phục kết quả này?" -#: templates/contest/ranking.html:446 +#: templates/contest/ranking.html:253 msgid "View user participation" msgstr "Xem các lần tham gia" -#: templates/contest/ranking.html:450 +#: templates/contest/ranking.html:257 msgid "Show organizations" msgstr "Hiển thị tổ chức" -#: templates/contest/ranking.html:454 +#: templates/contest/ranking.html:261 msgid "Show full name" msgstr "Hiển thị họ tên" -#: templates/contest/ranking.html:457 +#: templates/contest/ranking.html:264 msgid "Show friends only" msgstr "Chỉ hiển thị bạn bè" -#: templates/contest/ranking.html:460 +#: templates/contest/ranking.html:267 msgid "Total score only" msgstr "Chỉ hiển thị tổng điểm" -#: templates/contest/ranking.html:462 +#: templates/contest/ranking.html:269 msgid "Show virtual participation" msgstr "Hiển thị tham gia ảo" @@ -4062,10 +4062,6 @@ msgstr "Còn" msgid "Upcoming contests" msgstr "Kỳ thi sắp diễn ra" -#: templates/extra_js.html:4 -msgid "Submissions" -msgstr "Bài nộp" - #: templates/fine_uploader/script.html:4 msgid "Drop files here to upload" msgstr "" @@ -4525,7 +4521,7 @@ msgid "" msgstr "Bạn chuẩn bị {action} vài bài nộp. Tiếp tục?" #: templates/problem/manage_submission.html:127 -#: templates/submission/list.html:309 +#: templates/submission/list.html:308 msgid "Filter submissions" msgstr "Lọc bài nộp" @@ -4673,24 +4669,24 @@ msgstr[0] "Máy chấm:" msgid "none available" msgstr "không có sẵn" -#: templates/problem/problem.html:356 +#: templates/problem/problem.html:353 #, python-format msgid "This problem has %(length)s clarification(s)" msgstr "Bài này có %(length)s thông báo" -#: templates/problem/problem.html:384 +#: templates/problem/problem.html:378 msgid "Request clarification" msgstr "Yêu cầu làm rõ đề" -#: templates/problem/problem.html:386 +#: templates/problem/problem.html:380 msgid "Report an issue" msgstr "Báo cáo một vấn đề" -#: templates/problem/problem.html:395 +#: templates/problem/problem.html:389 msgid "View comments" msgstr "Xem bình luận" -#: templates/problem/problem.html:397 +#: templates/problem/problem.html:391 msgid "Be the first to comment" msgstr "Bình luận đầu tiên" @@ -4743,7 +4739,7 @@ msgstr "Tất cả" msgid "Point range" msgstr "Mốc điểm" -#: templates/problem/search-form.html:92 templates/submission/list.html:331 +#: templates/problem/search-form.html:92 templates/submission/list.html:330 #: templates/ticket/list.html:248 msgid "Go" msgstr "Lọc" @@ -4792,42 +4788,6 @@ msgstr "Không có máy chấm có thể chấm bài này." msgid "Submit!" msgstr "Nộp bài!" -#: templates/problem/voting-controls.html:55 -msgid "Edit difficulty" -msgstr "Thay đổi độ khó" - -#: templates/problem/voting-controls.html:63 -msgid "Vote difficulty" -msgstr "Bình chọn độ khó" - -#: templates/problem/voting-form.html:21 -msgid "How difficult is this problem?" -msgstr "Bạn thấy độ khó bài này thế nào?" - -#: templates/problem/voting-form.html:35 -msgid "This helps us improve the site" -msgstr "Bình chọn giúp admin cải thiện bài tập." - -#: templates/problem/voting-stats.html:29 -msgid "Voting Statistics" -msgstr "Thống kê" - -#: templates/problem/voting-stats.html:32 -msgid "No Votes Available!" -msgstr "Không có bình chọn nào!" - -#: templates/problem/voting-stats.html:35 -msgid "Median:" -msgstr "Trung vị:" - -#: templates/problem/voting-stats.html:37 -msgid "Mean:" -msgstr "Trung bình:" - -#: templates/problem/voting-stats.html:39 templates/submission/list.html:345 -msgid "Total:" -msgstr "Tổng:" - #: templates/registration/activate.html:3 #, python-format msgid "%(key)s is an invalid activation key." @@ -5085,7 +5045,11 @@ msgstr "Lọc theo kết quả..." msgid "Filter by language..." msgstr "Lọc theo ngôn ngữ..." -#: templates/submission/list.html:355 +#: templates/submission/list.html:344 +msgid "Total:" +msgstr "Tổng:" + +#: templates/submission/list.html:354 msgid "You were disconnected. Refresh to show latest updates." msgstr "Bạn bị ngắt kết nối. Hãy làm mới để xem cập nhật mới nhất." @@ -5219,6 +5183,37 @@ msgstr "Tốt nhất" msgid "%(user)s's" msgstr "" +#: templates/submission/user-ajax.html:2 +msgid "Contest submissions of" +msgstr "Các bài nộp của" + +#: templates/submission/user-ajax.html:13 +msgid "g:i a d/m/Y" +msgstr "" + +#: templates/submission/user-ajax.html:13 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " on %(time)s\n" +#| " " +msgid "" +"\n" +" %(time)s\n" +" " +msgstr "" +"\n" +" vào %(time)s\n" +" " + +#: templates/submission/user-ajax.html:22 +msgid "pretests" +msgstr "pretests" + +#: templates/submission/user-ajax.html:24 +msgid "main tests" +msgstr "test chính thức" + #: templates/ticket/feed.html:20 msgid " replied" msgstr "" @@ -5575,6 +5570,33 @@ msgstr "Thông tin" msgid "Check all" msgstr "Chọn tất cả" +#~ msgid "Submissions" +#~ msgstr "Bài nộp" + +#~ msgid "Edit difficulty" +#~ msgstr "Thay đổi độ khó" + +#~ msgid "Vote difficulty" +#~ msgstr "Bình chọn độ khó" + +#~ msgid "How difficult is this problem?" +#~ msgstr "Bạn thấy độ khó bài này thế nào?" + +#~ msgid "This helps us improve the site" +#~ msgstr "Bình chọn giúp admin cải thiện bài tập." + +#~ msgid "Voting Statistics" +#~ msgstr "Thống kê" + +#~ msgid "No Votes Available!" +#~ msgstr "Không có bình chọn nào!" + +#~ msgid "Median:" +#~ msgstr "Trung vị:" + +#~ msgid "Mean:" +#~ msgstr "Trung bình:" + #~ msgid "Edit organization" #~ msgstr "Chỉnh sửa" diff --git a/templates/submission/user-ajax.html b/templates/submission/user-ajax.html index de5e1a7..c612e43 100644 --- a/templates/submission/user-ajax.html +++ b/templates/submission/user-ajax.html @@ -18,7 +18,11 @@ {{submission.display_point}} ({{submission.short_status}}) - [{{_('pretests') if submission.contest.is_pretest else _('main tests')}}] + {% if submission.contest.is_pretest %} + [{{_('pretests')}}] + {% else %} + [{{_('main tests')}}] + {% endif %} {% if can_view %} From 957f04dcf149181d253c1c5dbb9ab2b75f9a3b69 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 1 Jun 2022 11:53:17 -0500 Subject: [PATCH 088/959] Relax access check --- judge/views/submission.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/judge/views/submission.py b/judge/views/submission.py index c196445..c52da75 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -753,11 +753,6 @@ class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): self.contest.name, ) - def access_check(self, request): - super(UserContestSubmissions, self).access_check(request) - if not self.contest.users.filter(user_id=self.profile.id).exists(): - raise Http404() - def get_content_title(self): if self.problem.is_accessible_by(self.request.user): return format_html( From 1e35ba864fb269398a88a14d5fcd6ff338b24ab3 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 1 Jun 2022 11:59:58 -0500 Subject: [PATCH 089/959] Add back access check --- judge/views/submission.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/judge/views/submission.py b/judge/views/submission.py index c52da75..c196445 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -753,6 +753,11 @@ class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): self.contest.name, ) + def access_check(self, request): + super(UserContestSubmissions, self).access_check(request) + if not self.contest.users.filter(user_id=self.profile.id).exists(): + raise Http404() + def get_content_title(self): if self.problem.is_accessible_by(self.request.user): return format_html( From 78b818901ea66247e51d3331553dc2c147c7749b Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 1 Jun 2022 14:31:20 -0500 Subject: [PATCH 090/959] Relax contest submission view --- judge/models/problem.py | 4 ++-- judge/views/submission.py | 19 +++++++++++++++---- templates/submission/user-ajax.html | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/judge/models/problem.py b/judge/models/problem.py index 7211a69..4562775 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -333,7 +333,7 @@ class Problem(models.Model): return True return user.has_perm("judge.edit_own_problem") and self.is_editor(user.profile) - def is_accessible_by(self, user): + def is_accessible_by(self, user, in_contest_mode=True): # Problem is public. if self.is_public: # Problem is not private to an organization. @@ -367,7 +367,7 @@ class Problem(models.Model): # If user is currently in a contest containing that problem. current = user.profile.current_contest_id - if current is None: + if not in_contest_mode or current is None: return False from judge.models import ContestProblem diff --git a/judge/views/submission.py b/judge/views/submission.py index c196445..df3c898 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -506,7 +506,7 @@ class AllUserSubmissions(ConditionalUserTabMixin, UserMixin, SubmissionsListBase class ProblemSubmissionsBase(SubmissionsListBase): show_problem = False dynamic_update = True - check_contest_in_access_check = True + check_contest_in_access_check = False def get_queryset(self): if ( @@ -535,13 +535,16 @@ class ProblemSubmissionsBase(SubmissionsListBase): def access_check_contest(self, request): if self.in_contest and not self.contest.can_see_own_scoreboard(request.user): raise Http404() - - def access_check(self, request): - if not self.problem.is_accessible_by(request.user): + if not self.contest.is_accessible_by(request.user): raise Http404() + def access_check(self, request): if self.check_contest_in_access_check: self.access_check_contest(request) + else: + is_own = hasattr(self, 'is_own') and self.is_own + if not is_own and not self.problem.is_accessible_by(request.user, request.in_contest_mode): + raise Http404() def get(self, request, *args, **kwargs): if "problem" not in kwargs: @@ -740,6 +743,8 @@ class ForceContestMixin(object): class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): + check_contest_in_access_check = True + def get_title(self): if self.problem.is_accessible_by(self.request.user): return "%s's submissions for %s in %s" % ( @@ -812,3 +817,9 @@ class UserContestSubmissionsAjax(UserContestSubmissions): total = floatformat(contest_problem.points, -self.contest.points_precision) s.display_point = f"{points} / {total}" return context + + def get(self, request, *args, **kwargs): + try: + return super(UserContestSubmissionsAjax, self).get(request, *args, **kwargs) + except Http404: + return HttpResponse(_("You don't have permission to access.")) \ No newline at end of file diff --git a/templates/submission/user-ajax.html b/templates/submission/user-ajax.html index c612e43..91a6e29 100644 --- a/templates/submission/user-ajax.html +++ b/templates/submission/user-ajax.html @@ -6,7 +6,7 @@ {% for submission in submissions %} {% set can_view = submission_layout(submission, profile_id, request.user, editable_problem_ids, completed_problem_ids) %} - + {% if submission.contest_time %} {{submission.contest_time}} {% else %} From 76b631366be64299488fb1a2db4e51a165a1ba91 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 1 Jun 2022 15:18:52 -0500 Subject: [PATCH 091/959] Fix mobile UI --- judge/views/submission.py | 1 + resources/submission.scss | 15 ++- templates/contest/ranking-css.html | 185 ++++++++++++++++++++++++++++ templates/contest/ranking.html | 184 +-------------------------- templates/submission/user-ajax.html | 2 +- 5 files changed, 198 insertions(+), 189 deletions(-) create mode 100644 templates/contest/ranking-css.html diff --git a/judge/views/submission.py b/judge/views/submission.py index df3c898..8fdcf4b 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -805,6 +805,7 @@ class UserContestSubmissionsAjax(UserContestSubmissions): context["contest"] = self.contest context["problem"] = self.problem context["profile"] = self.profile + context["profile_id"] = self.request.profile.id if self.request.profile else None contest_problem = self.contest.contest_problems.get(problem=self.problem) for s in context["submissions"]: diff --git a/resources/submission.scss b/resources/submission.scss index 2846273..a1dff72 100644 --- a/resources/submission.scss +++ b/resources/submission.scss @@ -363,9 +363,14 @@ label[for="language"], label[for="status"] { } } -.lightbox-submissions { - td { - padding-right: 0.2em; - font-size: 1.2em; +@media (min-width: 800px) { + .lightbox-submissions { + .lightbox-submissions-time { + padding-right: 1em; + } + td { + padding-right: 0.2em; + font-size: 1.2em; + } } -} \ No newline at end of file +} diff --git a/templates/contest/ranking-css.html b/templates/contest/ranking-css.html new file mode 100644 index 0000000..a0a10d6 --- /dev/null +++ b/templates/contest/ranking-css.html @@ -0,0 +1,185 @@ + + +{% if has_rating %} + +{% endif %} diff --git a/templates/contest/ranking.html b/templates/contest/ranking.html index 4820192..0ee131b 100644 --- a/templates/contest/ranking.html +++ b/templates/contest/ranking.html @@ -9,189 +9,7 @@ {% block users_media %} - - - - {% if has_rating %} - - {% endif %} - - + {% include "contest/ranking-css.html" %} {% endblock %} {% block users_js_media %} diff --git a/templates/submission/user-ajax.html b/templates/submission/user-ajax.html index 91a6e29..c9450af 100644 --- a/templates/submission/user-ajax.html +++ b/templates/submission/user-ajax.html @@ -6,7 +6,7 @@ {% for submission in submissions %} {% set can_view = submission_layout(submission, profile_id, request.user, editable_problem_ids, completed_problem_ids) %} - + {% if submission.contest_time %} {{submission.contest_time}} {% else %} From c9091f2e7560be5d1c240c362805c9a8927cea5b Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 1 Jun 2022 23:43:31 -0500 Subject: [PATCH 092/959] Remove caniuse to always use jax --- judge/template_context.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/judge/template_context.py b/judge/template_context.py index 3c1900d..e518bab 100644 --- a/judge/template_context.py +++ b/judge/template_context.py @@ -6,7 +6,6 @@ from django.contrib.sites.shortcuts import get_current_site from django.core.cache import cache from django.utils.functional import SimpleLazyObject, new_method_proxy -from judge.utils.caniuse import CanIUse, SUPPORT from .models import MiscConfig, NavigationBar, Profile @@ -121,14 +120,10 @@ def site_name(request): def math_setting(request): - caniuse = CanIUse(request.META.get("HTTP_USER_AGENT", "")) - if request.user.is_authenticated: engine = request.profile.math_engine else: engine = settings.MATHOID_DEFAULT_TYPE if engine == "auto": - engine = ( - "mml" if bool(settings.MATHOID_URL) and caniuse.mathml == SUPPORT else "jax" - ) - return {"MATH_ENGINE": engine, "REQUIRE_JAX": engine == "jax", "caniuse": caniuse} + engine = "jax" + return {"MATH_ENGINE": engine, "REQUIRE_JAX": engine == "jax"} From a1bcc2cb468baaadafa7cc73fcb4ca294d62f94d Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 1 Jun 2022 23:59:46 -0500 Subject: [PATCH 093/959] Add file io --- judge/migrations/0124_auto_20220602_1116.py | 23 +++ judge/models/problem_data.py | 6 + judge/utils/problem_data.py | 8 + judge/views/problem_data.py | 5 + locale/vi/LC_MESSAGES/django.po | 176 +++++++++++--------- templates/problem/problem.html | 10 ++ templates/problem/raw.html | 6 + 7 files changed, 154 insertions(+), 80 deletions(-) create mode 100644 judge/migrations/0124_auto_20220602_1116.py diff --git a/judge/migrations/0124_auto_20220602_1116.py b/judge/migrations/0124_auto_20220602_1116.py new file mode 100644 index 0000000..d8c045d --- /dev/null +++ b/judge/migrations/0124_auto_20220602_1116.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.25 on 2022-06-02 04:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0123_auto_20220502_2356'), + ] + + operations = [ + migrations.AddField( + model_name='problemdata', + name='fileio_input', + field=models.TextField(blank=True, null=True, verbose_name='input file name'), + ), + migrations.AddField( + model_name='problemdata', + name='fileio_output', + field=models.TextField(blank=True, null=True, verbose_name='output file name'), + ), + ] diff --git a/judge/models/problem_data.py b/judge/models/problem_data.py index 8617a61..2902bab 100644 --- a/judge/models/problem_data.py +++ b/judge/models/problem_data.py @@ -105,6 +105,12 @@ class ProblemData(models.Model): upload_to=problem_directory_file, validators=[FileExtensionValidator(allowed_extensions=["cpp"])], ) + fileio_input = models.TextField( + verbose_name=_("input file name"), blank=True, null=True + ) + fileio_output = models.TextField( + verbose_name=_("output file name"), blank=True, null=True + ) __original_zipfile = None diff --git a/judge/utils/problem_data.py b/judge/utils/problem_data.py index aeb64cb..0ebc8d0 100644 --- a/judge/utils/problem_data.py +++ b/judge/utils/problem_data.py @@ -239,6 +239,14 @@ class ProblemDataCompiler(object): init["checker"] = make_checker(self.data) else: self.data.checker_args = "" + if self.data.fileio_input: + if "file_io" not in init: + init["file_io"] = {} + init["file_io"]["input"] = self.data.fileio_input + if self.data.fileio_output: + if "file_io" not in init: + init["file_io"] = {} + init["file_io"]["output"] = self.data.fileio_output return init diff --git a/judge/views/problem_data.py b/judge/views/problem_data.py index 0f0b38a..7472d52 100644 --- a/judge/views/problem_data.py +++ b/judge/views/problem_data.py @@ -26,6 +26,7 @@ from django.forms import ( Select, formset_factory, FileInput, + TextInput, ) from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render @@ -88,6 +89,8 @@ class ProblemDataForm(ModelForm): "custom_checker", "custom_validator", "interactive_judge", + "fileio_input", + "fileio_output", ] widgets = { "zipfile": FineUploadFileInput, @@ -95,6 +98,8 @@ class ProblemDataForm(ModelForm): "generator": HiddenInput, "output_limit": HiddenInput, "output_prefix": HiddenInput, + "fileio_input": TextInput, + "fileio_output": TextInput, } diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 8ef9593..d5f0587 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-06-01 12:50+0700\n" +"POT-Creation-Date: 2022-06-02 11:58+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -1636,11 +1636,11 @@ msgstr "file zip chứa test" msgid "generator file" msgstr "file tạo test" -#: judge/models/problem_data.py:68 judge/models/problem_data.py:190 +#: judge/models/problem_data.py:68 judge/models/problem_data.py:196 msgid "output prefix length" msgstr "độ dài hiển thị output" -#: judge/models/problem_data.py:71 judge/models/problem_data.py:193 +#: judge/models/problem_data.py:71 judge/models/problem_data.py:199 msgid "output limit length" msgstr "giới hạn hiển thị output" @@ -1648,15 +1648,15 @@ msgstr "giới hạn hiển thị output" msgid "init.yml generation feedback" msgstr "phản hồi của quá trình tạo file init.yml" -#: judge/models/problem_data.py:77 judge/models/problem_data.py:196 +#: judge/models/problem_data.py:77 judge/models/problem_data.py:202 msgid "checker" msgstr "trình chấm" -#: judge/models/problem_data.py:80 judge/models/problem_data.py:199 +#: judge/models/problem_data.py:80 judge/models/problem_data.py:205 msgid "checker arguments" msgstr "các biến trong trình chấm" -#: judge/models/problem_data.py:82 judge/models/problem_data.py:201 +#: judge/models/problem_data.py:82 judge/models/problem_data.py:207 msgid "checker arguments as a JSON object" msgstr "các biến trong trình chấm theo dạng JSON" @@ -1672,47 +1672,47 @@ msgstr "file trình chấm" msgid "interactive judge" msgstr "" -#: judge/models/problem_data.py:165 -msgid "problem data set" -msgstr "tập hợp dữ liệu bài" - -#: judge/models/problem_data.py:169 -msgid "case position" -msgstr "vị trí test" - -#: judge/models/problem_data.py:172 -msgid "case type" -msgstr "loại test" - -#: judge/models/problem_data.py:174 -msgid "Normal case" -msgstr "Test bình thường" - -#: judge/models/problem_data.py:175 -msgid "Batch start" -msgstr "Bắt đầu nhóm" - -#: judge/models/problem_data.py:176 -msgid "Batch end" -msgstr "Kết thúc nhóm" - -#: judge/models/problem_data.py:181 +#: judge/models/problem_data.py:109 judge/models/problem_data.py:187 msgid "input file name" msgstr "tên file input" -#: judge/models/problem_data.py:184 +#: judge/models/problem_data.py:112 judge/models/problem_data.py:190 msgid "output file name" msgstr "tên file output" -#: judge/models/problem_data.py:186 +#: judge/models/problem_data.py:171 +msgid "problem data set" +msgstr "tập hợp dữ liệu bài" + +#: judge/models/problem_data.py:175 +msgid "case position" +msgstr "vị trí test" + +#: judge/models/problem_data.py:178 +msgid "case type" +msgstr "loại test" + +#: judge/models/problem_data.py:180 +msgid "Normal case" +msgstr "Test bình thường" + +#: judge/models/problem_data.py:181 +msgid "Batch start" +msgstr "Bắt đầu nhóm" + +#: judge/models/problem_data.py:182 +msgid "Batch end" +msgstr "Kết thúc nhóm" + +#: judge/models/problem_data.py:192 msgid "generator arguments" msgstr "biến trong file sinh test" -#: judge/models/problem_data.py:187 +#: judge/models/problem_data.py:193 msgid "point value" msgstr "điểm" -#: judge/models/problem_data.py:188 +#: judge/models/problem_data.py:194 msgid "case is pretest?" msgstr "test là pretest?" @@ -2881,39 +2881,39 @@ msgstr "Nộp bài cho %(problem)s" msgid "Clone Problem" msgstr "Nhân bản bài tập" -#: judge/views/problem_data.py:68 +#: judge/views/problem_data.py:69 msgid "Checker arguments must be a JSON object" msgstr "" -#: judge/views/problem_data.py:70 +#: judge/views/problem_data.py:71 msgid "Checker arguments is invalid JSON" msgstr "" -#: judge/views/problem_data.py:77 +#: judge/views/problem_data.py:78 msgid "Your zip file is invalid!" msgstr "File Zip không hợp lệ!" -#: judge/views/problem_data.py:157 +#: judge/views/problem_data.py:162 #, python-brace-format msgid "Comparing submissions for {0}" msgstr "So sánh các bài nộp cho {0}" -#: judge/views/problem_data.py:161 +#: judge/views/problem_data.py:166 #, python-brace-format msgid "Comparing submissions for {0}" msgstr "So sánh các bài nộp cho {0}" -#: judge/views/problem_data.py:198 +#: judge/views/problem_data.py:203 #, python-brace-format msgid "Editing data for {0}" msgstr "Chỉnh sửa dữ liệu cho {0}" -#: judge/views/problem_data.py:202 +#: judge/views/problem_data.py:207 #, python-format msgid "Editing data for %s" msgstr "Chỉnh sửa dữ liệu cho %s" -#: judge/views/problem_data.py:332 judge/views/problem_data.py:334 +#: judge/views/problem_data.py:337 judge/views/problem_data.py:339 #, python-format msgid "Generated init.yml for %s" msgstr "File init.yml cho %s" @@ -3043,25 +3043,25 @@ msgstr "Tất cả bài nộp của %s" msgid "All submissions for %s" msgstr "Tất cả bài nộp cho %s" -#: judge/views/submission.py:548 +#: judge/views/submission.py:551 msgid "Must pass a problem" msgstr "Phải làm được một bài" -#: judge/views/submission.py:606 +#: judge/views/submission.py:609 #, python-format msgid "My submissions for %(problem)s" msgstr "Bài nộp của tôi cho %(problem)s" -#: judge/views/submission.py:607 +#: judge/views/submission.py:610 #, python-format msgid "%(user)s's submissions for %(problem)s" msgstr "Các bài nộp của %(user)s cho %(problem)s" -#: judge/views/submission.py:737 +#: judge/views/submission.py:740 msgid "Must pass a contest" msgstr "Phải qua một kỳ thi" -#: judge/views/submission.py:765 +#: judge/views/submission.py:770 #, python-brace-format msgid "" "{0}'s submissions for {2} in {0} cho {2} trong {4}" -#: judge/views/submission.py:777 +#: judge/views/submission.py:782 #, python-brace-format msgid "" "{0}'s submissions for problem {2} in {3}" @@ -3079,6 +3079,12 @@ msgstr "" "Các bài nộp của {0} cho bài {2} trong {3}" "" +#: judge/views/submission.py:826 +#, fuzzy +#| msgid "You do not have the permission to rejudge submissions." +msgid "You don't have permission to access." +msgstr "Bạn không có quyền chấm lại bài." + #: judge/views/ticket.py:65 judge/views/ticket.py:71 msgid "Ticket title" msgstr "Tiêu đề báo cáo" @@ -3483,7 +3489,7 @@ msgid "You have no ticket" msgstr "Bạn không có báo cáo" #: templates/blog/list.html:94 templates/problem/list-base.html:392 -#: templates/problem/list.html:143 templates/problem/problem.html:401 +#: templates/problem/list.html:143 templates/problem/problem.html:411 msgid "Clarifications" msgstr "Thông báo" @@ -3492,7 +3498,7 @@ msgid "Add" msgstr "Thêm mới" #: templates/blog/list.html:119 templates/problem/list-base.html:414 -#: templates/problem/list.html:165 templates/problem/problem.html:412 +#: templates/problem/list.html:165 templates/problem/problem.html:422 msgid "No clarifications have been made at this time." msgstr "Không có thông báo nào." @@ -3998,35 +4004,35 @@ msgstr "Khôi phục kết quả" msgid "Disqualify" msgstr "Hủy kết quả" -#: templates/contest/ranking.html:203 +#: templates/contest/ranking.html:21 msgid "Are you sure you want to disqualify this participation?" msgstr "Bạn có chắc muốn hủy kết quả này?" -#: templates/contest/ranking.html:208 +#: templates/contest/ranking.html:26 msgid "Are you sure you want to un-disqualify this participation?" msgstr "Bạn có chắc muốn khôi phục kết quả này?" -#: templates/contest/ranking.html:253 +#: templates/contest/ranking.html:71 msgid "View user participation" msgstr "Xem các lần tham gia" -#: templates/contest/ranking.html:257 +#: templates/contest/ranking.html:75 msgid "Show organizations" msgstr "Hiển thị tổ chức" -#: templates/contest/ranking.html:261 +#: templates/contest/ranking.html:79 msgid "Show full name" msgstr "Hiển thị họ tên" -#: templates/contest/ranking.html:264 +#: templates/contest/ranking.html:82 msgid "Show friends only" msgstr "Chỉ hiển thị bạn bè" -#: templates/contest/ranking.html:267 +#: templates/contest/ranking.html:85 msgid "Total score only" msgstr "Chỉ hiển thị tổng điểm" -#: templates/contest/ranking.html:269 +#: templates/contest/ranking.html:87 msgid "Show virtual participation" msgstr "Hiển thị tham gia ảo" @@ -4641,60 +4647,78 @@ msgstr "Thời gian:" msgid "Memory limit:" msgstr "Bộ nhớ:" -#: templates/problem/problem.html:277 +#: templates/problem/problem.html:262 templates/problem/raw.html:64 +#: templates/submission/status-testcases.html:141 +msgid "Input:" +msgstr "Input:" + +#: templates/problem/problem.html:264 templates/problem/raw.html:64 +msgid "stdin" +msgstr "bàn phím" + +#: templates/problem/problem.html:268 templates/problem/raw.html:67 +#: templates/submission/status-testcases.html:145 +msgid "Output:" +msgstr "Output:" + +#: templates/problem/problem.html:269 templates/problem/raw.html:67 +msgid "stdout" +msgstr "màn hình" + +#: templates/problem/problem.html:287 msgid "Author:" msgid_plural "Authors:" msgstr[0] "Tác giả:" -#: templates/problem/problem.html:292 +#: templates/problem/problem.html:302 msgid "Problem type" msgid_plural "Problem types" msgstr[0] "Dạng bài" -#: templates/problem/problem.html:305 +#: templates/problem/problem.html:315 msgid "Allowed languages" msgstr "Ngôn ngữ cho phép" -#: templates/problem/problem.html:313 +#: templates/problem/problem.html:323 #, python-format msgid "No %(lang)s judge online" msgstr "Không có máy chấm cho %(lang)s" -#: templates/problem/problem.html:324 +#: templates/problem/problem.html:334 msgid "Judge:" msgid_plural "Judges:" msgstr[0] "Máy chấm:" -#: templates/problem/problem.html:341 +#: templates/problem/problem.html:351 msgid "none available" msgstr "không có sẵn" -#: templates/problem/problem.html:353 +#: templates/problem/problem.html:363 #, python-format msgid "This problem has %(length)s clarification(s)" msgstr "Bài này có %(length)s thông báo" -#: templates/problem/problem.html:378 +#: templates/problem/problem.html:388 msgid "Request clarification" msgstr "Yêu cầu làm rõ đề" -#: templates/problem/problem.html:380 +#: templates/problem/problem.html:390 msgid "Report an issue" msgstr "Báo cáo một vấn đề" -#: templates/problem/problem.html:389 +#: templates/problem/problem.html:399 msgid "View comments" msgstr "Xem bình luận" -#: templates/problem/problem.html:391 +#: templates/problem/problem.html:401 msgid "Be the first to comment" msgstr "Bình luận đầu tiên" -#: templates/problem/raw.html:64 +#: templates/problem/raw.html:70 msgid "Time Limit:" msgstr "Giới hạn thời gian:" -#: templates/problem/raw.html:73 +#: templates/problem/raw.html:79 msgid "Memory Limit:" msgstr "Giới hạn bộ nhớ:" @@ -5138,14 +5162,6 @@ msgstr "Pretest" msgid "Test case" msgstr "Test" -#: templates/submission/status-testcases.html:141 -msgid "Input:" -msgstr "Input:" - -#: templates/submission/status-testcases.html:145 -msgid "Output:" -msgstr "Output:" - #: templates/submission/status-testcases.html:149 msgid "Answer:" msgstr "Kết quả:" diff --git a/templates/problem/problem.html b/templates/problem/problem.html index c4ef5da..83ca7e4 100644 --- a/templates/problem/problem.html +++ b/templates/problem/problem.html @@ -258,6 +258,16 @@ {{ _('Memory limit:') }} {{ problem.memory_limit|kbsimpleformat }}
+
+ {{ _('Input:') }} + + {{ problem.data_files.fileio_input or _('stdin') }} + +
+
+ {{ _('Output:') }} + {{ problem.data_files.fileio_output or _('stdout') }} +
{% for name, limit in problem.language_memory_limit %}
diff --git a/templates/problem/raw.html b/templates/problem/raw.html index bca3412..f677ab6 100644 --- a/templates/problem/raw.html +++ b/templates/problem/raw.html @@ -60,6 +60,12 @@

{{ problem_name }}


+
+ {{ _('Input:') }} {{ problem.data_files.fileio_input or _('stdin') }} +
+
+ {{ _('Output:') }} {{ problem.data_files.fileio_output or _('stdout') }} +
{{ _('Time Limit:') }} {{ problem.time_limit }}s {% for name, limit in problem.language_time_limit %} From 8ba39a1f9727810069c37e82c25d1c759b48c915 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 2 Jun 2022 00:20:45 -0500 Subject: [PATCH 094/959] Add help text and trans --- judge/migrations/0124_auto_20220602_1116.py | 18 ++++++----- judge/migrations/0125_auto_20220602_1216.py | 33 +++++++++++++++++++++ judge/models/problem_data.py | 10 +++++-- judge/views/submission.py | 12 +++++--- locale/vi/LC_MESSAGES/django.po | 28 +++++++++++++---- templates/problem/data.html | 6 ++-- 6 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 judge/migrations/0125_auto_20220602_1216.py diff --git a/judge/migrations/0124_auto_20220602_1116.py b/judge/migrations/0124_auto_20220602_1116.py index d8c045d..5b15aef 100644 --- a/judge/migrations/0124_auto_20220602_1116.py +++ b/judge/migrations/0124_auto_20220602_1116.py @@ -6,18 +6,22 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0123_auto_20220502_2356'), + ("judge", "0123_auto_20220502_2356"), ] operations = [ migrations.AddField( - model_name='problemdata', - name='fileio_input', - field=models.TextField(blank=True, null=True, verbose_name='input file name'), + model_name="problemdata", + name="fileio_input", + field=models.TextField( + blank=True, null=True, verbose_name="input file name" + ), ), migrations.AddField( - model_name='problemdata', - name='fileio_output', - field=models.TextField(blank=True, null=True, verbose_name='output file name'), + model_name="problemdata", + name="fileio_output", + field=models.TextField( + blank=True, null=True, verbose_name="output file name" + ), ), ] diff --git a/judge/migrations/0125_auto_20220602_1216.py b/judge/migrations/0125_auto_20220602_1216.py new file mode 100644 index 0000000..18bee1c --- /dev/null +++ b/judge/migrations/0125_auto_20220602_1216.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.25 on 2022-06-02 05:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0124_auto_20220602_1116"), + ] + + operations = [ + migrations.AlterField( + model_name="problemdata", + name="fileio_input", + field=models.TextField( + blank=True, + help_text="Leave empty for stdin", + null=True, + verbose_name="input file name", + ), + ), + migrations.AlterField( + model_name="problemdata", + name="fileio_output", + field=models.TextField( + blank=True, + help_text="Leave empty for stdout", + null=True, + verbose_name="output file name", + ), + ), + ] diff --git a/judge/models/problem_data.py b/judge/models/problem_data.py index 2902bab..f219197 100644 --- a/judge/models/problem_data.py +++ b/judge/models/problem_data.py @@ -106,10 +106,16 @@ class ProblemData(models.Model): validators=[FileExtensionValidator(allowed_extensions=["cpp"])], ) fileio_input = models.TextField( - verbose_name=_("input file name"), blank=True, null=True + verbose_name=_("input file name"), + blank=True, + null=True, + help_text=_("Leave empty for stdin"), ) fileio_output = models.TextField( - verbose_name=_("output file name"), blank=True, null=True + verbose_name=_("output file name"), + blank=True, + null=True, + help_text=_("Leave empty for stdout"), ) __original_zipfile = None diff --git a/judge/views/submission.py b/judge/views/submission.py index 8fdcf4b..65656c2 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -542,8 +542,10 @@ class ProblemSubmissionsBase(SubmissionsListBase): if self.check_contest_in_access_check: self.access_check_contest(request) else: - is_own = hasattr(self, 'is_own') and self.is_own - if not is_own and not self.problem.is_accessible_by(request.user, request.in_contest_mode): + is_own = hasattr(self, "is_own") and self.is_own + if not is_own and not self.problem.is_accessible_by( + request.user, request.in_contest_mode + ): raise Http404() def get(self, request, *args, **kwargs): @@ -805,7 +807,9 @@ class UserContestSubmissionsAjax(UserContestSubmissions): context["contest"] = self.contest context["problem"] = self.problem context["profile"] = self.profile - context["profile_id"] = self.request.profile.id if self.request.profile else None + context["profile_id"] = ( + self.request.profile.id if self.request.profile else None + ) contest_problem = self.contest.contest_problems.get(problem=self.problem) for s in context["submissions"]: @@ -823,4 +827,4 @@ class UserContestSubmissionsAjax(UserContestSubmissions): try: return super(UserContestSubmissionsAjax, self).get(request, *args, **kwargs) except Http404: - return HttpResponse(_("You don't have permission to access.")) \ No newline at end of file + return HttpResponse(_("You don't have permission to access.")) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index d5f0587..08d7b4e 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-06-02 11:58+0700\n" +"POT-Creation-Date: 2022-06-02 12:17+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -1676,10 +1676,18 @@ msgstr "" msgid "input file name" msgstr "tên file input" +#: judge/models/problem_data.py:109 +msgid "Leave empty for stdin" +msgstr "Để trống nếu nhập từ bàn phím" + #: judge/models/problem_data.py:112 judge/models/problem_data.py:190 msgid "output file name" msgstr "tên file output" +#: judge/models/problem_data.py:112 +msgid "Leave empty for stdout" +msgstr "Để trống nếu xuất ra màn hình" + #: judge/models/problem_data.py:171 msgid "problem data set" msgstr "tập hợp dữ liệu bài" @@ -4387,6 +4395,19 @@ msgstr "Hướng dẫn" msgid "View YAML" msgstr "Xem YAML" +#: templates/problem/data.html:454 +msgid "Autofill testcases" +msgstr "Tự động điền test" + +#: templates/problem/data.html:457 templates/problem/problem.html:302 +msgid "Problem type" +msgid_plural "Problem types" +msgstr[0] "Dạng bài" + +#: templates/problem/data.html:464 +msgid "Fill testcases" +msgstr "Điền test" + #: templates/problem/data.html:468 templates/problem/data.html:519 msgid "Apply!" msgstr "Lưu!" @@ -4670,11 +4691,6 @@ msgid "Author:" msgid_plural "Authors:" msgstr[0] "Tác giả:" -#: templates/problem/problem.html:302 -msgid "Problem type" -msgid_plural "Problem types" -msgstr[0] "Dạng bài" - #: templates/problem/problem.html:315 msgid "Allowed languages" msgstr "Ngôn ngữ cho phép" diff --git a/templates/problem/data.html b/templates/problem/data.html index ce0eb9b..1b77d85 100644 --- a/templates/problem/data.html +++ b/templates/problem/data.html @@ -451,17 +451,17 @@ {{ data_form.as_table() }}
- + - Problem type: + {{_('Problem type')}}: - Fill testcases + {{_('Fill testcases')}}
From b89c12770775ddcce75a00794528212a39717a32 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 2 Jun 2022 11:21:55 -0500 Subject: [PATCH 095/959] Fix io display bug --- judge/views/problem.py | 6 ++++++ templates/problem/problem.html | 4 ++-- templates/problem/raw.html | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/judge/views/problem.py b/judge/views/problem.py index 050f8fa..7fd44a1 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -305,6 +305,12 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView): ) context["meta_description"] = self.object.summary or metadata[0] context["og_image"] = self.object.og_image or metadata[1] + if hasattr(self.object, "data_files"): + context["fileio_input"] = self.object.data_files.fileio_input + context["fileio_output"] = self.object.data_files.fileio_output + else: + context["fileio_input"] = None + context["fileio_output"] = None return context diff --git a/templates/problem/problem.html b/templates/problem/problem.html index 83ca7e4..672f303 100644 --- a/templates/problem/problem.html +++ b/templates/problem/problem.html @@ -261,12 +261,12 @@
{{ _('Input:') }} - {{ problem.data_files.fileio_input or _('stdin') }} + {{ fileio_input or _('stdin') }}
{{ _('Output:') }} - {{ problem.data_files.fileio_output or _('stdout') }} + {{ fileio_output or _('stdout') }}
{% for name, limit in problem.language_memory_limit %} diff --git a/templates/problem/raw.html b/templates/problem/raw.html index f677ab6..591bedb 100644 --- a/templates/problem/raw.html +++ b/templates/problem/raw.html @@ -61,10 +61,10 @@
- {{ _('Input:') }} {{ problem.data_files.fileio_input or _('stdin') }} + {{ _('Input:') }} {{ fileio_input or _('stdin') }}
- {{ _('Output:') }} {{ problem.data_files.fileio_output or _('stdout') }} + {{ _('Output:') }} {{ fileio_output or _('stdout') }}
{{ _('Time Limit:') }} {{ problem.time_limit }}s From 5e963c64945a7e3b849d26f590025fd456a7187e Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 2 Jun 2022 11:23:16 -0500 Subject: [PATCH 096/959] Fix io bug for problem raw --- judge/views/problem.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/judge/views/problem.py b/judge/views/problem.py index 7fd44a1..1035717 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -207,6 +207,12 @@ class ProblemRaw( context["problem_name"] = self.object.name context["url"] = self.request.build_absolute_uri() context["description"] = self.object.description + if hasattr(self.object, "data_files"): + context["fileio_input"] = self.object.data_files.fileio_input + context["fileio_output"] = self.object.data_files.fileio_output + else: + context["fileio_input"] = None + context["fileio_output"] = None return context def get(self, request, *args, **kwargs): From ba69ec2ffc019b8745a0f087d04581eb2a6690e8 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 2 Jun 2022 17:09:24 -0500 Subject: [PATCH 097/959] Change UI --- resources/blog.scss | 156 +++++++++--------- resources/problem.scss | 2 +- templates/organization/org-right-sidebar.html | 22 ++- templates/three-column-content.html | 7 +- 4 files changed, 96 insertions(+), 91 deletions(-) diff --git a/resources/blog.scss b/resources/blog.scss index 5db8d5f..95e313c 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -39,6 +39,8 @@ .left-sidebar-item.active { color: green; + font-weight: bold; + background-color: lightgreen; .sidebar-icon { color: green; } @@ -101,67 +103,6 @@ color: #555; } -@media (max-width: 799px) { - .left-sidebar-header { - display: none; - } - .left-sidebar-item { - display: inline-block; - flex: 1; - } - .left-sidebar { - text-align: right; - margin-bottom: 1em; - border-radius: 7px; - display: flex; - } -} -@media (min-width: 800px) { - .middle-content, .blog-sidebar, .right-sidebar { - display: block !important; - } - - .middle-content { - margin-right: 1em !important; - } - - #mobile.tabs { - display: none; - } - - #three-col-container { - display: flex; - } - - .middle-content { - max-width: 71.5%; - margin-left: 10%; - } - - .blog-sidebar, .right-sidebar { - width: 18%; - } - - .left-sidebar { - width: 8%; - margin-right: 1em; - position: fixed; - height: 100%; - margin-top: -4em; - padding-top: 4em; - border-right: 3px solid black; - } - - .feed-table { - font-size: small; - } - - .blog-box { - border-left: 2px solid black; - border-right: 2px solid black; - border-radius: 8px; - } -} #mobile.tabs { margin: 0; @@ -194,9 +135,9 @@ } .blog-box { - border-bottom: 1px solid black; - border-top: 2px solid black; - margin-bottom: 1em; + border-bottom: 1.4px solid lightgray; + border-top: 1.4px solid lightgray; + margin-bottom: 1.5em; width: 90%; padding: 1em 1.25em 0.5em 1.25em; background-color: white; @@ -230,22 +171,21 @@ color: gray; } -.left-sidebar { - background-color: #f0f1f3; - color: #616161; -} - .left-sidebar-item { - padding: 1em 0.5em; - text-align: center; + padding: 0.8em 0.2em 0.8em 1em; + .sidebar-icon { + width: 1.2em; + display: inline-block; + } } .left-sidebar-item:hover { - background-color: lightgray; + background-color: #e3e3e3; cursor: pointer; } +.left-sidebar-item.active:hover { + background-color: lightgreen; +} .sidebar-icon { - font-size: x-large; - margin-bottom: 0.1em; color: black; } .left-sidebar-header { @@ -257,4 +197,72 @@ } .feed-table { margin: 0; +} +.pre-expand-blog { + background-image: -webkit-linear-gradient(bottom, gray, lightgray 3%, transparent 8%, transparent 100%); + padding-bottom: 0; +} +@media (max-width: 799px) { + .left-sidebar-header { + display: none; + } + .left-sidebar-item { + display: inline-block; + flex: 1; + border-radius: 8px; + } + .left-sidebar { + text-align: center; + margin-bottom: 1em; + border-radius: 7px; + display: flex; + } +} +@media (min-width: 800px) { + .middle-content, .blog-sidebar, .right-sidebar { + display: block !important; + } + + .middle-content { + margin-right: 1em !important; + } + + #mobile.tabs { + display: none; + } + + #three-col-container { + display: flex; + } + + .middle-content { + max-width: 65%; + margin-left: 13%; + } + + .blog-sidebar, .right-sidebar { + width: 20%; + } + + .left-sidebar { + width: 11%; + position: fixed; + height: 100%; + margin-top: -4em; + padding-top: 4em; + } + + .left-sidebar-item { + border-radius: 0 2em 2em 0; + } + + .feed-table { + font-size: small; + } + + .blog-box { + border-left: 1.4px solid lightgray; + border-right: 1.4px solid lightgray; + border-radius: 8px; + } } \ No newline at end of file diff --git a/resources/problem.scss b/resources/problem.scss index 63d4a55..804ca69 100644 --- a/resources/problem.scss +++ b/resources/problem.scss @@ -318,7 +318,7 @@ ul.problem-list { } .problem-feed-option-item.active { - background: goldenrod; + background: lightblue; color: darkblue; } diff --git a/templates/organization/org-right-sidebar.html b/templates/organization/org-right-sidebar.html index bd5c572..496115b 100644 --- a/templates/organization/org-right-sidebar.html +++ b/templates/organization/org-right-sidebar.html @@ -2,18 +2,6 @@ {% if (is_member or can_edit) %} {% include 'contests-countdown.html' %} {% endif %} - {% if is_member or can_edit %} - - {% endif %} {% if can_edit or is_member %} + {% endif %} {% include 'top-users.html' %}
\ No newline at end of file diff --git a/templates/three-column-content.html b/templates/three-column-content.html index cb6825c..4e54d12 100644 --- a/templates/three-column-content.html +++ b/templates/three-column-content.html @@ -31,8 +31,7 @@ }) $('.blog-description').each(function() { if ($(this).prop('scrollHeight') > $(this).height() ) { - $(this).parent().css('background-image', '-webkit-linear-gradient(bottom, gray, lightgray 3%, transparent 8%, transparent 100%)'); - $(this).parent().css('padding-bottom', '0'); + $(this).parent().addClass('pre-expand-blog'); $(this).css('cursor', 'pointer'); } }); @@ -43,8 +42,8 @@ {% macro make_tab_item(name, fa, url, text) %} {% endmacro %} From 3d2b7b44d582f72c8ca94b9afbe14cf0cd9e711d Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 2 Jun 2022 17:29:00 -0500 Subject: [PATCH 098/959] Add blog hover --- resources/blog.scss | 3 +++ templates/three-column-content.html | 14 +++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/resources/blog.scss b/resources/blog.scss index 95e313c..09e49a7 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -202,6 +202,9 @@ background-image: -webkit-linear-gradient(bottom, gray, lightgray 3%, transparent 8%, transparent 100%); padding-bottom: 0; } +.pre-expand-blog:hover { + background-color: #f3f3f3; +} @media (max-width: 799px) { .left-sidebar-header { display: none; diff --git a/templates/three-column-content.html b/templates/three-column-content.html index 4e54d12..3913a89 100644 --- a/templates/three-column-content.html +++ b/templates/three-column-content.html @@ -20,18 +20,18 @@ if (url === '#') return; window.location.href = url; }); - $('.blog-description').on('click', function() { - var max_height = $(this).css('max-height'); + $('.blog-box').on('click', function() { + var $description = $(this).children('.blog-description'); + var max_height = $description.css('max-height'); if (max_height !== 'fit-content') { - $(this).css('max-height', 'fit-content'); - $(this).parent().css('background-image', 'inherit') - .css('padding-bottom', '0.5em'); + $description.css('max-height', 'fit-content'); $(this).css('cursor', 'auto'); + $(this).removeClass('pre-expand-blog'); } }) - $('.blog-description').each(function() { + $('.blog-box').each(function() { if ($(this).prop('scrollHeight') > $(this).height() ) { - $(this).parent().addClass('pre-expand-blog'); + $(this).addClass('pre-expand-blog'); $(this).css('cursor', 'pointer'); } }); From d26597c9b63db9516e1afa193ddb522f32b5a750 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 2 Jun 2022 17:35:25 -0500 Subject: [PATCH 099/959] Remove icon on mobile --- resources/blog.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/blog.scss b/resources/blog.scss index 09e49a7..a9680cd 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -213,6 +213,10 @@ display: inline-block; flex: 1; border-radius: 8px; + + .sidebar-icon { + display: none; + } } .left-sidebar { text-align: center; From 253693d1ea9ef69013bf39a0326bb53863b3cd38 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 2 Jun 2022 22:14:01 -0500 Subject: [PATCH 100/959] Fix submission page bug --- judge/views/submission.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/judge/views/submission.py b/judge/views/submission.py index 65656c2..078dd61 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -533,10 +533,11 @@ class ProblemSubmissionsBase(SubmissionsListBase): ) def access_check_contest(self, request): - if self.in_contest and not self.contest.can_see_own_scoreboard(request.user): - raise Http404() - if not self.contest.is_accessible_by(request.user): - raise Http404() + if self.in_contest: + if not self.contest.can_see_own_scoreboard(request.user): + raise Http404() + if not self.contest.is_accessible_by(request.user): + raise Http404() def access_check(self, request): if self.check_contest_in_access_check: From 2ee1885880386f196e5fd348ee17965a17c8f3cf Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 3 Jun 2022 01:27:51 -0500 Subject: [PATCH 101/959] Use bridged checker --- judge/utils/problem_data.py | 41 ++++++++++++------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/judge/utils/problem_data.py b/judge/utils/problem_data.py index 0ebc8d0..17b0065 100644 --- a/judge/utils/problem_data.py +++ b/judge/utils/problem_data.py @@ -13,9 +13,6 @@ from django.urls import reverse from django.utils.translation import gettext as _ from django.core.cache import cache -VALIDATOR_TEMPLATE_PATH = "validator_template/template.py" - - if os.altsep: def split_path_first( @@ -75,30 +72,6 @@ class ProblemDataCompiler(object): raise ProblemDataError(_("Empty batches not allowed.")) cases.append(batch) - def make_checker_for_validator(case): - checker_name = "cppvalidator.py" - validator_path = split_path_first(case.custom_validator.name) - - if len(validator_path) != 2: - raise ProblemDataError( - _("How did you corrupt the custom checker path?") - ) - - checker = os.path.join( - settings.DMOJ_PROBLEM_DATA_ROOT, validator_path[0], checker_name - ) - - validator_name = validator_path[1] - shutil.copy(VALIDATOR_TEMPLATE_PATH, checker) - - # replace {{filecpp}} and {{problemid}} in checker file - filedata = open(checker, "r").read() - filedata = filedata.replace("{{filecpp}}", "'%s'" % validator_name) - filedata = filedata.replace("{{problemid}}", "'%s'" % validator_path[0]) - open(checker, "w").write(filedata) - - return checker_name - def make_checker(case): if case.checker == "custom": custom_checker_path = split_path_first(case.custom_checker.name) @@ -109,7 +82,19 @@ class ProblemDataCompiler(object): return custom_checker_path[1] if case.checker == "customval": - return make_checker_for_validator(case) + custom_checker_path = split_path_first(case.custom_validator.name) + if len(custom_checker_path) != 2: + raise ProblemDataError( + _("How did you corrupt the custom checker path?") + ) + return { + "name": "bridged", + "args": { + "files": custom_checker_path[1], + "lang": "CPP14", + "type": "lqdoj", + }, + } if case.checker_args: return { From 18024584081ac576eefa393e054ad939bd0969b2 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 3 Jun 2022 14:01:49 -0500 Subject: [PATCH 102/959] Fix bug --- judge/views/problem.py | 3 +- templates/problem/list-base.html | 200 +------------------------- templates/problem/recent-attempt.html | 40 +++--- 3 files changed, 26 insertions(+), 217 deletions(-) diff --git a/judge/views/problem.py b/judge/views/problem.py index 1035717..aa913d8 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -512,7 +512,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView def get_org_query(self, query): if not self.profile: - return None + return [] return [ i for i in query @@ -541,6 +541,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView ) if self.org_query: self.org_query = self.get_org_query(self.org_query) + print(self.org_query) queryset = queryset.filter( Q(organizations__in=self.org_query) | Q(contests__contest__organizations__in=self.org_query) diff --git a/templates/problem/list-base.html b/templates/problem/list-base.html index 2eabfe6..d97dc83 100644 --- a/templates/problem/list-base.html +++ b/templates/problem/list-base.html @@ -55,13 +55,6 @@ @@ -251,195 +246,6 @@ {% endif %} {% endblock %} -{% block middle_content %} - {% if in_contest_mode or page_type == 'list' %} -
- - - - {% if request.in_contest_mode %} - {% if request.user.is_authenticated %} - - {% endif %} - - - {% if show_types %} - - {% endif %} - - - {% else %} - {% if request.user.is_authenticated %} - - {% endif %} - - - - {% if show_types %} - - {% endif %} - - - - {% if show_editorial %} - - {% endif %} - {% endif %} - - - - {% for problem in object_list %} - - {% if request.user.is_authenticated %} - {% if problem.id in completed_problem_ids %} - - {% elif problem.id in attempted_problems %} - - {% else %} - - {% endif %} - {% endif %} - - - {% if not request.in_contest_mode %} - - {% endif %} - {% if show_types %} - - {% endif %} - - {% if not request.in_contest_mode %} - - {% endif %} - - {% if show_editorial%} - - {% endif %} - - {% endfor %} - -
{{ _('Problem') }}{{ _('Problem code') }}{{ _('Types') }}{{ _('Points') }}{{ _('Users') }} - {{ sort_order.solved }} - - - {{ _('Problem') }}{{ sort_order.name }} - - {{ _('Problem code') }} - - {{ _('Category') }}{{ sort_order.group }} - - {{ _('Types') }}{{ sort_order.type }} - - {{ _('Points') }}{{ sort_order.points }} - - {{ _('AC %%') }}{{ sort_order.ac_rate }} - - {{ _('Users') }}{{ sort_order.user_count }} - - {{_('Editorial')}} -
- - {% if problem.is_public or request.in_contest_mode %} - - {% else %} - - {% endif %} - - - - {% if problem.is_public or request.in_contest_mode %} - - {% else %} - - {% endif %} - - - {% if problem.is_public or request.in_contest_mode %} - - {% else %} - - {% endif %} - - {{ problem.i18n_name }} - - {{ problem.code }} - {{ problem.group.full_name }} - {% for type in problem.types_list %} - {{ type }}{% if not loop.last %}, {% endif %} - {% endfor %} - {{ problem.points|floatformat }}{% if problem.partial %}p{% endif %}{{ problem.ac_rate|floatformat(1) }}% - - {% if not request.in_contest_mode or not hide_contest_scoreboard %} - {{ problem.user_count }} - {% else %} - ??? - {% endif %} - - - {% if problem.has_public_editorial %} - {{ _('Editorial') }} - {% endif %} -
- {% if request.in_contest_mode and request.participation.contest.use_clarifications %} -

- {% if can_edit_contest %} - - {% endif %} - {% if has_clarifications %} -
- {{_('Clarifications')}}
- - - - - - - {% for clarification in clarifications %} - - - - - - {% endfor %} -
{{_('Problem')}}{{_('Time')}}{{_('Description')}}
- - {{ clarification.problem.name }} - - {{ relative_time(clarification.date) }}

{{clarification.description}}

- {% else %} -

- {{ _('No clarifications have been made at this time.') }} -

- {% endif %} - {% endif %} - -
- {% elif page_type == 'feed' %} -
- - {{_('FOR YOU')}} - - - {{_('NEW')}} - - {% if request.user.has_perm('judge.suggest_problem_changes') %} - - {{_('VOLUNTEER')}} - - {% endif %} -
- {% for problem in problems %} - {% include "problem/feed.html" %} - {% endfor %} - {% endif %} - {% if page_obj.num_pages > 1 %} -
{% include "list-pages.html" %}
- {% endif %} -{% endblock %} - {% block right_sidebar %} {% if not request.in_contest_mode %}
{% endblock %} From 0e324a3cce05664d67cd0aee49d8a271be03dac9 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sun, 26 Jun 2022 12:07:34 +0700 Subject: [PATCH 139/959] Only allow open org to show members --- judge/admin/problem.py | 2 +- judge/migrations/0128_auto_20220620_2210.py | 15 ++++-- judge/migrations/0129_auto_20220622_1424.py | 50 ++++++++++++++++--- judge/utils/problem_data.py | 4 +- judge/views/organization.py | 11 ++++ templates/organization/org-left-sidebar.html | 4 +- templates/organization/org-right-sidebar.html | 4 +- 7 files changed, 75 insertions(+), 15 deletions(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index b52c63a..0cab70a 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -70,7 +70,7 @@ class ProblemForm(ModelForm): ), "types": AdminSelect2MultipleWidget, "group": AdminSelect2Widget, - "memory_limit": TextInput(attrs={'size':'20'}), + "memory_limit": TextInput(attrs={"size": "20"}), } if HeavyPreviewAdminPageDownWidget is not None: widgets["description"] = HeavyPreviewAdminPageDownWidget( diff --git a/judge/migrations/0128_auto_20220620_2210.py b/judge/migrations/0128_auto_20220620_2210.py index ef74ad5..27fed32 100644 --- a/judge/migrations/0128_auto_20220620_2210.py +++ b/judge/migrations/0128_auto_20220620_2210.py @@ -7,13 +7,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0127_auto_20220616_1442'), + ("judge", "0127_auto_20220616_1442"), ] operations = [ migrations.AlterField( - model_name='problem', - name='memory_limit', - field=models.PositiveIntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 256mb = 262144 kilobytes).', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1048576)], verbose_name='memory limit'), + model_name="problem", + name="memory_limit", + field=models.PositiveIntegerField( + help_text="The memory limit for this problem, in kilobytes (e.g. 256mb = 262144 kilobytes).", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1048576), + ], + verbose_name="memory limit", + ), ), ] diff --git a/judge/migrations/0129_auto_20220622_1424.py b/judge/migrations/0129_auto_20220622_1424.py index 601bb85..ab1356a 100644 --- a/judge/migrations/0129_auto_20220622_1424.py +++ b/judge/migrations/0129_auto_20220622_1424.py @@ -6,18 +6,54 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0128_auto_20220620_2210'), + ("judge", "0128_auto_20220620_2210"), ] operations = [ migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)'), ('interact', 'Interactive'), ('testlib', 'Testlib')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ("interact", "Interactive"), + ("testlib", "Testlib"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)'), ('interact', 'Interactive'), ('testlib', 'Testlib')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ("interact", "Interactive"), + ("testlib", "Testlib"), + ], + max_length=10, + verbose_name="checker", + ), ), ] diff --git a/judge/utils/problem_data.py b/judge/utils/problem_data.py index 95b5a12..6ace1bc 100644 --- a/judge/utils/problem_data.py +++ b/judge/utils/problem_data.py @@ -232,7 +232,9 @@ class ProblemDataCompiler(object): if self.data.checker == "interact": interactor_path = split_path_first(self.data.interactive_judge.name) if len(interactor_path) != 2: - raise ProblemDataError(_("How did you corrupt the interactor path?")) + raise ProblemDataError( + _("How did you corrupt the interactor path?") + ) init["interactive"] = { "files": interactor_path[1], "feedback": True, diff --git a/judge/views/organization.py b/judge/views/organization.py index 1afd2e0..8154eb9 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -306,6 +306,17 @@ class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView): default_desc = all_sorts default_sort = "-performance_points" + def dispatch(self, request, *args, **kwargs): + res = super(OrganizationUsers, self).dispatch(request, *args, **kwargs) + if self.can_access(self.organization) or self.organization.is_open: + return res + return generic_message( + request, + _("Can't access organization"), + _("You are not allowed to access this organization."), + status=403, + ) + def get_context_data(self, **kwargs): context = super(OrganizationUsers, self).get_context_data(**kwargs) context["title"] = _("%s Members") % self.object.name diff --git a/templates/organization/org-left-sidebar.html b/templates/organization/org-left-sidebar.html index 8f2c442..9e7ab6d 100644 --- a/templates/organization/org-left-sidebar.html +++ b/templates/organization/org-left-sidebar.html @@ -5,7 +5,9 @@ {{ make_tab_item('contests', 'fa fa-trophy', organization.get_contests_url(), _('Contests')) }} {{ make_tab_item('submissions', 'fa fa-book', organization.get_submissions_url(), _('Submissions')) }} {% endif %} - {{ make_tab_item('users', 'fa fa-user', organization.get_users_url(), _('Members')) }} + {% if is_member or can_edit or organization.is_open %} + {{ make_tab_item('users', 'fa fa-user', organization.get_users_url(), _('Members')) }} + {% endif %} {% if perms.judge.change_organization %} {{ make_tab_item('admin', 'fa fa-edit', url('admin:judge_organization_change', organization.id), _('Admin')) }} {% endif %} diff --git a/templates/organization/org-right-sidebar.html b/templates/organization/org-right-sidebar.html index 496115b..22db950 100644 --- a/templates/organization/org-right-sidebar.html +++ b/templates/organization/org-right-sidebar.html @@ -70,5 +70,7 @@
{% endif %} - {% include 'top-users.html' %} + {% if is_member or can_edit or organization.is_open %} + {% include 'top-users.html' %} + {% endif %}
\ No newline at end of file From a19a79531679cfac08583c6375ef3b8a39f7fbb8 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sun, 26 Jun 2022 14:24:38 +0700 Subject: [PATCH 140/959] Add notification for pending blogs --- judge/views/organization.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/judge/views/organization.py b/judge/views/organization.py index 8154eb9..bd68c65 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -51,6 +51,7 @@ from judge.models import ( Problem, Profile, Contest, + Notification, ) from judge import event_poster as event from judge.utils.ranker import ranker @@ -801,6 +802,24 @@ class AddOrganizationBlog( revisions.set_comment(_("Added from site")) revisions.set_user(self.request.user) + + link = reverse( + "edit_organization_blog", + args=[self.organization.id, self.organization.slug, self.object.id], + ) + html = ( + f'{self.object.title} - {self.organization.name}' + ) + for user in self.organization.admins.all(): + if user.id == self.request.profile.id: + continue + notification = Notification( + owner=user, + author=self.request.profile, + category="Add blog", + html_link=html, + ) + notification.save() return res @@ -860,6 +879,24 @@ class EditOrganizationBlog( res = super(EditOrganizationBlog, self).form_valid(form) revisions.set_comment(_("Edited from site")) revisions.set_user(self.request.user) + + link = reverse( + "edit_organization_blog", + args=[self.organization.id, self.organization.slug, self.object.id], + ) + html = ( + f'{self.object.title} - {self.organization.name}' + ) + for user in self.organization.admins.all(): + if user.id == self.request.profile.id: + continue + notification = Notification( + owner=user, + author=self.request.profile, + category="Edit blog", + html_link=html, + ) + notification.save() return res From 4cf77f95d85ecdec68c58d350fdb16ce134526e5 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 28 Jun 2022 19:23:28 +0700 Subject: [PATCH 141/959] Fix notification --- resources/common.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/resources/common.js b/resources/common.js index 1fed1ce..88584e4 100644 --- a/resources/common.js +++ b/resources/common.js @@ -334,13 +334,6 @@ window.register_contest_notification = function(url) { console.log("Fail to update clarification"); }) .done(function(data) { - try { - JSON.parse(data); - } - catch (e) { - return; - } - for (i of data) { window.notify_clarification(i); } From ee6aaaf1aff7d10692741ca16b31e3b64a786b2b Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 5 Jul 2022 10:07:47 +0700 Subject: [PATCH 142/959] Change mobile width of blog --- resources/blog.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/blog.scss b/resources/blog.scss index 6f94f81..6f3480c 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -226,6 +226,11 @@ border-radius: 7px; display: flex; } + .blog-box { + width: 95%; + padding-left: 0; + padding-right: 0; + } } @media (min-width: 800px) { .left-sidebar-item { From 91ab000d804b37b23b1b06190fe5ddf41e503b57 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 5 Jul 2022 10:19:32 +0700 Subject: [PATCH 143/959] Fix mobile x scroll --- resources/base.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/base.scss b/resources/base.scss index 835e41c..c4ca219 100644 --- a/resources/base.scss +++ b/resources/base.scss @@ -683,7 +683,6 @@ math { #content { width: auto; - padding: 0 5px; } } From 12dc691f205efb0325b7563d47a1e801d3f65cc0 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 5 Jul 2022 10:23:43 +0700 Subject: [PATCH 144/959] Change mobile x width --- resources/blog.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/blog.scss b/resources/blog.scss index 6f3480c..f23c56f 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -227,7 +227,6 @@ display: flex; } .blog-box { - width: 95%; padding-left: 0; padding-right: 0; } From 8a4ec11d82298e0ab31d7bbd20fed6410734d02a Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 5 Jul 2022 12:54:04 +0700 Subject: [PATCH 145/959] Fix mobile width: --- resources/base.scss | 4 ---- resources/blog.scss | 4 ++-- templates/problem/list-base.html | 16 ++++++++++++---- templates/three-column-content.html | 14 +++++++++++--- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/resources/base.scss b/resources/base.scss index c4ca219..244c922 100644 --- a/resources/base.scss +++ b/resources/base.scss @@ -680,10 +680,6 @@ math { } } } - - #content { - width: auto; - } } @media not all and (max-width: 760px) { diff --git a/resources/blog.scss b/resources/blog.scss index f23c56f..0f5fdd9 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -227,8 +227,8 @@ display: flex; } .blog-box { - padding-left: 0; - padding-right: 0; + padding-left: 5%; + padding-right: 5%; } } @media (min-width: 800px) { diff --git a/templates/problem/list-base.html b/templates/problem/list-base.html index 869a626..201247e 100644 --- a/templates/problem/list-base.html +++ b/templates/problem/list-base.html @@ -30,10 +30,6 @@ ul.problem-list { padding: 0 !important; } - #content { - width: 99%; - margin-left: 0; - } .volunteer-types { width: 100%; } @@ -41,6 +37,18 @@ height: 2em; padding-top: 4px; } + @media(min-width: 800px) { + #content { + width: 99%; + margin-left: 0; + } + } + @media(max-width: 799px) { + #content { + width: 100%; + margin-left: 0; + } + } {% else %} {% block three_col_media %}{% endblock %} From ab64ab61346a3d4fc42760f69db629271bfac859 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 5 Jul 2022 13:03:15 +0700 Subject: [PATCH 146/959] Fix submission status on mobile --- templates/submission/source.html | 1 + templates/submission/status.html | 1 + 2 files changed, 2 insertions(+) diff --git a/templates/submission/source.html b/templates/submission/source.html index f69331c..8f9b9d8 100644 --- a/templates/submission/source.html +++ b/templates/submission/source.html @@ -25,6 +25,7 @@ {% endblock %} {% block body %} +

diff --git a/templates/submission/status.html b/templates/submission/status.html index 41dc7f5..2b29dbd 100644 --- a/templates/submission/status.html +++ b/templates/submission/status.html @@ -55,6 +55,7 @@ {% endblock %} {% block body %} +

{% if request.user == submission.user.user or perms.judge.resubmit_other %} From 1ef68e0fdb33df80a01fb604e9e06f007c6e5613 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 15 Jul 2022 13:00:34 +0700 Subject: [PATCH 147/959] Upgrade pagedown --- dmoj/urls.py | 4 +++- judge/views/comment.py | 12 ++++++----- judge/widgets/pagedown.py | 21 ++++++++++++++++---- requirements.txt | 2 +- resources/pagedown_widget.css | 3 +++ templates/comments/edit-ajax.html | 7 ++----- templates/comments/media-js.html | 4 +--- templates/pagedown.html | 33 +++++++++++++++++++++++++------ 8 files changed, 61 insertions(+), 25 deletions(-) diff --git a/dmoj/urls.py b/dmoj/urls.py index e184e0d..c136ea9 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -12,6 +12,7 @@ from django.utils.functional import lazystr from django.utils.translation import ugettext_lazy as _ from django.views.generic import RedirectView from django.contrib.auth.decorators import login_required +from django.conf.urls.static import static as url_static from judge.feed import ( @@ -201,6 +202,7 @@ def paged_list_view(view, name, **kwargs): urlpatterns = [ + url("", include("pagedown.urls")), url( r"^$", blog.PostList.as_view(template_name="home.html", title=_("Home")), @@ -1086,7 +1088,7 @@ urlpatterns = [ ] ), ), -] +] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) favicon_paths = [ "apple-touch-icon-180x180.png", diff --git a/judge/views/comment.py b/judge/views/comment.py index edba7f3..20d1d5b 100644 --- a/judge/views/comment.py +++ b/judge/views/comment.py @@ -14,13 +14,14 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST from django.views.generic import DetailView, UpdateView +from django.urls import reverse_lazy from reversion import revisions from reversion.models import Version from judge.dblock import LockModel from judge.models import Comment, CommentVote, Notification from judge.utils.views import TitleMixin -from judge.widgets import MathJaxPagedownWidget +from judge.widgets import MathJaxPagedownWidget, HeavyPreviewPageDownWidget from judge.comments import add_mention_notifications, del_mention_notifications __all__ = [ @@ -133,10 +134,11 @@ class CommentEditForm(ModelForm): class Meta: model = Comment fields = ["body"] - if MathJaxPagedownWidget is not None: - widgets = { - "body": MathJaxPagedownWidget(attrs={"id": "id-edit-comment-body"}) - } + widgets = { + "body": HeavyPreviewPageDownWidget( + id="id-edit-comment-body", preview=reverse_lazy("comment_preview") + ), + } class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): diff --git a/judge/widgets/pagedown.py b/judge/widgets/pagedown.py index 3225dc7..27c72e7 100644 --- a/judge/widgets/pagedown.py +++ b/judge/widgets/pagedown.py @@ -3,6 +3,7 @@ from django.forms.utils import flatatt from django.template.loader import get_template from django.utils.encoding import force_text from django.utils.html import conditional_escape +from django.conf import settings from judge.widgets.mixins import CompressorWidgetMixin @@ -34,9 +35,15 @@ else: compress_js = True def __init__(self, *args, **kwargs): - kwargs.setdefault("css", ("pagedown_widget.css",)) super(PagedownWidget, self).__init__(*args, **kwargs) + class Media: + css = { + "all": [ + "pagedown_widget.css", + ] + } + class AdminPagedownWidget(PagedownWidget, admin_widgets.AdminTextareaWidget): class Media: css = { @@ -60,7 +67,8 @@ else: class HeavyPreviewPageDownWidget(PagedownWidget): def __init__(self, *args, **kwargs): - kwargs.setdefault("template", "pagedown.html") + self.template = "pagedown.html" + self.id = kwargs.pop("id", None) self.preview_url = kwargs.pop("preview") self.preview_timeout = kwargs.pop("preview_timeout", None) self.hide_preview_button = kwargs.pop("hide_preview_button", False) @@ -73,16 +81,21 @@ else: if "class" not in final_attrs: final_attrs["class"] = "" final_attrs["class"] += " wmd-input" + if self.id: + final_attrs["id"] = self.id return get_template(self.template).render( self.get_template_context(final_attrs, value) ) def get_template_context(self, attrs, value): return { + "image_upload_enabled": getattr( + settings, "PAGEDOWN_IMAGE_UPLOAD_ENABLED", False + ), "attrs": flatatt(attrs), "body": conditional_escape(force_text(value)), - "id": attrs["id"], - "show_preview": self.show_preview, + "postfix": attrs["id"], + "show_preview": True, "preview_url": self.preview_url, "preview_timeout": self.preview_timeout, "extra_classes": "dmmd-no-button" if self.hide_preview_button else None, diff --git a/requirements.txt b/requirements.txt index dae6d6e..f43e4c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ Django>=2.2,<3 django_compressor django-mptt -django-pagedown<2 +django-pagedown django-registration-redux django-reversion django-reversion-compare diff --git a/resources/pagedown_widget.css b/resources/pagedown_widget.css index 3d2326a..fbdd83e 100644 --- a/resources/pagedown_widget.css +++ b/resources/pagedown_widget.css @@ -59,14 +59,17 @@ .wmd-spacer1 { left: 50px; + display: None; } .wmd-spacer2 { left: 175px; + display: None; } .wmd-spacer3 { left: 300px; + display: None; } .wmd-prompt-background { diff --git a/templates/comments/edit-ajax.html b/templates/comments/edit-ajax.html index c854133..77357e3 100644 --- a/templates/comments/edit-ajax.html +++ b/templates/comments/edit-ajax.html @@ -3,11 +3,8 @@ {% csrf_token %} -
- {{ form.non_field_errors() }} - {{ form.body.errors }} - -
+ {{ form.non_field_errors() }} + {{ form.body.errors }}
{{ form.body }}
diff --git a/templates/comments/media-js.html b/templates/comments/media-js.html index df9f711..8789e95 100644 --- a/templates/comments/media-js.html +++ b/templates/comments/media-js.html @@ -161,15 +161,13 @@ $comments.find('a.edit-link').featherlight({ afterOpen: function () { + register_dmmd_preview($('#id-edit-comment-body-preview')); if ('DjangoPagedown' in window) { var $wmd = $('.featherlight .wmd-input'); if ($wmd.length) { window.DjangoPagedown.createEditor($wmd.get(0)); if ('MathJax' in window) { var preview = $('.featherlight div.wmd-preview')[0]; - window.editors[$wmd.attr('id')].hooks.chain('onPreviewRefresh', function () { - MathJax.Hub.Queue(['Typeset', MathJax.Hub, preview]); - }); MathJax.Hub.Queue(['Typeset', MathJax.Hub, preview]); } } diff --git a/templates/pagedown.html b/templates/pagedown.html index 74a038d..03b1cc3 100644 --- a/templates/pagedown.html +++ b/templates/pagedown.html @@ -1,14 +1,35 @@ -
+
-
- {{ body }} +
+
- {% if show_preview %} -
Update Preview
{% endif %} -
+ {% if image_upload_enabled %} +
+

Insert Image

+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ {% endif %} +
\ No newline at end of file From d1e5aaa3e15a69a5064ae75aade3f4d68f64b64f Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 18 Jul 2022 11:35:51 +0700 Subject: [PATCH 148/959] Always show problem types for volunteer --- judge/views/problem.py | 3 +++ templates/problem/search-form.html | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/judge/views/problem.py b/judge/views/problem.py index a05936f..cf78b9c 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -885,6 +885,9 @@ class ProblemFeed(ProblemList): context["feed_type"] = self.feed_type context["has_show_editorial_option"] = False context["has_have_editorial_option"] = False + + if self.feed_type == "volunteer": + context["problem_types"] = ProblemType.objects.all() return context def get(self, request, *args, **kwargs): diff --git a/templates/problem/search-form.html b/templates/problem/search-form.html index 9f0c910..403c48d 100644 --- a/templates/problem/search-form.html +++ b/templates/problem/search-form.html @@ -14,11 +14,13 @@
{% endif %} -
- - -
+ {% if feed_type != 'volunteer' %} +
+ + +
+ {% endif %} {% if has_show_editorial_option %}
Date: Mon, 18 Jul 2022 12:59:45 +0700 Subject: [PATCH 149/959] Add filter problems by solved --- judge/views/problem.py | 16 ++++++++++++++-- templates/problem/list-base.html | 2 +- templates/problem/search-form.html | 9 ++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/judge/views/problem.py b/judge/views/problem.py index cf78b9c..100220a 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -604,6 +604,9 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView context["full_text"] = 0 if self.in_contest else int(self.full_text) context["show_editorial"] = 0 if self.in_contest else int(self.show_editorial) context["have_editorial"] = 0 if self.in_contest else int(self.have_editorial) + context["show_solved_only"] = ( + 0 if self.in_contest else int(self.show_solved_only) + ) if self.request.profile: context["organizations"] = self.request.profile.organizations.all() @@ -704,6 +707,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView self.full_text = self.GET_with_session(request, "full_text") self.show_editorial = self.GET_with_session(request, "show_editorial") self.have_editorial = self.GET_with_session(request, "have_editorial") + self.show_solved_only = self.GET_with_session(request, "show_solved_only") self.search_query = None self.category = None @@ -751,6 +755,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView "full_text", "show_editorial", "have_editorial", + "show_solved_only", ) for key in to_update: if key in request.GET: @@ -810,6 +815,9 @@ class ProblemFeed(ProblemList): return res def get_queryset(self): + if self.feed_type == "volunteer": + self.hide_solved = 0 + self.show_types = 1 queryset = super(ProblemFeed, self).get_queryset() if self.have_editorial: @@ -823,6 +831,12 @@ class ProblemFeed(ProblemList): voted_problems = user.volunteer_problem_votes.values_list( "problem", flat=True ) + if self.show_solved_only: + queryset = queryset.filter( + id__in=Submission.objects.filter( + user=self.profile, points=F("problem__points") + ).values_list("problem__id", flat=True) + ) return queryset.exclude(id__in=voted_problems).order_by("?") if not settings.ML_OUTPUT_PATH or not user: return queryset.order_by("?") @@ -886,8 +900,6 @@ class ProblemFeed(ProblemList): context["has_show_editorial_option"] = False context["has_have_editorial_option"] = False - if self.feed_type == "volunteer": - context["problem_types"] = ProblemType.objects.all() return context def get(self, request, *args, **kwargs): diff --git a/templates/problem/list-base.html b/templates/problem/list-base.html index 201247e..b5dce9c 100644 --- a/templates/problem/list-base.html +++ b/templates/problem/list-base.html @@ -121,7 +121,7 @@ $('#go').click(clean_submit); - $('input#full_text, input#hide_solved, input#show_types, input#show_editorial, input#have_editorial').click(function () { + $('input#full_text, input#hide_solved, input#show_types, input#show_editorial, input#have_editorial, input#show_solved_only').click(function () { prep_form(); ($('
').attr('action', window.location.pathname + '?' + form_serialize()) .append($('').attr('type', 'hidden').attr('name', 'csrfmiddlewaretoken') diff --git a/templates/problem/search-form.html b/templates/problem/search-form.html index 403c48d..62e62b8 100644 --- a/templates/problem/search-form.html +++ b/templates/problem/search-form.html @@ -7,13 +7,20 @@
- {% if request.user.is_authenticated %} + {% if feed_type != 'volunteer' and request.user.is_authenticated %}
{% endif %} + {% if feed_type == 'volunteer' and request.user.is_authenticated %} +
+ + +
+ {% endif %} {% if feed_type != 'volunteer' %}
Date: Mon, 18 Jul 2022 13:00:56 +0700 Subject: [PATCH 150/959] Add trans --- locale/vi/LC_MESSAGES/django.po | 297 +++++++++++++++++--------------- 1 file changed, 156 insertions(+), 141 deletions(-) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index fca02a8..7ca88dd 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-06-20 22:11+0700\n" +"POT-Creation-Date: 2022-07-18 12:59+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -104,11 +104,11 @@ msgstr "" msgid "Traditional Chinese" msgstr "" -#: dmoj/urls.py:130 +#: dmoj/urls.py:131 msgid "Login" msgstr "Đăng nhập" -#: dmoj/urls.py:206 templates/base.html:213 +#: dmoj/urls.py:208 templates/base.html:213 #: templates/organization/org-left-sidebar.html:2 msgid "Home" msgstr "Trang chủ" @@ -175,7 +175,7 @@ msgstr "" msgid "Access" msgstr "Truy cập" -#: judge/admin/contest.py:203 judge/admin/problem.py:207 +#: judge/admin/contest.py:203 judge/admin/problem.py:208 msgid "Justice" msgstr "Xử phạt" @@ -247,7 +247,7 @@ msgstr "" msgid "diff" msgstr "" -#: judge/admin/organization.py:59 judge/admin/problem.py:264 +#: judge/admin/organization.py:59 judge/admin/problem.py:265 #: judge/admin/profile.py:116 msgid "View on site" msgstr "Xem trên trang" @@ -256,81 +256,81 @@ msgstr "Xem trên trang" msgid "Describe the changes you made (optional)" msgstr "Mô tả các thay đổi (tùy chọn)" -#: judge/admin/problem.py:200 +#: judge/admin/problem.py:201 msgid "Social Media" msgstr "Mạng Xã Hội" -#: judge/admin/problem.py:203 +#: judge/admin/problem.py:204 msgid "Taxonomy" msgstr "" -#: judge/admin/problem.py:204 judge/admin/problem.py:393 -#: templates/contest/contest.html:84 templates/problem/data.html:476 +#: judge/admin/problem.py:205 judge/admin/problem.py:394 +#: templates/contest/contest.html:84 templates/problem/data.html:475 #: templates/problem/list.html:20 templates/problem/list.html:44 #: templates/user/base-users-table.html:10 templates/user/user-about.html:36 #: templates/user/user-about.html:52 templates/user/user-problems.html:58 msgid "Points" msgstr "Điểm" -#: judge/admin/problem.py:205 +#: judge/admin/problem.py:206 msgid "Limits" msgstr "Giới hạn" -#: judge/admin/problem.py:206 judge/admin/submission.py:353 +#: judge/admin/problem.py:207 judge/admin/submission.py:353 #: templates/stats/base.html:14 templates/submission/list.html:342 msgid "Language" msgstr "Ngôn ngữ" -#: judge/admin/problem.py:208 +#: judge/admin/problem.py:209 msgid "History" msgstr "Lịch sử" -#: judge/admin/problem.py:260 templates/problem/list-base.html:98 +#: judge/admin/problem.py:261 templates/problem/list-base.html:106 msgid "Authors" msgstr "Các tác giả" -#: judge/admin/problem.py:281 +#: judge/admin/problem.py:282 #, python-format msgid "%d problem successfully marked as public." msgid_plural "%d problems successfully marked as public." msgstr[0] "%d bài tập đã được đánh dấu công khai." -#: judge/admin/problem.py:288 +#: judge/admin/problem.py:289 msgid "Mark problems as public" msgstr "Công khai bài tập" -#: judge/admin/problem.py:297 +#: judge/admin/problem.py:298 #, python-format msgid "%d problem successfully marked as private." msgid_plural "%d problems successfully marked as private." msgstr[0] "%d bài tập đã được đánh dấu riêng tư." -#: judge/admin/problem.py:304 +#: judge/admin/problem.py:305 msgid "Mark problems as private" msgstr "Đánh dấu các bài tập là riêng tư" -#: judge/admin/problem.py:387 judge/admin/submission.py:316 +#: judge/admin/problem.py:388 judge/admin/submission.py:316 #: templates/problem/list.html:16 templates/problem/list.html:33 msgid "Problem code" msgstr "Mã bài" -#: judge/admin/problem.py:399 judge/admin/submission.py:322 +#: judge/admin/problem.py:400 judge/admin/submission.py:322 msgid "Problem name" msgstr "Tên bài" -#: judge/admin/problem.py:405 +#: judge/admin/problem.py:406 #, fuzzy #| msgid "contest rating" msgid "Voter rating" msgstr "rating kỳ thi" -#: judge/admin/problem.py:411 +#: judge/admin/problem.py:412 #, fuzzy #| msgid "Total points" msgid "Voter point" msgstr "Tổng điểm" -#: judge/admin/problem.py:417 +#: judge/admin/problem.py:418 msgid "Vote" msgstr "" @@ -520,7 +520,7 @@ msgstr "Đăng ký để nhận thông báo về các kỳ thi" msgid "Enable experimental features" msgstr "Sử dụng các tính năng thử nghiệm" -#: judge/forms.py:86 judge/views/organization.py:458 judge/views/register.py:68 +#: judge/forms.py:86 judge/views/organization.py:470 judge/views/register.py:68 #, python-brace-format msgid "You may not be part of more than {count} public groups." msgstr "Bạn không thể tham gia nhiều hơn {count} nhóm công khai." @@ -1085,7 +1085,7 @@ msgstr "lần tham gia kỳ thi" #: judge/models/contest.py:739 judge/models/contest.py:783 #: judge/models/contest.py:845 judge/models/problem.py:576 #: judge/models/problem.py:583 judge/models/problem.py:614 -#: judge/models/problem.py:645 judge/models/problem_data.py:49 +#: judge/models/problem.py:645 judge/models/problem_data.py:50 msgid "problem" msgstr "bài tập" @@ -1647,99 +1647,103 @@ msgstr "Trình chấm tự viết (C++)" msgid "Interactive" msgstr "" -#: judge/models/problem_data.py:54 +#: judge/models/problem_data.py:43 +msgid "Testlib" +msgstr "" + +#: judge/models/problem_data.py:55 msgid "data zip file" msgstr "file zip chứa test" -#: judge/models/problem_data.py:61 +#: judge/models/problem_data.py:62 msgid "generator file" msgstr "file tạo test" -#: judge/models/problem_data.py:68 judge/models/problem_data.py:202 +#: judge/models/problem_data.py:69 judge/models/problem_data.py:203 msgid "output prefix length" msgstr "độ dài hiển thị output" -#: judge/models/problem_data.py:71 judge/models/problem_data.py:205 +#: judge/models/problem_data.py:72 judge/models/problem_data.py:206 msgid "output limit length" msgstr "giới hạn hiển thị output" -#: judge/models/problem_data.py:74 +#: judge/models/problem_data.py:75 msgid "init.yml generation feedback" msgstr "phản hồi của quá trình tạo file init.yml" -#: judge/models/problem_data.py:77 judge/models/problem_data.py:208 +#: judge/models/problem_data.py:78 judge/models/problem_data.py:209 msgid "checker" msgstr "trình chấm" -#: judge/models/problem_data.py:80 judge/models/problem_data.py:211 +#: judge/models/problem_data.py:81 judge/models/problem_data.py:212 msgid "checker arguments" msgstr "các biến trong trình chấm" -#: judge/models/problem_data.py:82 judge/models/problem_data.py:213 +#: judge/models/problem_data.py:83 judge/models/problem_data.py:214 msgid "checker arguments as a JSON object" msgstr "các biến trong trình chấm theo dạng JSON" -#: judge/models/problem_data.py:85 +#: judge/models/problem_data.py:86 msgid "custom checker file" msgstr "file trình chấm" -#: judge/models/problem_data.py:93 +#: judge/models/problem_data.py:94 msgid "custom validator file" msgstr "file trình chấm" -#: judge/models/problem_data.py:101 +#: judge/models/problem_data.py:102 msgid "interactive judge" msgstr "" -#: judge/models/problem_data.py:109 judge/models/problem_data.py:193 +#: judge/models/problem_data.py:110 judge/models/problem_data.py:194 msgid "input file name" msgstr "tên file input" -#: judge/models/problem_data.py:112 +#: judge/models/problem_data.py:113 msgid "Leave empty for stdin" msgstr "Để trống nếu nhập từ bàn phím" -#: judge/models/problem_data.py:115 judge/models/problem_data.py:196 +#: judge/models/problem_data.py:116 judge/models/problem_data.py:197 msgid "output file name" msgstr "tên file output" -#: judge/models/problem_data.py:118 +#: judge/models/problem_data.py:119 msgid "Leave empty for stdout" msgstr "Để trống nếu xuất ra màn hình" -#: judge/models/problem_data.py:177 +#: judge/models/problem_data.py:178 msgid "problem data set" msgstr "tập hợp dữ liệu bài" -#: judge/models/problem_data.py:181 +#: judge/models/problem_data.py:182 msgid "case position" msgstr "vị trí test" -#: judge/models/problem_data.py:184 +#: judge/models/problem_data.py:185 msgid "case type" msgstr "loại test" -#: judge/models/problem_data.py:186 +#: judge/models/problem_data.py:187 msgid "Normal case" msgstr "Test bình thường" -#: judge/models/problem_data.py:187 +#: judge/models/problem_data.py:188 msgid "Batch start" msgstr "Bắt đầu nhóm" -#: judge/models/problem_data.py:188 +#: judge/models/problem_data.py:189 msgid "Batch end" msgstr "Kết thúc nhóm" -#: judge/models/problem_data.py:198 +#: judge/models/problem_data.py:199 msgid "generator arguments" msgstr "biến trong file sinh test" -#: judge/models/problem_data.py:199 +#: judge/models/problem_data.py:200 msgid "point value" msgstr "điểm" -#: judge/models/problem_data.py:200 +#: judge/models/problem_data.py:201 msgid "case is pretest?" msgstr "test là pretest?" @@ -2389,42 +2393,49 @@ msgid "Empty batches not allowed." msgstr "Nhóm test trống là không hợp lệ." #: judge/utils/problem_data.py:80 judge/utils/problem_data.py:88 +#: judge/utils/problem_data.py:103 msgid "How did you corrupt the custom checker path?" msgstr "How did you corrupt the custom checker path?" -#: judge/utils/problem_data.py:115 +#: judge/utils/problem_data.py:130 #, python-format msgid "Points must be defined for non-batch case #%d." msgstr "Ô điểm số cho test #%d phải được điền." -#: judge/utils/problem_data.py:122 +#: judge/utils/problem_data.py:137 #, python-format msgid "Input file for case %d does not exist: %s" msgstr "File input cho test %d không tồn tại: %s" -#: judge/utils/problem_data.py:127 +#: judge/utils/problem_data.py:142 #, python-format msgid "Output file for case %d does not exist: %s" msgstr "File output cho test %d không tồn tại: %s" -#: judge/utils/problem_data.py:154 +#: judge/utils/problem_data.py:169 #, python-format msgid "Batch start case #%d requires points." msgstr "Nhóm test #%d cần được điền điểm số." -#: judge/utils/problem_data.py:177 +#: judge/utils/problem_data.py:192 #, python-format msgid "Attempt to end batch outside of one in case #%d" msgstr "Nhóm test #%d kết thúc không hợp lệ" -#: judge/utils/problem_data.py:196 +#: judge/utils/problem_data.py:211 msgid "How did you corrupt the zip path?" msgstr "" -#: judge/utils/problem_data.py:202 +#: judge/utils/problem_data.py:217 msgid "How did you corrupt the generator path?" msgstr "" +#: judge/utils/problem_data.py:236 +#, fuzzy +#| msgid "How did you corrupt the custom checker path?" +msgid "How did you corrupt the interactor path?" +msgstr "How did you corrupt the custom checker path?" + #: judge/utils/problems.py:117 msgid "Wrong" msgstr "Sai" @@ -2501,24 +2512,24 @@ msgstr "Báo cáo" msgid "Comment feed" msgstr "Bình luận" -#: judge/views/comment.py:39 +#: judge/views/comment.py:40 msgid "Messing around, are we?" msgstr "Messing around, are we?" -#: judge/views/comment.py:55 +#: judge/views/comment.py:56 msgid "You must solve at least one problem before you can vote." msgstr "Bạn phải giải ít nhất 1 bài trước khi được vote." -#: judge/views/comment.py:86 +#: judge/views/comment.py:87 msgid "You already voted." msgstr "Bạn đã vote." -#: judge/views/comment.py:153 judge/views/organization.py:758 -#: judge/views/organization.py:850 +#: judge/views/comment.py:155 judge/views/organization.py:770 +#: judge/views/organization.py:880 msgid "Edited from site" msgstr "Chỉnh sửa từ web" -#: judge/views/comment.py:174 +#: judge/views/comment.py:176 msgid "Editing comment" msgstr "Chỉnh sửa bình luận" @@ -2691,90 +2702,90 @@ msgstr "Runtimes" msgid "Notifications (%d unseen)" msgstr "Thông báo (%d chưa xem)" -#: judge/views/organization.py:134 judge/views/organization.py:140 +#: judge/views/organization.py:135 judge/views/organization.py:141 msgid "No such organization" msgstr "Không có tổ chức như vậy" -#: judge/views/organization.py:135 +#: judge/views/organization.py:136 #, python-format msgid "Could not find an organization with the key \"%s\"." msgstr "Không tìm thấy tổ chức với mã \"%s\"." -#: judge/views/organization.py:141 +#: judge/views/organization.py:142 msgid "Could not find such organization." msgstr "" -#: judge/views/organization.py:157 +#: judge/views/organization.py:158 msgid "Can't edit organization" msgstr "Không thể chỉnh sửa tổ chức" -#: judge/views/organization.py:158 +#: judge/views/organization.py:159 msgid "You are not allowed to edit this organization." msgstr "Bạn không được phép chỉnh sửa tổ chức này." -#: judge/views/organization.py:170 +#: judge/views/organization.py:171 judge/views/organization.py:316 #, fuzzy #| msgid "Can't edit organization" msgid "Can't access organization" msgstr "Không thể chỉnh sửa tổ chức" -#: judge/views/organization.py:171 +#: judge/views/organization.py:172 judge/views/organization.py:317 msgid "You are not allowed to access this organization." msgstr "Bạn không được phép chỉnh sửa tổ chức này." -#: judge/views/organization.py:229 judge/views/register.py:49 -#: templates/contest/list.html:109 templates/problem/list-base.html:96 +#: judge/views/organization.py:230 judge/views/register.py:49 +#: templates/contest/list.html:109 templates/problem/list-base.html:104 #: templates/user/user-left-sidebar.html:4 templates/user/user-list-tabs.html:6 msgid "Groups" msgstr "Nhóm" -#: judge/views/organization.py:311 +#: judge/views/organization.py:323 #, python-format msgid "%s Members" msgstr "%s Thành viên" -#: judge/views/organization.py:414 +#: judge/views/organization.py:426 #, python-brace-format msgid "All submissions in {0}" msgstr "Bài nộp trong {0}" -#: judge/views/organization.py:444 judge/views/organization.py:450 -#: judge/views/organization.py:457 +#: judge/views/organization.py:456 judge/views/organization.py:462 +#: judge/views/organization.py:469 msgid "Joining group" msgstr "Tham gia nhóm" -#: judge/views/organization.py:445 +#: judge/views/organization.py:457 msgid "You are already in the group." msgstr "Bạn đã ở trong nhóm." -#: judge/views/organization.py:450 +#: judge/views/organization.py:462 msgid "This group is not open." msgstr "Nhóm này là nhóm kín." -#: judge/views/organization.py:473 +#: judge/views/organization.py:485 msgid "Leaving group" msgstr "Rời nhóm" -#: judge/views/organization.py:474 +#: judge/views/organization.py:486 #, python-format msgid "You are not in \"%s\"." msgstr "Bạn không ở trong \"%s\"." -#: judge/views/organization.py:499 +#: judge/views/organization.py:511 #, python-format msgid "Request to join %s" msgstr "Đăng ký tham gia %s" -#: judge/views/organization.py:530 +#: judge/views/organization.py:542 msgid "Join request detail" msgstr "Chi tiết đơn đăng ký" -#: judge/views/organization.py:578 +#: judge/views/organization.py:590 #, python-format msgid "Managing join requests for %s" msgstr "Quản lý đơn đăng ký cho %s" -#: judge/views/organization.py:618 +#: judge/views/organization.py:630 #, python-format msgid "" "Your organization can only receive %d more members. You cannot approve %d " @@ -2783,71 +2794,71 @@ msgstr "" "Tổ chức chỉ có thể chứa %d thành viên. Bạn không thể chấp thuận nhiều hơn %d " "người." -#: judge/views/organization.py:636 +#: judge/views/organization.py:648 #, python-format msgid "Approved %d user." msgid_plural "Approved %d users." msgstr[0] "Đã chấp thuận %d người." -#: judge/views/organization.py:639 +#: judge/views/organization.py:651 #, python-format msgid "Rejected %d user." msgid_plural "Rejected %d users." msgstr[0] "Đã từ chối %d người." -#: judge/views/organization.py:679 +#: judge/views/organization.py:691 #, python-format msgid "Add member for %s" msgstr "Thêm thành viên cho %s" -#: judge/views/organization.py:691 +#: judge/views/organization.py:703 #, fuzzy #| msgid "Edited from site" msgid "Added members from site" msgstr "Chỉnh sửa từ web" -#: judge/views/organization.py:711 judge/views/organization.py:719 +#: judge/views/organization.py:723 judge/views/organization.py:731 msgid "Can't kick user" msgstr "Không thể đuổi" -#: judge/views/organization.py:712 +#: judge/views/organization.py:724 msgid "The user you are trying to kick does not exist!" msgstr "" -#: judge/views/organization.py:720 +#: judge/views/organization.py:732 #, python-format msgid "The user you are trying to kick is not in organization: %s." msgstr "" -#: judge/views/organization.py:741 +#: judge/views/organization.py:753 #, fuzzy, python-format #| msgid "Editing %s" msgid "Edit %s" msgstr "Đang chỉnh sửa %s" -#: judge/views/organization.py:780 +#: judge/views/organization.py:792 #, python-format msgid "Add blog for %s" msgstr "Thêm bài đăng cho %s" -#: judge/views/organization.py:791 +#: judge/views/organization.py:803 msgid "Added from site" msgstr "Thêm từ web" -#: judge/views/organization.py:825 +#: judge/views/organization.py:855 msgid "Permission denied" msgstr "Truy cập bị từ chối" -#: judge/views/organization.py:826 +#: judge/views/organization.py:856 msgid "Not allowed to edit this blog" msgstr "Bạn không được phép chỉnh sửa bài đăng này." -#: judge/views/organization.py:845 +#: judge/views/organization.py:875 #, python-format msgid "Edit blog %s" msgstr "Chỉnh sửa %s" -#: judge/views/organization.py:875 +#: judge/views/organization.py:923 #, python-format msgid "Pending blogs in %s" msgstr "Bài đang đợi duyệt trong %s" @@ -2878,34 +2889,34 @@ msgstr "Hướng dẫn cho {0}" msgid "Problems" msgstr "Bài tập" -#: judge/views/problem.py:772 +#: judge/views/problem.py:777 msgid "Problem feed" msgstr "Bài tập" -#: judge/views/problem.py:996 +#: judge/views/problem.py:1011 msgid "Banned from submitting" msgstr "Bị cấm nộp bài" -#: judge/views/problem.py:998 +#: judge/views/problem.py:1013 msgid "" "You have been declared persona non grata for this problem. You are " "permanently barred from submitting this problem." msgstr "Bạn đã bị cấm nộp bài này." -#: judge/views/problem.py:1021 +#: judge/views/problem.py:1036 msgid "Too many submissions" msgstr "Quá nhiều lần nộp" -#: judge/views/problem.py:1023 +#: judge/views/problem.py:1038 msgid "You have exceeded the submission limit for this problem." msgstr "Bạn đã vượt quá số lần nộp cho bài này." -#: judge/views/problem.py:1102 judge/views/problem.py:1107 +#: judge/views/problem.py:1117 judge/views/problem.py:1122 #, python-format msgid "Submit to %(problem)s" msgstr "Nộp bài cho %(problem)s" -#: judge/views/problem.py:1130 +#: judge/views/problem.py:1145 msgid "Clone Problem" msgstr "Nhân bản bài tập" @@ -3405,7 +3416,7 @@ msgstr "Chỉnh sửa thông tin" #: templates/admin/judge/submission/change_form.html:14 #: templates/admin/judge/submission/change_form.html:17 -#: templates/submission/source.html:38 templates/submission/status.html:67 +#: templates/submission/source.html:39 templates/submission/status.html:68 msgid "Rejudge" msgstr "Chấm lại" @@ -3426,8 +3437,8 @@ msgstr "Xin chào, %(username)s." #: templates/comments/list.html:89 templates/contest/contest-list-tabs.html:24 #: templates/contest/list.html:130 templates/contest/ranking-table.html:49 #: templates/internal/base.html:59 -#: templates/organization/org-left-sidebar.html:10 -#: templates/problem/left-sidebar.html:5 templates/problem/list-base.html:249 +#: templates/organization/org-left-sidebar.html:12 +#: templates/problem/left-sidebar.html:5 templates/problem/list-base.html:257 #: templates/problem/problem-list-tabs.html:6 #: templates/submission/info-base.html:12 templates/submission/list.html:386 #: templates/submission/submission-list-tabs.html:15 @@ -3747,7 +3758,7 @@ msgid "Next" msgstr "Tiếp" #: templates/contest/contest-list-tabs.html:21 templates/contest/list.html:128 -#: templates/problem/left-sidebar.html:4 templates/problem/list-base.html:248 +#: templates/problem/left-sidebar.html:4 templates/problem/list-base.html:256 #: templates/problem/problem-list-tabs.html:5 msgid "List" msgstr "Danh sách" @@ -4252,7 +4263,7 @@ msgid "Blog" msgstr "" #: templates/organization/blog/pending.html:11 -#: templates/problem/search-form.html:49 +#: templates/problem/search-form.html:58 msgid "Author" msgstr "Tác giả" @@ -4302,7 +4313,7 @@ msgstr "Nhóm kín" msgid "Submissions" msgstr "Bài nộp" -#: templates/organization/org-left-sidebar.html:8 +#: templates/organization/org-left-sidebar.html:9 msgid "Members" msgstr "Thành viên" @@ -4366,7 +4377,7 @@ msgid "There are no requests to approve." msgstr "Không có đơn đăng ký." #: templates/organization/requests/pending.html:17 -#: templates/problem/data.html:479 +#: templates/problem/data.html:478 msgid "Delete?" msgstr "Xóa?" @@ -4407,48 +4418,48 @@ msgstr "Đuổi" msgid "Enter a new code for the cloned problem:" msgstr "Nhập mã bài mới cho bài tập được nhân bản:" -#: templates/problem/data.html:149 +#: templates/problem/data.html:148 msgid "Instruction" msgstr "Hướng dẫn" -#: templates/problem/data.html:437 +#: templates/problem/data.html:436 msgid "View YAML" msgstr "Xem YAML" -#: templates/problem/data.html:454 +#: templates/problem/data.html:453 msgid "Autofill testcases" msgstr "Tự động điền test" -#: templates/problem/data.html:457 templates/problem/problem.html:302 +#: templates/problem/data.html:456 templates/problem/problem.html:302 msgid "Problem type" msgid_plural "Problem types" msgstr[0] "Dạng bài" -#: templates/problem/data.html:464 +#: templates/problem/data.html:463 msgid "Fill testcases" msgstr "Điền test" -#: templates/problem/data.html:468 templates/problem/data.html:519 +#: templates/problem/data.html:467 templates/problem/data.html:518 msgid "Apply!" msgstr "Lưu!" -#: templates/problem/data.html:473 +#: templates/problem/data.html:472 msgid "Type" msgstr "Kiểu" -#: templates/problem/data.html:474 +#: templates/problem/data.html:473 msgid "Input file" msgstr "File Input" -#: templates/problem/data.html:475 +#: templates/problem/data.html:474 msgid "Output file" msgstr "File Output" -#: templates/problem/data.html:477 +#: templates/problem/data.html:476 msgid "Pretest?" msgstr "Pretest?" -#: templates/problem/data.html:520 +#: templates/problem/data.html:519 msgid "Add new case" msgstr "Thêm test mới" @@ -4495,7 +4506,7 @@ msgstr "Độ khó kiến thức" msgid "Thinking point" msgstr "Độ khó nghĩ" -#: templates/problem/feed.html:90 templates/problem/search-form.html:75 +#: templates/problem/feed.html:90 templates/problem/search-form.html:84 msgid "Problem types" msgstr "Dạng bài" @@ -4503,27 +4514,27 @@ msgstr "Dạng bài" msgid "Any additional note here" msgstr "Lưu ý thêm cho admin" -#: templates/problem/left-sidebar.html:3 templates/problem/list-base.html:247 +#: templates/problem/left-sidebar.html:3 templates/problem/list-base.html:255 msgid "Feed" msgstr "Gợi ý" -#: templates/problem/list-base.html:94 +#: templates/problem/list-base.html:102 msgid "Filter by type..." msgstr "Lọc theo dạng..." -#: templates/problem/list-base.html:153 templates/problem/list-base.html:179 +#: templates/problem/list-base.html:161 templates/problem/list-base.html:187 msgid "Add types..." msgstr "Thêm dạng" -#: templates/problem/list-base.html:195 +#: templates/problem/list-base.html:203 msgid "Fail to vote!" msgstr "Hệ thống lỗi!" -#: templates/problem/list-base.html:198 +#: templates/problem/list-base.html:206 msgid "Successful vote! Thank you!" msgstr "Đã gửi thành công! Cảm ơn bạn!" -#: templates/problem/list.html:36 templates/problem/search-form.html:59 +#: templates/problem/list.html:36 templates/problem/search-form.html:68 #: templates/user/user-problems.html:57 msgid "Category" msgstr "Nhóm bài" @@ -4778,38 +4789,42 @@ msgstr "Tìm bài tập..." msgid "Hide solved problems" msgstr "Ẩn các bài đã giải" -#: templates/problem/search-form.html:20 +#: templates/problem/search-form.html:21 +msgid "Show solved problems" +msgstr "Hiện các bài đã giải" + +#: templates/problem/search-form.html:28 msgid "Show problem types" msgstr "Hiển thị dạng bài" -#: templates/problem/search-form.html:26 +#: templates/problem/search-form.html:35 msgid "Show editorial" msgstr "Hiển thị hướng dẫn" -#: templates/problem/search-form.html:33 +#: templates/problem/search-form.html:42 msgid "Have editorial" msgstr "Có hướng dẫn" -#: templates/problem/search-form.html:38 +#: templates/problem/search-form.html:47 msgid "Group" msgstr "" -#: templates/problem/search-form.html:62 templates/problem/search-form.html:64 +#: templates/problem/search-form.html:71 templates/problem/search-form.html:73 #: templates/submission/list.html:375 #: templates/submission/submission-list-tabs.html:4 msgid "All" msgstr "Tất cả" -#: templates/problem/search-form.html:86 +#: templates/problem/search-form.html:95 msgid "Point range" msgstr "Mốc điểm" -#: templates/problem/search-form.html:92 templates/submission/list.html:351 +#: templates/problem/search-form.html:101 templates/submission/list.html:351 #: templates/ticket/list.html:248 msgid "Go" msgstr "Lọc" -#: templates/problem/search-form.html:93 +#: templates/problem/search-form.html:102 msgid "Random" msgstr "Ngẫu nhiên" @@ -5151,15 +5166,15 @@ msgstr "chấm lại" msgid "admin" msgstr "admin" -#: templates/submission/source.html:29 +#: templates/submission/source.html:30 msgid "View status" msgstr "Xem kết quả chấm" -#: templates/submission/source.html:30 +#: templates/submission/source.html:31 msgid "View raw source" msgstr "Xem mã nguồn" -#: templates/submission/source.html:32 templates/submission/status.html:61 +#: templates/submission/source.html:33 templates/submission/status.html:62 msgid "Resubmit" msgstr "Nộp lại" @@ -5240,11 +5255,11 @@ msgstr "AC pretest không đồng nghĩa AC cả bài nhé :))" msgid "Submission aborted!" msgstr "Đã hủy chấm bài nộp!" -#: templates/submission/status.html:59 +#: templates/submission/status.html:60 msgid "View source" msgstr "Xem mã nguồn" -#: templates/submission/status.html:88 +#: templates/submission/status.html:89 msgid "Abort" msgstr "Hủy chấm" From bd703af53edfd450a94985b79dd7f76f2278d6f6 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 18 Jul 2022 13:17:00 +0700 Subject: [PATCH 151/959] Add view statment src --- templates/problem/feed.html | 2 ++ templates/problem/list-base.html | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/templates/problem/feed.html b/templates/problem/feed.html index d30f7ac..0fd482c 100644 --- a/templates/problem/feed.html +++ b/templates/problem/feed.html @@ -52,6 +52,8 @@ {{ problem.description|markdown("problem", MATH_ENGINE)|reference|str|safe }} {% endcache %} {% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %} + {{ _('View source') }} +

{{_('Volunteer form')}}


diff --git a/templates/problem/list-base.html b/templates/problem/list-base.html index b5dce9c..d1b6a53 100644 --- a/templates/problem/list-base.html +++ b/templates/problem/list-base.html @@ -155,6 +155,10 @@ }); } {% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %} + $(".view-statement-src").on('click', function(e) { + e.preventDefault(); + $(this).siblings('.statement-src').toggle(); + }); $(".edit-btn").on('click', function() { var pid = $(this).attr('pid'); $('#volunteer-types-' + pid).css({'width': '100%'}); From dc8cbc697638fef35734891eac30a6676d4adcea Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 20 Jul 2022 15:06:15 +0700 Subject: [PATCH 152/959] Auto add editing user to curators --- judge/admin/problem.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 0cab70a..53db1d8 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -346,6 +346,13 @@ class ProblemAdmin(CompareVersionAdmin): ): self._rescore(request, obj.id) + def save_related(self, request, form, formsets, change): + super().save_related(request, form, formsets, change) + # Only rescored if we did not already do so in `save_model` + obj = form.instance + obj.curators.add(request.profile) + obj.save() + def construct_change_message(self, request, form, *args, **kwargs): if form.cleaned_data.get("change_message"): return form.cleaned_data["change_message"] From 3ed4fc7a0ea285b367c68b127a6f8defbc957d53 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 27 Jul 2022 20:18:31 +0700 Subject: [PATCH 153/959] Change ~ to $ for latex --- judge/jinja2/markdown/math.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/judge/jinja2/markdown/math.py b/judge/jinja2/markdown/math.py index 1b5f4ce..8063ab6 100644 --- a/judge/jinja2/markdown/math.py +++ b/judge/jinja2/markdown/math.py @@ -11,7 +11,7 @@ mistune._pre_tags.append("latex") class MathInlineGrammar(mistune.InlineGrammar): block_math = re.compile(r"^\$\$(.*?)\$\$|^\\\[(.*?)\\\]", re.DOTALL) - math = re.compile(r"^~(.*?)~|^\\\((.*?)\\\)", re.DOTALL) + math = re.compile(r"^~(.*?)~|\$(.*?)\$|^\\\((.*?)\\\)", re.DOTALL) text = re.compile(r"^[\s\S]+?(?=[\\ Date: Thu, 28 Jul 2022 04:22:01 +0700 Subject: [PATCH 154/959] Fix internal page url --- judge/views/internal.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/judge/views/internal.py b/judge/views/internal.py index 07fa75b..d6ce1da 100644 --- a/judge/views/internal.py +++ b/judge/views/internal.py @@ -39,6 +39,9 @@ class InternalProblem(ListView): context = super(InternalProblem, self).get_context_data(**kwargs) context["page_type"] = "problem" context["title"] = self.title + context["page_prefix"] = self.request.path + "?page=" + context["first_page_href"] = self.request.path + return context def get(self, request, *args, **kwargs): From f969dbb290295f207093ef6e0d77d4a0a091a284 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 29 Jul 2022 14:55:12 +0700 Subject: [PATCH 155/959] Fix toolbar UI --- resources/pagedown_widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/pagedown_widget.css b/resources/pagedown_widget.css index fbdd83e..0dff7ee 100644 --- a/resources/pagedown_widget.css +++ b/resources/pagedown_widget.css @@ -6,7 +6,7 @@ .wmd-button-bar { width: 100%; - background-color: Silver; + background-color: white; } .wmd-input { From 89b74e8ef8e836ca7d055c3f2f2a3f045d5ae80a Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 29 Jul 2022 15:00:44 +0700 Subject: [PATCH 156/959] Remove submodule --- .gitmodules | 8 - resources/libs | 1 - resources/libs/README.md | 2 + resources/libs/chart.js/Chart.js | 14680 ++++++++++++++++ resources/libs/clipboard/clipboard.js | 742 + resources/libs/clipboard/tooltip.css | 163 + resources/libs/clipboard/tooltip.js | 18 + .../libs/featherlight/featherlight.min.css | 8 + .../libs/featherlight/featherlight.min.js | 8 + resources/libs/fontawesome/font-awesome.css | 1801 ++ .../libs/fontawesome/fontawesome-webfont.eot | Bin 0 -> 60767 bytes .../libs/fontawesome/fontawesome-webfont.svg | 565 + .../libs/fontawesome/fontawesome-webfont.ttf | Bin 0 -> 122092 bytes .../libs/fontawesome/fontawesome-webfont.woff | Bin 0 -> 71508 bytes .../fontawesome/fontawesome-webfont.woff2 | Bin 0 -> 56780 bytes resources/libs/jquery-3.4.1.min.js | 2 + resources/libs/jquery-cookie.js | 117 + resources/libs/jquery-sortable.js | 692 + resources/libs/jquery-taphold.js | 117 + resources/libs/jquery.unveil.js | 56 + .../libs/latinmodernmath/latinmodern-math.eot | Bin 0 -> 1019564 bytes .../libs/latinmodernmath/latinmodern-math.ttf | Bin 0 -> 847764 bytes .../latinmodernmath/latinmodern-math.woff | Bin 0 -> 471260 bytes .../latinmodernmath/latinmodern-math.woff2 | Bin 0 -> 295936 bytes resources/libs/moment.js | 5610 ++++++ resources/libs/nouislider.min.css | 1 + resources/libs/nouislider.min.js | 3 + resources/libs/raven.min.js | 3 + resources/libs/select2/select2.css | 482 + resources/libs/select2/select2.js | 3 + resources/libs/tablesorter.js | 2713 +++ resources/libs/timezone-map/timezone-map.css | 44 + .../libs/timezone-map/timezone-picker.js | 91 + .../libs/timezone-map/timezone-picker.json | 1 + resources/pagedown | 1 - resources/pagedown/LICENSE.txt | 32 + resources/pagedown/Markdown.Converter.js | 1332 ++ resources/pagedown/Markdown.Editor.js | 2333 +++ resources/pagedown/Markdown.Sanitizer.js | 108 + resources/pagedown/README.md | 21 + resources/pagedown/demo/browser/demo.css | 119 + resources/pagedown/demo/browser/demo.html | 83 + resources/pagedown/demo/node/demo.js | 44 + resources/pagedown/local/Markdown.local.fr.js | 43 + resources/pagedown/node-pagedown.js | 2 + resources/pagedown/package.json | 12 + resources/pagedown/resources/wmd-buttons.pdn | Bin 0 -> 27769 bytes resources/pagedown/resources/wmd-buttons.png | Bin 0 -> 8809 bytes resources/pagedown/resources/wmd-buttons.psd | Bin 0 -> 61236 bytes resources/pagedown/wmd-buttons.png | Bin 0 -> 9388 bytes 50 files changed, 32051 insertions(+), 10 deletions(-) delete mode 100644 .gitmodules delete mode 160000 resources/libs create mode 100644 resources/libs/README.md create mode 100644 resources/libs/chart.js/Chart.js create mode 100644 resources/libs/clipboard/clipboard.js create mode 100644 resources/libs/clipboard/tooltip.css create mode 100644 resources/libs/clipboard/tooltip.js create mode 100644 resources/libs/featherlight/featherlight.min.css create mode 100644 resources/libs/featherlight/featherlight.min.js create mode 100644 resources/libs/fontawesome/font-awesome.css create mode 100644 resources/libs/fontawesome/fontawesome-webfont.eot create mode 100644 resources/libs/fontawesome/fontawesome-webfont.svg create mode 100644 resources/libs/fontawesome/fontawesome-webfont.ttf create mode 100644 resources/libs/fontawesome/fontawesome-webfont.woff create mode 100644 resources/libs/fontawesome/fontawesome-webfont.woff2 create mode 100644 resources/libs/jquery-3.4.1.min.js create mode 100644 resources/libs/jquery-cookie.js create mode 100644 resources/libs/jquery-sortable.js create mode 100644 resources/libs/jquery-taphold.js create mode 100644 resources/libs/jquery.unveil.js create mode 100644 resources/libs/latinmodernmath/latinmodern-math.eot create mode 100644 resources/libs/latinmodernmath/latinmodern-math.ttf create mode 100644 resources/libs/latinmodernmath/latinmodern-math.woff create mode 100644 resources/libs/latinmodernmath/latinmodern-math.woff2 create mode 100644 resources/libs/moment.js create mode 100644 resources/libs/nouislider.min.css create mode 100644 resources/libs/nouislider.min.js create mode 100644 resources/libs/raven.min.js create mode 100644 resources/libs/select2/select2.css create mode 100644 resources/libs/select2/select2.js create mode 100644 resources/libs/tablesorter.js create mode 100644 resources/libs/timezone-map/timezone-map.css create mode 100644 resources/libs/timezone-map/timezone-picker.js create mode 100644 resources/libs/timezone-map/timezone-picker.json delete mode 160000 resources/pagedown create mode 100644 resources/pagedown/LICENSE.txt create mode 100644 resources/pagedown/Markdown.Converter.js create mode 100644 resources/pagedown/Markdown.Editor.js create mode 100644 resources/pagedown/Markdown.Sanitizer.js create mode 100644 resources/pagedown/README.md create mode 100644 resources/pagedown/demo/browser/demo.css create mode 100644 resources/pagedown/demo/browser/demo.html create mode 100644 resources/pagedown/demo/node/demo.js create mode 100644 resources/pagedown/local/Markdown.local.fr.js create mode 100644 resources/pagedown/node-pagedown.js create mode 100644 resources/pagedown/package.json create mode 100644 resources/pagedown/resources/wmd-buttons.pdn create mode 100644 resources/pagedown/resources/wmd-buttons.png create mode 100644 resources/pagedown/resources/wmd-buttons.psd create mode 100644 resources/pagedown/wmd-buttons.png diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0bea662..0000000 --- a/.gitmodules +++ /dev/null @@ -1,8 +0,0 @@ -[submodule "resources/pagedown"] - path = resources/pagedown - url = https://github.com/DMOJ/dmoj-pagedown.git - branch = master -[submodule "resources/libs"] - path = resources/libs - url = https://github.com/DMOJ/site-assets.git - branch = master diff --git a/resources/libs b/resources/libs deleted file mode 160000 index 2681309..0000000 --- a/resources/libs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2681309a5ddd3482e699761488c75d9a62a626ec diff --git a/resources/libs/README.md b/resources/libs/README.md new file mode 100644 index 0000000..8f4e171 --- /dev/null +++ b/resources/libs/README.md @@ -0,0 +1,2 @@ +# site-assets +DMOJ site assets. diff --git a/resources/libs/chart.js/Chart.js b/resources/libs/chart.js/Chart.js new file mode 100644 index 0000000..4c50e09 --- /dev/null +++ b/resources/libs/chart.js/Chart.js @@ -0,0 +1,14680 @@ +/*! + * Chart.js v2.8.0 + * https://www.chartjs.org + * (c) 2019 Chart.js Contributors + * Released under the MIT License + */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(function() { try { return require('moment'); } catch(e) { } }()) : +typeof define === 'function' && define.amd ? define(['require'], function(require) { return factory(function() { try { return require('moment'); } catch(e) { } }()); }) : +(global.Chart = factory(global.moment)); +}(this, (function (moment) { 'use strict'; + +moment = moment && moment.hasOwnProperty('default') ? moment['default'] : moment; + +/* MIT license */ + +var conversions = { + rgb2hsl: rgb2hsl, + rgb2hsv: rgb2hsv, + rgb2hwb: rgb2hwb, + rgb2cmyk: rgb2cmyk, + rgb2keyword: rgb2keyword, + rgb2xyz: rgb2xyz, + rgb2lab: rgb2lab, + rgb2lch: rgb2lch, + + hsl2rgb: hsl2rgb, + hsl2hsv: hsl2hsv, + hsl2hwb: hsl2hwb, + hsl2cmyk: hsl2cmyk, + hsl2keyword: hsl2keyword, + + hsv2rgb: hsv2rgb, + hsv2hsl: hsv2hsl, + hsv2hwb: hsv2hwb, + hsv2cmyk: hsv2cmyk, + hsv2keyword: hsv2keyword, + + hwb2rgb: hwb2rgb, + hwb2hsl: hwb2hsl, + hwb2hsv: hwb2hsv, + hwb2cmyk: hwb2cmyk, + hwb2keyword: hwb2keyword, + + cmyk2rgb: cmyk2rgb, + cmyk2hsl: cmyk2hsl, + cmyk2hsv: cmyk2hsv, + cmyk2hwb: cmyk2hwb, + cmyk2keyword: cmyk2keyword, + + keyword2rgb: keyword2rgb, + keyword2hsl: keyword2hsl, + keyword2hsv: keyword2hsv, + keyword2hwb: keyword2hwb, + keyword2cmyk: keyword2cmyk, + keyword2lab: keyword2lab, + keyword2xyz: keyword2xyz, + + xyz2rgb: xyz2rgb, + xyz2lab: xyz2lab, + xyz2lch: xyz2lch, + + lab2xyz: lab2xyz, + lab2rgb: lab2rgb, + lab2lch: lab2lch, + + lch2lab: lch2lab, + lch2xyz: lch2xyz, + lch2rgb: lch2rgb +}; + + +function rgb2hsl(rgb) { + var r = rgb[0]/255, + g = rgb[1]/255, + b = rgb[2]/255, + min = Math.min(r, g, b), + max = Math.max(r, g, b), + delta = max - min, + h, s, l; + + if (max == min) + h = 0; + else if (r == max) + h = (g - b) / delta; + else if (g == max) + h = 2 + (b - r) / delta; + else if (b == max) + h = 4 + (r - g)/ delta; + + h = Math.min(h * 60, 360); + + if (h < 0) + h += 360; + + l = (min + max) / 2; + + if (max == min) + s = 0; + else if (l <= 0.5) + s = delta / (max + min); + else + s = delta / (2 - max - min); + + return [h, s * 100, l * 100]; +} + +function rgb2hsv(rgb) { + var r = rgb[0], + g = rgb[1], + b = rgb[2], + min = Math.min(r, g, b), + max = Math.max(r, g, b), + delta = max - min, + h, s, v; + + if (max == 0) + s = 0; + else + s = (delta/max * 1000)/10; + + if (max == min) + h = 0; + else if (r == max) + h = (g - b) / delta; + else if (g == max) + h = 2 + (b - r) / delta; + else if (b == max) + h = 4 + (r - g) / delta; + + h = Math.min(h * 60, 360); + + if (h < 0) + h += 360; + + v = ((max / 255) * 1000) / 10; + + return [h, s, v]; +} + +function rgb2hwb(rgb) { + var r = rgb[0], + g = rgb[1], + b = rgb[2], + h = rgb2hsl(rgb)[0], + w = 1/255 * Math.min(r, Math.min(g, b)), + b = 1 - 1/255 * Math.max(r, Math.max(g, b)); + + return [h, w * 100, b * 100]; +} + +function rgb2cmyk(rgb) { + var r = rgb[0] / 255, + g = rgb[1] / 255, + b = rgb[2] / 255, + c, m, y, k; + + k = Math.min(1 - r, 1 - g, 1 - b); + c = (1 - r - k) / (1 - k) || 0; + m = (1 - g - k) / (1 - k) || 0; + y = (1 - b - k) / (1 - k) || 0; + return [c * 100, m * 100, y * 100, k * 100]; +} + +function rgb2keyword(rgb) { + return reverseKeywords[JSON.stringify(rgb)]; +} + +function rgb2xyz(rgb) { + var r = rgb[0] / 255, + g = rgb[1] / 255, + b = rgb[2] / 255; + + // assume sRGB + r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92); + g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92); + b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92); + + var x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805); + var y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722); + var z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505); + + return [x * 100, y *100, z * 100]; +} + +function rgb2lab(rgb) { + var xyz = rgb2xyz(rgb), + x = xyz[0], + y = xyz[1], + z = xyz[2], + l, a, b; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; +} + +function rgb2lch(args) { + return lab2lch(rgb2lab(args)); +} + +function hsl2rgb(hsl) { + var h = hsl[0] / 360, + s = hsl[1] / 100, + l = hsl[2] / 100, + t1, t2, t3, rgb, val; + + if (s == 0) { + val = l * 255; + return [val, val, val]; + } + + if (l < 0.5) + t2 = l * (1 + s); + else + t2 = l + s - l * s; + t1 = 2 * l - t2; + + rgb = [0, 0, 0]; + for (var i = 0; i < 3; i++) { + t3 = h + 1 / 3 * - (i - 1); + t3 < 0 && t3++; + t3 > 1 && t3--; + + if (6 * t3 < 1) + val = t1 + (t2 - t1) * 6 * t3; + else if (2 * t3 < 1) + val = t2; + else if (3 * t3 < 2) + val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; + else + val = t1; + + rgb[i] = val * 255; + } + + return rgb; +} + +function hsl2hsv(hsl) { + var h = hsl[0], + s = hsl[1] / 100, + l = hsl[2] / 100, + sv, v; + + if(l === 0) { + // no need to do calc on black + // also avoids divide by 0 error + return [0, 0, 0]; + } + + l *= 2; + s *= (l <= 1) ? l : 2 - l; + v = (l + s) / 2; + sv = (2 * s) / (l + s); + return [h, sv * 100, v * 100]; +} + +function hsl2hwb(args) { + return rgb2hwb(hsl2rgb(args)); +} + +function hsl2cmyk(args) { + return rgb2cmyk(hsl2rgb(args)); +} + +function hsl2keyword(args) { + return rgb2keyword(hsl2rgb(args)); +} + + +function hsv2rgb(hsv) { + var h = hsv[0] / 60, + s = hsv[1] / 100, + v = hsv[2] / 100, + hi = Math.floor(h) % 6; + + var f = h - Math.floor(h), + p = 255 * v * (1 - s), + q = 255 * v * (1 - (s * f)), + t = 255 * v * (1 - (s * (1 - f))), + v = 255 * v; + + switch(hi) { + case 0: + return [v, t, p]; + case 1: + return [q, v, p]; + case 2: + return [p, v, t]; + case 3: + return [p, q, v]; + case 4: + return [t, p, v]; + case 5: + return [v, p, q]; + } +} + +function hsv2hsl(hsv) { + var h = hsv[0], + s = hsv[1] / 100, + v = hsv[2] / 100, + sl, l; + + l = (2 - s) * v; + sl = s * v; + sl /= (l <= 1) ? l : 2 - l; + sl = sl || 0; + l /= 2; + return [h, sl * 100, l * 100]; +} + +function hsv2hwb(args) { + return rgb2hwb(hsv2rgb(args)) +} + +function hsv2cmyk(args) { + return rgb2cmyk(hsv2rgb(args)); +} + +function hsv2keyword(args) { + return rgb2keyword(hsv2rgb(args)); +} + +// http://dev.w3.org/csswg/css-color/#hwb-to-rgb +function hwb2rgb(hwb) { + var h = hwb[0] / 360, + wh = hwb[1] / 100, + bl = hwb[2] / 100, + ratio = wh + bl, + i, v, f, n; + + // wh + bl cant be > 1 + if (ratio > 1) { + wh /= ratio; + bl /= ratio; + } + + i = Math.floor(6 * h); + v = 1 - bl; + f = 6 * h - i; + if ((i & 0x01) != 0) { + f = 1 - f; + } + n = wh + f * (v - wh); // linear interpolation + + switch (i) { + default: + case 6: + case 0: r = v; g = n; b = wh; break; + case 1: r = n; g = v; b = wh; break; + case 2: r = wh; g = v; b = n; break; + case 3: r = wh; g = n; b = v; break; + case 4: r = n; g = wh; b = v; break; + case 5: r = v; g = wh; b = n; break; + } + + return [r * 255, g * 255, b * 255]; +} + +function hwb2hsl(args) { + return rgb2hsl(hwb2rgb(args)); +} + +function hwb2hsv(args) { + return rgb2hsv(hwb2rgb(args)); +} + +function hwb2cmyk(args) { + return rgb2cmyk(hwb2rgb(args)); +} + +function hwb2keyword(args) { + return rgb2keyword(hwb2rgb(args)); +} + +function cmyk2rgb(cmyk) { + var c = cmyk[0] / 100, + m = cmyk[1] / 100, + y = cmyk[2] / 100, + k = cmyk[3] / 100, + r, g, b; + + r = 1 - Math.min(1, c * (1 - k) + k); + g = 1 - Math.min(1, m * (1 - k) + k); + b = 1 - Math.min(1, y * (1 - k) + k); + return [r * 255, g * 255, b * 255]; +} + +function cmyk2hsl(args) { + return rgb2hsl(cmyk2rgb(args)); +} + +function cmyk2hsv(args) { + return rgb2hsv(cmyk2rgb(args)); +} + +function cmyk2hwb(args) { + return rgb2hwb(cmyk2rgb(args)); +} + +function cmyk2keyword(args) { + return rgb2keyword(cmyk2rgb(args)); +} + + +function xyz2rgb(xyz) { + var x = xyz[0] / 100, + y = xyz[1] / 100, + z = xyz[2] / 100, + r, g, b; + + r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986); + g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415); + b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570); + + // assume sRGB + r = r > 0.0031308 ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) + : r = (r * 12.92); + + g = g > 0.0031308 ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) + : g = (g * 12.92); + + b = b > 0.0031308 ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) + : b = (b * 12.92); + + r = Math.min(Math.max(0, r), 1); + g = Math.min(Math.max(0, g), 1); + b = Math.min(Math.max(0, b), 1); + + return [r * 255, g * 255, b * 255]; +} + +function xyz2lab(xyz) { + var x = xyz[0], + y = xyz[1], + z = xyz[2], + l, a, b; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; +} + +function xyz2lch(args) { + return lab2lch(xyz2lab(args)); +} + +function lab2xyz(lab) { + var l = lab[0], + a = lab[1], + b = lab[2], + x, y, z, y2; + + if (l <= 8) { + y = (l * 100) / 903.3; + y2 = (7.787 * (y / 100)) + (16 / 116); + } else { + y = 100 * Math.pow((l + 16) / 116, 3); + y2 = Math.pow(y / 100, 1/3); + } + + x = x / 95.047 <= 0.008856 ? x = (95.047 * ((a / 500) + y2 - (16 / 116))) / 7.787 : 95.047 * Math.pow((a / 500) + y2, 3); + + z = z / 108.883 <= 0.008859 ? z = (108.883 * (y2 - (b / 200) - (16 / 116))) / 7.787 : 108.883 * Math.pow(y2 - (b / 200), 3); + + return [x, y, z]; +} + +function lab2lch(lab) { + var l = lab[0], + a = lab[1], + b = lab[2], + hr, h, c; + + hr = Math.atan2(b, a); + h = hr * 360 / 2 / Math.PI; + if (h < 0) { + h += 360; + } + c = Math.sqrt(a * a + b * b); + return [l, c, h]; +} + +function lab2rgb(args) { + return xyz2rgb(lab2xyz(args)); +} + +function lch2lab(lch) { + var l = lch[0], + c = lch[1], + h = lch[2], + a, b, hr; + + hr = h / 360 * 2 * Math.PI; + a = c * Math.cos(hr); + b = c * Math.sin(hr); + return [l, a, b]; +} + +function lch2xyz(args) { + return lab2xyz(lch2lab(args)); +} + +function lch2rgb(args) { + return lab2rgb(lch2lab(args)); +} + +function keyword2rgb(keyword) { + return cssKeywords[keyword]; +} + +function keyword2hsl(args) { + return rgb2hsl(keyword2rgb(args)); +} + +function keyword2hsv(args) { + return rgb2hsv(keyword2rgb(args)); +} + +function keyword2hwb(args) { + return rgb2hwb(keyword2rgb(args)); +} + +function keyword2cmyk(args) { + return rgb2cmyk(keyword2rgb(args)); +} + +function keyword2lab(args) { + return rgb2lab(keyword2rgb(args)); +} + +function keyword2xyz(args) { + return rgb2xyz(keyword2rgb(args)); +} + +var cssKeywords = { + aliceblue: [240,248,255], + antiquewhite: [250,235,215], + aqua: [0,255,255], + aquamarine: [127,255,212], + azure: [240,255,255], + beige: [245,245,220], + bisque: [255,228,196], + black: [0,0,0], + blanchedalmond: [255,235,205], + blue: [0,0,255], + blueviolet: [138,43,226], + brown: [165,42,42], + burlywood: [222,184,135], + cadetblue: [95,158,160], + chartreuse: [127,255,0], + chocolate: [210,105,30], + coral: [255,127,80], + cornflowerblue: [100,149,237], + cornsilk: [255,248,220], + crimson: [220,20,60], + cyan: [0,255,255], + darkblue: [0,0,139], + darkcyan: [0,139,139], + darkgoldenrod: [184,134,11], + darkgray: [169,169,169], + darkgreen: [0,100,0], + darkgrey: [169,169,169], + darkkhaki: [189,183,107], + darkmagenta: [139,0,139], + darkolivegreen: [85,107,47], + darkorange: [255,140,0], + darkorchid: [153,50,204], + darkred: [139,0,0], + darksalmon: [233,150,122], + darkseagreen: [143,188,143], + darkslateblue: [72,61,139], + darkslategray: [47,79,79], + darkslategrey: [47,79,79], + darkturquoise: [0,206,209], + darkviolet: [148,0,211], + deeppink: [255,20,147], + deepskyblue: [0,191,255], + dimgray: [105,105,105], + dimgrey: [105,105,105], + dodgerblue: [30,144,255], + firebrick: [178,34,34], + floralwhite: [255,250,240], + forestgreen: [34,139,34], + fuchsia: [255,0,255], + gainsboro: [220,220,220], + ghostwhite: [248,248,255], + gold: [255,215,0], + goldenrod: [218,165,32], + gray: [128,128,128], + green: [0,128,0], + greenyellow: [173,255,47], + grey: [128,128,128], + honeydew: [240,255,240], + hotpink: [255,105,180], + indianred: [205,92,92], + indigo: [75,0,130], + ivory: [255,255,240], + khaki: [240,230,140], + lavender: [230,230,250], + lavenderblush: [255,240,245], + lawngreen: [124,252,0], + lemonchiffon: [255,250,205], + lightblue: [173,216,230], + lightcoral: [240,128,128], + lightcyan: [224,255,255], + lightgoldenrodyellow: [250,250,210], + lightgray: [211,211,211], + lightgreen: [144,238,144], + lightgrey: [211,211,211], + lightpink: [255,182,193], + lightsalmon: [255,160,122], + lightseagreen: [32,178,170], + lightskyblue: [135,206,250], + lightslategray: [119,136,153], + lightslategrey: [119,136,153], + lightsteelblue: [176,196,222], + lightyellow: [255,255,224], + lime: [0,255,0], + limegreen: [50,205,50], + linen: [250,240,230], + magenta: [255,0,255], + maroon: [128,0,0], + mediumaquamarine: [102,205,170], + mediumblue: [0,0,205], + mediumorchid: [186,85,211], + mediumpurple: [147,112,219], + mediumseagreen: [60,179,113], + mediumslateblue: [123,104,238], + mediumspringgreen: [0,250,154], + mediumturquoise: [72,209,204], + mediumvioletred: [199,21,133], + midnightblue: [25,25,112], + mintcream: [245,255,250], + mistyrose: [255,228,225], + moccasin: [255,228,181], + navajowhite: [255,222,173], + navy: [0,0,128], + oldlace: [253,245,230], + olive: [128,128,0], + olivedrab: [107,142,35], + orange: [255,165,0], + orangered: [255,69,0], + orchid: [218,112,214], + palegoldenrod: [238,232,170], + palegreen: [152,251,152], + paleturquoise: [175,238,238], + palevioletred: [219,112,147], + papayawhip: [255,239,213], + peachpuff: [255,218,185], + peru: [205,133,63], + pink: [255,192,203], + plum: [221,160,221], + powderblue: [176,224,230], + purple: [128,0,128], + rebeccapurple: [102, 51, 153], + red: [255,0,0], + rosybrown: [188,143,143], + royalblue: [65,105,225], + saddlebrown: [139,69,19], + salmon: [250,128,114], + sandybrown: [244,164,96], + seagreen: [46,139,87], + seashell: [255,245,238], + sienna: [160,82,45], + silver: [192,192,192], + skyblue: [135,206,235], + slateblue: [106,90,205], + slategray: [112,128,144], + slategrey: [112,128,144], + snow: [255,250,250], + springgreen: [0,255,127], + steelblue: [70,130,180], + tan: [210,180,140], + teal: [0,128,128], + thistle: [216,191,216], + tomato: [255,99,71], + turquoise: [64,224,208], + violet: [238,130,238], + wheat: [245,222,179], + white: [255,255,255], + whitesmoke: [245,245,245], + yellow: [255,255,0], + yellowgreen: [154,205,50] +}; + +var reverseKeywords = {}; +for (var key in cssKeywords) { + reverseKeywords[JSON.stringify(cssKeywords[key])] = key; +} + +var convert = function() { + return new Converter(); +}; + +for (var func in conversions) { + // export Raw versions + convert[func + "Raw"] = (function(func) { + // accept array or plain args + return function(arg) { + if (typeof arg == "number") + arg = Array.prototype.slice.call(arguments); + return conversions[func](arg); + } + })(func); + + var pair = /(\w+)2(\w+)/.exec(func), + from = pair[1], + to = pair[2]; + + // export rgb2hsl and ["rgb"]["hsl"] + convert[from] = convert[from] || {}; + + convert[from][to] = convert[func] = (function(func) { + return function(arg) { + if (typeof arg == "number") + arg = Array.prototype.slice.call(arguments); + + var val = conversions[func](arg); + if (typeof val == "string" || val === undefined) + return val; // keyword + + for (var i = 0; i < val.length; i++) + val[i] = Math.round(val[i]); + return val; + } + })(func); +} + + +/* Converter does lazy conversion and caching */ +var Converter = function() { + this.convs = {}; +}; + +/* Either get the values for a space or + set the values for a space, depending on args */ +Converter.prototype.routeSpace = function(space, args) { + var values = args[0]; + if (values === undefined) { + // color.rgb() + return this.getValues(space); + } + // color.rgb(10, 10, 10) + if (typeof values == "number") { + values = Array.prototype.slice.call(args); + } + + return this.setValues(space, values); +}; + +/* Set the values for a space, invalidating cache */ +Converter.prototype.setValues = function(space, values) { + this.space = space; + this.convs = {}; + this.convs[space] = values; + return this; +}; + +/* Get the values for a space. If there's already + a conversion for the space, fetch it, otherwise + compute it */ +Converter.prototype.getValues = function(space) { + var vals = this.convs[space]; + if (!vals) { + var fspace = this.space, + from = this.convs[fspace]; + vals = convert[fspace][space](from); + + this.convs[space] = vals; + } + return vals; +}; + +["rgb", "hsl", "hsv", "cmyk", "keyword"].forEach(function(space) { + Converter.prototype[space] = function(vals) { + return this.routeSpace(space, arguments); + }; +}); + +var colorConvert = convert; + +var colorName = { + "aliceblue": [240, 248, 255], + "antiquewhite": [250, 235, 215], + "aqua": [0, 255, 255], + "aquamarine": [127, 255, 212], + "azure": [240, 255, 255], + "beige": [245, 245, 220], + "bisque": [255, 228, 196], + "black": [0, 0, 0], + "blanchedalmond": [255, 235, 205], + "blue": [0, 0, 255], + "blueviolet": [138, 43, 226], + "brown": [165, 42, 42], + "burlywood": [222, 184, 135], + "cadetblue": [95, 158, 160], + "chartreuse": [127, 255, 0], + "chocolate": [210, 105, 30], + "coral": [255, 127, 80], + "cornflowerblue": [100, 149, 237], + "cornsilk": [255, 248, 220], + "crimson": [220, 20, 60], + "cyan": [0, 255, 255], + "darkblue": [0, 0, 139], + "darkcyan": [0, 139, 139], + "darkgoldenrod": [184, 134, 11], + "darkgray": [169, 169, 169], + "darkgreen": [0, 100, 0], + "darkgrey": [169, 169, 169], + "darkkhaki": [189, 183, 107], + "darkmagenta": [139, 0, 139], + "darkolivegreen": [85, 107, 47], + "darkorange": [255, 140, 0], + "darkorchid": [153, 50, 204], + "darkred": [139, 0, 0], + "darksalmon": [233, 150, 122], + "darkseagreen": [143, 188, 143], + "darkslateblue": [72, 61, 139], + "darkslategray": [47, 79, 79], + "darkslategrey": [47, 79, 79], + "darkturquoise": [0, 206, 209], + "darkviolet": [148, 0, 211], + "deeppink": [255, 20, 147], + "deepskyblue": [0, 191, 255], + "dimgray": [105, 105, 105], + "dimgrey": [105, 105, 105], + "dodgerblue": [30, 144, 255], + "firebrick": [178, 34, 34], + "floralwhite": [255, 250, 240], + "forestgreen": [34, 139, 34], + "fuchsia": [255, 0, 255], + "gainsboro": [220, 220, 220], + "ghostwhite": [248, 248, 255], + "gold": [255, 215, 0], + "goldenrod": [218, 165, 32], + "gray": [128, 128, 128], + "green": [0, 128, 0], + "greenyellow": [173, 255, 47], + "grey": [128, 128, 128], + "honeydew": [240, 255, 240], + "hotpink": [255, 105, 180], + "indianred": [205, 92, 92], + "indigo": [75, 0, 130], + "ivory": [255, 255, 240], + "khaki": [240, 230, 140], + "lavender": [230, 230, 250], + "lavenderblush": [255, 240, 245], + "lawngreen": [124, 252, 0], + "lemonchiffon": [255, 250, 205], + "lightblue": [173, 216, 230], + "lightcoral": [240, 128, 128], + "lightcyan": [224, 255, 255], + "lightgoldenrodyellow": [250, 250, 210], + "lightgray": [211, 211, 211], + "lightgreen": [144, 238, 144], + "lightgrey": [211, 211, 211], + "lightpink": [255, 182, 193], + "lightsalmon": [255, 160, 122], + "lightseagreen": [32, 178, 170], + "lightskyblue": [135, 206, 250], + "lightslategray": [119, 136, 153], + "lightslategrey": [119, 136, 153], + "lightsteelblue": [176, 196, 222], + "lightyellow": [255, 255, 224], + "lime": [0, 255, 0], + "limegreen": [50, 205, 50], + "linen": [250, 240, 230], + "magenta": [255, 0, 255], + "maroon": [128, 0, 0], + "mediumaquamarine": [102, 205, 170], + "mediumblue": [0, 0, 205], + "mediumorchid": [186, 85, 211], + "mediumpurple": [147, 112, 219], + "mediumseagreen": [60, 179, 113], + "mediumslateblue": [123, 104, 238], + "mediumspringgreen": [0, 250, 154], + "mediumturquoise": [72, 209, 204], + "mediumvioletred": [199, 21, 133], + "midnightblue": [25, 25, 112], + "mintcream": [245, 255, 250], + "mistyrose": [255, 228, 225], + "moccasin": [255, 228, 181], + "navajowhite": [255, 222, 173], + "navy": [0, 0, 128], + "oldlace": [253, 245, 230], + "olive": [128, 128, 0], + "olivedrab": [107, 142, 35], + "orange": [255, 165, 0], + "orangered": [255, 69, 0], + "orchid": [218, 112, 214], + "palegoldenrod": [238, 232, 170], + "palegreen": [152, 251, 152], + "paleturquoise": [175, 238, 238], + "palevioletred": [219, 112, 147], + "papayawhip": [255, 239, 213], + "peachpuff": [255, 218, 185], + "peru": [205, 133, 63], + "pink": [255, 192, 203], + "plum": [221, 160, 221], + "powderblue": [176, 224, 230], + "purple": [128, 0, 128], + "rebeccapurple": [102, 51, 153], + "red": [255, 0, 0], + "rosybrown": [188, 143, 143], + "royalblue": [65, 105, 225], + "saddlebrown": [139, 69, 19], + "salmon": [250, 128, 114], + "sandybrown": [244, 164, 96], + "seagreen": [46, 139, 87], + "seashell": [255, 245, 238], + "sienna": [160, 82, 45], + "silver": [192, 192, 192], + "skyblue": [135, 206, 235], + "slateblue": [106, 90, 205], + "slategray": [112, 128, 144], + "slategrey": [112, 128, 144], + "snow": [255, 250, 250], + "springgreen": [0, 255, 127], + "steelblue": [70, 130, 180], + "tan": [210, 180, 140], + "teal": [0, 128, 128], + "thistle": [216, 191, 216], + "tomato": [255, 99, 71], + "turquoise": [64, 224, 208], + "violet": [238, 130, 238], + "wheat": [245, 222, 179], + "white": [255, 255, 255], + "whitesmoke": [245, 245, 245], + "yellow": [255, 255, 0], + "yellowgreen": [154, 205, 50] +}; + +/* MIT license */ + + +var colorString = { + getRgba: getRgba, + getHsla: getHsla, + getRgb: getRgb, + getHsl: getHsl, + getHwb: getHwb, + getAlpha: getAlpha, + + hexString: hexString, + rgbString: rgbString, + rgbaString: rgbaString, + percentString: percentString, + percentaString: percentaString, + hslString: hslString, + hslaString: hslaString, + hwbString: hwbString, + keyword: keyword +}; + +function getRgba(string) { + if (!string) { + return; + } + var abbr = /^#([a-fA-F0-9]{3,4})$/i, + hex = /^#([a-fA-F0-9]{6}([a-fA-F0-9]{2})?)$/i, + rgba = /^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i, + per = /^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i, + keyword = /(\w+)/; + + var rgb = [0, 0, 0], + a = 1, + match = string.match(abbr), + hexAlpha = ""; + if (match) { + match = match[1]; + hexAlpha = match[3]; + for (var i = 0; i < rgb.length; i++) { + rgb[i] = parseInt(match[i] + match[i], 16); + } + if (hexAlpha) { + a = Math.round((parseInt(hexAlpha + hexAlpha, 16) / 255) * 100) / 100; + } + } + else if (match = string.match(hex)) { + hexAlpha = match[2]; + match = match[1]; + for (var i = 0; i < rgb.length; i++) { + rgb[i] = parseInt(match.slice(i * 2, i * 2 + 2), 16); + } + if (hexAlpha) { + a = Math.round((parseInt(hexAlpha, 16) / 255) * 100) / 100; + } + } + else if (match = string.match(rgba)) { + for (var i = 0; i < rgb.length; i++) { + rgb[i] = parseInt(match[i + 1]); + } + a = parseFloat(match[4]); + } + else if (match = string.match(per)) { + for (var i = 0; i < rgb.length; i++) { + rgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55); + } + a = parseFloat(match[4]); + } + else if (match = string.match(keyword)) { + if (match[1] == "transparent") { + return [0, 0, 0, 0]; + } + rgb = colorName[match[1]]; + if (!rgb) { + return; + } + } + + for (var i = 0; i < rgb.length; i++) { + rgb[i] = scale(rgb[i], 0, 255); + } + if (!a && a != 0) { + a = 1; + } + else { + a = scale(a, 0, 1); + } + rgb[3] = a; + return rgb; +} + +function getHsla(string) { + if (!string) { + return; + } + var hsl = /^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/; + var match = string.match(hsl); + if (match) { + var alpha = parseFloat(match[4]); + var h = scale(parseInt(match[1]), 0, 360), + s = scale(parseFloat(match[2]), 0, 100), + l = scale(parseFloat(match[3]), 0, 100), + a = scale(isNaN(alpha) ? 1 : alpha, 0, 1); + return [h, s, l, a]; + } +} + +function getHwb(string) { + if (!string) { + return; + } + var hwb = /^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/; + var match = string.match(hwb); + if (match) { + var alpha = parseFloat(match[4]); + var h = scale(parseInt(match[1]), 0, 360), + w = scale(parseFloat(match[2]), 0, 100), + b = scale(parseFloat(match[3]), 0, 100), + a = scale(isNaN(alpha) ? 1 : alpha, 0, 1); + return [h, w, b, a]; + } +} + +function getRgb(string) { + var rgba = getRgba(string); + return rgba && rgba.slice(0, 3); +} + +function getHsl(string) { + var hsla = getHsla(string); + return hsla && hsla.slice(0, 3); +} + +function getAlpha(string) { + var vals = getRgba(string); + if (vals) { + return vals[3]; + } + else if (vals = getHsla(string)) { + return vals[3]; + } + else if (vals = getHwb(string)) { + return vals[3]; + } +} + +// generators +function hexString(rgba, a) { + var a = (a !== undefined && rgba.length === 3) ? a : rgba[3]; + return "#" + hexDouble(rgba[0]) + + hexDouble(rgba[1]) + + hexDouble(rgba[2]) + + ( + (a >= 0 && a < 1) + ? hexDouble(Math.round(a * 255)) + : "" + ); +} + +function rgbString(rgba, alpha) { + if (alpha < 1 || (rgba[3] && rgba[3] < 1)) { + return rgbaString(rgba, alpha); + } + return "rgb(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ")"; +} + +function rgbaString(rgba, alpha) { + if (alpha === undefined) { + alpha = (rgba[3] !== undefined ? rgba[3] : 1); + } + return "rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + + ", " + alpha + ")"; +} + +function percentString(rgba, alpha) { + if (alpha < 1 || (rgba[3] && rgba[3] < 1)) { + return percentaString(rgba, alpha); + } + var r = Math.round(rgba[0]/255 * 100), + g = Math.round(rgba[1]/255 * 100), + b = Math.round(rgba[2]/255 * 100); + + return "rgb(" + r + "%, " + g + "%, " + b + "%)"; +} + +function percentaString(rgba, alpha) { + var r = Math.round(rgba[0]/255 * 100), + g = Math.round(rgba[1]/255 * 100), + b = Math.round(rgba[2]/255 * 100); + return "rgba(" + r + "%, " + g + "%, " + b + "%, " + (alpha || rgba[3] || 1) + ")"; +} + +function hslString(hsla, alpha) { + if (alpha < 1 || (hsla[3] && hsla[3] < 1)) { + return hslaString(hsla, alpha); + } + return "hsl(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%)"; +} + +function hslaString(hsla, alpha) { + if (alpha === undefined) { + alpha = (hsla[3] !== undefined ? hsla[3] : 1); + } + return "hsla(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%, " + + alpha + ")"; +} + +// hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax +// (hwb have alpha optional & 1 is default value) +function hwbString(hwb, alpha) { + if (alpha === undefined) { + alpha = (hwb[3] !== undefined ? hwb[3] : 1); + } + return "hwb(" + hwb[0] + ", " + hwb[1] + "%, " + hwb[2] + "%" + + (alpha !== undefined && alpha !== 1 ? ", " + alpha : "") + ")"; +} + +function keyword(rgb) { + return reverseNames[rgb.slice(0, 3)]; +} + +// helpers +function scale(num, min, max) { + return Math.min(Math.max(min, num), max); +} + +function hexDouble(num) { + var str = num.toString(16).toUpperCase(); + return (str.length < 2) ? "0" + str : str; +} + + +//create a list of reverse color names +var reverseNames = {}; +for (var name in colorName) { + reverseNames[colorName[name]] = name; +} + +/* MIT license */ + + + +var Color = function (obj) { + if (obj instanceof Color) { + return obj; + } + if (!(this instanceof Color)) { + return new Color(obj); + } + + this.valid = false; + this.values = { + rgb: [0, 0, 0], + hsl: [0, 0, 0], + hsv: [0, 0, 0], + hwb: [0, 0, 0], + cmyk: [0, 0, 0, 0], + alpha: 1 + }; + + // parse Color() argument + var vals; + if (typeof obj === 'string') { + vals = colorString.getRgba(obj); + if (vals) { + this.setValues('rgb', vals); + } else if (vals = colorString.getHsla(obj)) { + this.setValues('hsl', vals); + } else if (vals = colorString.getHwb(obj)) { + this.setValues('hwb', vals); + } + } else if (typeof obj === 'object') { + vals = obj; + if (vals.r !== undefined || vals.red !== undefined) { + this.setValues('rgb', vals); + } else if (vals.l !== undefined || vals.lightness !== undefined) { + this.setValues('hsl', vals); + } else if (vals.v !== undefined || vals.value !== undefined) { + this.setValues('hsv', vals); + } else if (vals.w !== undefined || vals.whiteness !== undefined) { + this.setValues('hwb', vals); + } else if (vals.c !== undefined || vals.cyan !== undefined) { + this.setValues('cmyk', vals); + } + } +}; + +Color.prototype = { + isValid: function () { + return this.valid; + }, + rgb: function () { + return this.setSpace('rgb', arguments); + }, + hsl: function () { + return this.setSpace('hsl', arguments); + }, + hsv: function () { + return this.setSpace('hsv', arguments); + }, + hwb: function () { + return this.setSpace('hwb', arguments); + }, + cmyk: function () { + return this.setSpace('cmyk', arguments); + }, + + rgbArray: function () { + return this.values.rgb; + }, + hslArray: function () { + return this.values.hsl; + }, + hsvArray: function () { + return this.values.hsv; + }, + hwbArray: function () { + var values = this.values; + if (values.alpha !== 1) { + return values.hwb.concat([values.alpha]); + } + return values.hwb; + }, + cmykArray: function () { + return this.values.cmyk; + }, + rgbaArray: function () { + var values = this.values; + return values.rgb.concat([values.alpha]); + }, + hslaArray: function () { + var values = this.values; + return values.hsl.concat([values.alpha]); + }, + alpha: function (val) { + if (val === undefined) { + return this.values.alpha; + } + this.setValues('alpha', val); + return this; + }, + + red: function (val) { + return this.setChannel('rgb', 0, val); + }, + green: function (val) { + return this.setChannel('rgb', 1, val); + }, + blue: function (val) { + return this.setChannel('rgb', 2, val); + }, + hue: function (val) { + if (val) { + val %= 360; + val = val < 0 ? 360 + val : val; + } + return this.setChannel('hsl', 0, val); + }, + saturation: function (val) { + return this.setChannel('hsl', 1, val); + }, + lightness: function (val) { + return this.setChannel('hsl', 2, val); + }, + saturationv: function (val) { + return this.setChannel('hsv', 1, val); + }, + whiteness: function (val) { + return this.setChannel('hwb', 1, val); + }, + blackness: function (val) { + return this.setChannel('hwb', 2, val); + }, + value: function (val) { + return this.setChannel('hsv', 2, val); + }, + cyan: function (val) { + return this.setChannel('cmyk', 0, val); + }, + magenta: function (val) { + return this.setChannel('cmyk', 1, val); + }, + yellow: function (val) { + return this.setChannel('cmyk', 2, val); + }, + black: function (val) { + return this.setChannel('cmyk', 3, val); + }, + + hexString: function () { + return colorString.hexString(this.values.rgb); + }, + rgbString: function () { + return colorString.rgbString(this.values.rgb, this.values.alpha); + }, + rgbaString: function () { + return colorString.rgbaString(this.values.rgb, this.values.alpha); + }, + percentString: function () { + return colorString.percentString(this.values.rgb, this.values.alpha); + }, + hslString: function () { + return colorString.hslString(this.values.hsl, this.values.alpha); + }, + hslaString: function () { + return colorString.hslaString(this.values.hsl, this.values.alpha); + }, + hwbString: function () { + return colorString.hwbString(this.values.hwb, this.values.alpha); + }, + keyword: function () { + return colorString.keyword(this.values.rgb, this.values.alpha); + }, + + rgbNumber: function () { + var rgb = this.values.rgb; + return (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; + }, + + luminosity: function () { + // http://www.w3.org/TR/WCAG20/#relativeluminancedef + var rgb = this.values.rgb; + var lum = []; + for (var i = 0; i < rgb.length; i++) { + var chan = rgb[i] / 255; + lum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4); + } + return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; + }, + + contrast: function (color2) { + // http://www.w3.org/TR/WCAG20/#contrast-ratiodef + var lum1 = this.luminosity(); + var lum2 = color2.luminosity(); + if (lum1 > lum2) { + return (lum1 + 0.05) / (lum2 + 0.05); + } + return (lum2 + 0.05) / (lum1 + 0.05); + }, + + level: function (color2) { + var contrastRatio = this.contrast(color2); + if (contrastRatio >= 7.1) { + return 'AAA'; + } + + return (contrastRatio >= 4.5) ? 'AA' : ''; + }, + + dark: function () { + // YIQ equation from http://24ways.org/2010/calculating-color-contrast + var rgb = this.values.rgb; + var yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; + return yiq < 128; + }, + + light: function () { + return !this.dark(); + }, + + negate: function () { + var rgb = []; + for (var i = 0; i < 3; i++) { + rgb[i] = 255 - this.values.rgb[i]; + } + this.setValues('rgb', rgb); + return this; + }, + + lighten: function (ratio) { + var hsl = this.values.hsl; + hsl[2] += hsl[2] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + darken: function (ratio) { + var hsl = this.values.hsl; + hsl[2] -= hsl[2] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + saturate: function (ratio) { + var hsl = this.values.hsl; + hsl[1] += hsl[1] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + desaturate: function (ratio) { + var hsl = this.values.hsl; + hsl[1] -= hsl[1] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + whiten: function (ratio) { + var hwb = this.values.hwb; + hwb[1] += hwb[1] * ratio; + this.setValues('hwb', hwb); + return this; + }, + + blacken: function (ratio) { + var hwb = this.values.hwb; + hwb[2] += hwb[2] * ratio; + this.setValues('hwb', hwb); + return this; + }, + + greyscale: function () { + var rgb = this.values.rgb; + // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale + var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11; + this.setValues('rgb', [val, val, val]); + return this; + }, + + clearer: function (ratio) { + var alpha = this.values.alpha; + this.setValues('alpha', alpha - (alpha * ratio)); + return this; + }, + + opaquer: function (ratio) { + var alpha = this.values.alpha; + this.setValues('alpha', alpha + (alpha * ratio)); + return this; + }, + + rotate: function (degrees) { + var hsl = this.values.hsl; + var hue = (hsl[0] + degrees) % 360; + hsl[0] = hue < 0 ? 360 + hue : hue; + this.setValues('hsl', hsl); + return this; + }, + + /** + * Ported from sass implementation in C + * https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209 + */ + mix: function (mixinColor, weight) { + var color1 = this; + var color2 = mixinColor; + var p = weight === undefined ? 0.5 : weight; + + var w = 2 * p - 1; + var a = color1.alpha() - color2.alpha(); + + var w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + return this + .rgb( + w1 * color1.red() + w2 * color2.red(), + w1 * color1.green() + w2 * color2.green(), + w1 * color1.blue() + w2 * color2.blue() + ) + .alpha(color1.alpha() * p + color2.alpha() * (1 - p)); + }, + + toJSON: function () { + return this.rgb(); + }, + + clone: function () { + // NOTE(SB): using node-clone creates a dependency to Buffer when using browserify, + // making the final build way to big to embed in Chart.js. So let's do it manually, + // assuming that values to clone are 1 dimension arrays containing only numbers, + // except 'alpha' which is a number. + var result = new Color(); + var source = this.values; + var target = result.values; + var value, type; + + for (var prop in source) { + if (source.hasOwnProperty(prop)) { + value = source[prop]; + type = ({}).toString.call(value); + if (type === '[object Array]') { + target[prop] = value.slice(0); + } else if (type === '[object Number]') { + target[prop] = value; + } else { + console.error('unexpected color value:', value); + } + } + } + + return result; + } +}; + +Color.prototype.spaces = { + rgb: ['red', 'green', 'blue'], + hsl: ['hue', 'saturation', 'lightness'], + hsv: ['hue', 'saturation', 'value'], + hwb: ['hue', 'whiteness', 'blackness'], + cmyk: ['cyan', 'magenta', 'yellow', 'black'] +}; + +Color.prototype.maxes = { + rgb: [255, 255, 255], + hsl: [360, 100, 100], + hsv: [360, 100, 100], + hwb: [360, 100, 100], + cmyk: [100, 100, 100, 100] +}; + +Color.prototype.getValues = function (space) { + var values = this.values; + var vals = {}; + + for (var i = 0; i < space.length; i++) { + vals[space.charAt(i)] = values[space][i]; + } + + if (values.alpha !== 1) { + vals.a = values.alpha; + } + + // {r: 255, g: 255, b: 255, a: 0.4} + return vals; +}; + +Color.prototype.setValues = function (space, vals) { + var values = this.values; + var spaces = this.spaces; + var maxes = this.maxes; + var alpha = 1; + var i; + + this.valid = true; + + if (space === 'alpha') { + alpha = vals; + } else if (vals.length) { + // [10, 10, 10] + values[space] = vals.slice(0, space.length); + alpha = vals[space.length]; + } else if (vals[space.charAt(0)] !== undefined) { + // {r: 10, g: 10, b: 10} + for (i = 0; i < space.length; i++) { + values[space][i] = vals[space.charAt(i)]; + } + + alpha = vals.a; + } else if (vals[spaces[space][0]] !== undefined) { + // {red: 10, green: 10, blue: 10} + var chans = spaces[space]; + + for (i = 0; i < space.length; i++) { + values[space][i] = vals[chans[i]]; + } + + alpha = vals.alpha; + } + + values.alpha = Math.max(0, Math.min(1, (alpha === undefined ? values.alpha : alpha))); + + if (space === 'alpha') { + return false; + } + + var capped; + + // cap values of the space prior converting all values + for (i = 0; i < space.length; i++) { + capped = Math.max(0, Math.min(maxes[space][i], values[space][i])); + values[space][i] = Math.round(capped); + } + + // convert to all the other color spaces + for (var sname in spaces) { + if (sname !== space) { + values[sname] = colorConvert[space][sname](values[space]); + } + } + + return true; +}; + +Color.prototype.setSpace = function (space, args) { + var vals = args[0]; + + if (vals === undefined) { + // color.rgb() + return this.getValues(space); + } + + // color.rgb(10, 10, 10) + if (typeof vals === 'number') { + vals = Array.prototype.slice.call(args); + } + + this.setValues(space, vals); + return this; +}; + +Color.prototype.setChannel = function (space, index, val) { + var svalues = this.values[space]; + if (val === undefined) { + // color.red() + return svalues[index]; + } else if (val === svalues[index]) { + // color.red(color.red()) + return this; + } + + // color.red(100) + svalues[index] = val; + this.setValues(space, svalues); + + return this; +}; + +if (typeof window !== 'undefined') { + window.Color = Color; +} + +var chartjsColor = Color; + +/** + * @namespace Chart.helpers + */ +var helpers = { + /** + * An empty function that can be used, for example, for optional callback. + */ + noop: function() {}, + + /** + * Returns a unique id, sequentially generated from a global variable. + * @returns {number} + * @function + */ + uid: (function() { + var id = 0; + return function() { + return id++; + }; + }()), + + /** + * Returns true if `value` is neither null nor undefined, else returns false. + * @param {*} value - The value to test. + * @returns {boolean} + * @since 2.7.0 + */ + isNullOrUndef: function(value) { + return value === null || typeof value === 'undefined'; + }, + + /** + * Returns true if `value` is an array (including typed arrays), else returns false. + * @param {*} value - The value to test. + * @returns {boolean} + * @function + */ + isArray: function(value) { + if (Array.isArray && Array.isArray(value)) { + return true; + } + var type = Object.prototype.toString.call(value); + if (type.substr(0, 7) === '[object' && type.substr(-6) === 'Array]') { + return true; + } + return false; + }, + + /** + * Returns true if `value` is an object (excluding null), else returns false. + * @param {*} value - The value to test. + * @returns {boolean} + * @since 2.7.0 + */ + isObject: function(value) { + return value !== null && Object.prototype.toString.call(value) === '[object Object]'; + }, + + /** + * Returns true if `value` is a finite number, else returns false + * @param {*} value - The value to test. + * @returns {boolean} + */ + isFinite: function(value) { + return (typeof value === 'number' || value instanceof Number) && isFinite(value); + }, + + /** + * Returns `value` if defined, else returns `defaultValue`. + * @param {*} value - The value to return if defined. + * @param {*} defaultValue - The value to return if `value` is undefined. + * @returns {*} + */ + valueOrDefault: function(value, defaultValue) { + return typeof value === 'undefined' ? defaultValue : value; + }, + + /** + * Returns value at the given `index` in array if defined, else returns `defaultValue`. + * @param {Array} value - The array to lookup for value at `index`. + * @param {number} index - The index in `value` to lookup for value. + * @param {*} defaultValue - The value to return if `value[index]` is undefined. + * @returns {*} + */ + valueAtIndexOrDefault: function(value, index, defaultValue) { + return helpers.valueOrDefault(helpers.isArray(value) ? value[index] : value, defaultValue); + }, + + /** + * Calls `fn` with the given `args` in the scope defined by `thisArg` and returns the + * value returned by `fn`. If `fn` is not a function, this method returns undefined. + * @param {function} fn - The function to call. + * @param {Array|undefined|null} args - The arguments with which `fn` should be called. + * @param {object} [thisArg] - The value of `this` provided for the call to `fn`. + * @returns {*} + */ + callback: function(fn, args, thisArg) { + if (fn && typeof fn.call === 'function') { + return fn.apply(thisArg, args); + } + }, + + /** + * Note(SB) for performance sake, this method should only be used when loopable type + * is unknown or in none intensive code (not called often and small loopable). Else + * it's preferable to use a regular for() loop and save extra function calls. + * @param {object|Array} loopable - The object or array to be iterated. + * @param {function} fn - The function to call for each item. + * @param {object} [thisArg] - The value of `this` provided for the call to `fn`. + * @param {boolean} [reverse] - If true, iterates backward on the loopable. + */ + each: function(loopable, fn, thisArg, reverse) { + var i, len, keys; + if (helpers.isArray(loopable)) { + len = loopable.length; + if (reverse) { + for (i = len - 1; i >= 0; i--) { + fn.call(thisArg, loopable[i], i); + } + } else { + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[i], i); + } + } + } else if (helpers.isObject(loopable)) { + keys = Object.keys(loopable); + len = keys.length; + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[keys[i]], keys[i]); + } + } + }, + + /** + * Returns true if the `a0` and `a1` arrays have the same content, else returns false. + * @see https://stackoverflow.com/a/14853974 + * @param {Array} a0 - The array to compare + * @param {Array} a1 - The array to compare + * @returns {boolean} + */ + arrayEquals: function(a0, a1) { + var i, ilen, v0, v1; + + if (!a0 || !a1 || a0.length !== a1.length) { + return false; + } + + for (i = 0, ilen = a0.length; i < ilen; ++i) { + v0 = a0[i]; + v1 = a1[i]; + + if (v0 instanceof Array && v1 instanceof Array) { + if (!helpers.arrayEquals(v0, v1)) { + return false; + } + } else if (v0 !== v1) { + // NOTE: two different object instances will never be equal: {x:20} != {x:20} + return false; + } + } + + return true; + }, + + /** + * Returns a deep copy of `source` without keeping references on objects and arrays. + * @param {*} source - The value to clone. + * @returns {*} + */ + clone: function(source) { + if (helpers.isArray(source)) { + return source.map(helpers.clone); + } + + if (helpers.isObject(source)) { + var target = {}; + var keys = Object.keys(source); + var klen = keys.length; + var k = 0; + + for (; k < klen; ++k) { + target[keys[k]] = helpers.clone(source[keys[k]]); + } + + return target; + } + + return source; + }, + + /** + * The default merger when Chart.helpers.merge is called without merger option. + * Note(SB): also used by mergeConfig and mergeScaleConfig as fallback. + * @private + */ + _merger: function(key, target, source, options) { + var tval = target[key]; + var sval = source[key]; + + if (helpers.isObject(tval) && helpers.isObject(sval)) { + helpers.merge(tval, sval, options); + } else { + target[key] = helpers.clone(sval); + } + }, + + /** + * Merges source[key] in target[key] only if target[key] is undefined. + * @private + */ + _mergerIf: function(key, target, source) { + var tval = target[key]; + var sval = source[key]; + + if (helpers.isObject(tval) && helpers.isObject(sval)) { + helpers.mergeIf(tval, sval); + } else if (!target.hasOwnProperty(key)) { + target[key] = helpers.clone(sval); + } + }, + + /** + * Recursively deep copies `source` properties into `target` with the given `options`. + * IMPORTANT: `target` is not cloned and will be updated with `source` properties. + * @param {object} target - The target object in which all sources are merged into. + * @param {object|object[]} source - Object(s) to merge into `target`. + * @param {object} [options] - Merging options: + * @param {function} [options.merger] - The merge method (key, target, source, options) + * @returns {object} The `target` object. + */ + merge: function(target, source, options) { + var sources = helpers.isArray(source) ? source : [source]; + var ilen = sources.length; + var merge, i, keys, klen, k; + + if (!helpers.isObject(target)) { + return target; + } + + options = options || {}; + merge = options.merger || helpers._merger; + + for (i = 0; i < ilen; ++i) { + source = sources[i]; + if (!helpers.isObject(source)) { + continue; + } + + keys = Object.keys(source); + for (k = 0, klen = keys.length; k < klen; ++k) { + merge(keys[k], target, source, options); + } + } + + return target; + }, + + /** + * Recursively deep copies `source` properties into `target` *only* if not defined in target. + * IMPORTANT: `target` is not cloned and will be updated with `source` properties. + * @param {object} target - The target object in which all sources are merged into. + * @param {object|object[]} source - Object(s) to merge into `target`. + * @returns {object} The `target` object. + */ + mergeIf: function(target, source) { + return helpers.merge(target, source, {merger: helpers._mergerIf}); + }, + + /** + * Applies the contents of two or more objects together into the first object. + * @param {object} target - The target object in which all objects are merged into. + * @param {object} arg1 - Object containing additional properties to merge in target. + * @param {object} argN - Additional objects containing properties to merge in target. + * @returns {object} The `target` object. + */ + extend: function(target) { + var setFn = function(value, key) { + target[key] = value; + }; + for (var i = 1, ilen = arguments.length; i < ilen; ++i) { + helpers.each(arguments[i], setFn); + } + return target; + }, + + /** + * Basic javascript inheritance based on the model created in Backbone.js + */ + inherits: function(extensions) { + var me = this; + var ChartElement = (extensions && extensions.hasOwnProperty('constructor')) ? extensions.constructor : function() { + return me.apply(this, arguments); + }; + + var Surrogate = function() { + this.constructor = ChartElement; + }; + + Surrogate.prototype = me.prototype; + ChartElement.prototype = new Surrogate(); + ChartElement.extend = helpers.inherits; + + if (extensions) { + helpers.extend(ChartElement.prototype, extensions); + } + + ChartElement.__super__ = me.prototype; + return ChartElement; + } +}; + +var helpers_core = helpers; + +// DEPRECATIONS + +/** + * Provided for backward compatibility, use Chart.helpers.callback instead. + * @function Chart.helpers.callCallback + * @deprecated since version 2.6.0 + * @todo remove at version 3 + * @private + */ +helpers.callCallback = helpers.callback; + +/** + * Provided for backward compatibility, use Array.prototype.indexOf instead. + * Array.prototype.indexOf compatibility: Chrome, Opera, Safari, FF1.5+, IE9+ + * @function Chart.helpers.indexOf + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ +helpers.indexOf = function(array, item, fromIndex) { + return Array.prototype.indexOf.call(array, item, fromIndex); +}; + +/** + * Provided for backward compatibility, use Chart.helpers.valueOrDefault instead. + * @function Chart.helpers.getValueOrDefault + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ +helpers.getValueOrDefault = helpers.valueOrDefault; + +/** + * Provided for backward compatibility, use Chart.helpers.valueAtIndexOrDefault instead. + * @function Chart.helpers.getValueAtIndexOrDefault + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ +helpers.getValueAtIndexOrDefault = helpers.valueAtIndexOrDefault; + +/** + * Easing functions adapted from Robert Penner's easing equations. + * @namespace Chart.helpers.easingEffects + * @see http://www.robertpenner.com/easing/ + */ +var effects = { + linear: function(t) { + return t; + }, + + easeInQuad: function(t) { + return t * t; + }, + + easeOutQuad: function(t) { + return -t * (t - 2); + }, + + easeInOutQuad: function(t) { + if ((t /= 0.5) < 1) { + return 0.5 * t * t; + } + return -0.5 * ((--t) * (t - 2) - 1); + }, + + easeInCubic: function(t) { + return t * t * t; + }, + + easeOutCubic: function(t) { + return (t = t - 1) * t * t + 1; + }, + + easeInOutCubic: function(t) { + if ((t /= 0.5) < 1) { + return 0.5 * t * t * t; + } + return 0.5 * ((t -= 2) * t * t + 2); + }, + + easeInQuart: function(t) { + return t * t * t * t; + }, + + easeOutQuart: function(t) { + return -((t = t - 1) * t * t * t - 1); + }, + + easeInOutQuart: function(t) { + if ((t /= 0.5) < 1) { + return 0.5 * t * t * t * t; + } + return -0.5 * ((t -= 2) * t * t * t - 2); + }, + + easeInQuint: function(t) { + return t * t * t * t * t; + }, + + easeOutQuint: function(t) { + return (t = t - 1) * t * t * t * t + 1; + }, + + easeInOutQuint: function(t) { + if ((t /= 0.5) < 1) { + return 0.5 * t * t * t * t * t; + } + return 0.5 * ((t -= 2) * t * t * t * t + 2); + }, + + easeInSine: function(t) { + return -Math.cos(t * (Math.PI / 2)) + 1; + }, + + easeOutSine: function(t) { + return Math.sin(t * (Math.PI / 2)); + }, + + easeInOutSine: function(t) { + return -0.5 * (Math.cos(Math.PI * t) - 1); + }, + + easeInExpo: function(t) { + return (t === 0) ? 0 : Math.pow(2, 10 * (t - 1)); + }, + + easeOutExpo: function(t) { + return (t === 1) ? 1 : -Math.pow(2, -10 * t) + 1; + }, + + easeInOutExpo: function(t) { + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + if ((t /= 0.5) < 1) { + return 0.5 * Math.pow(2, 10 * (t - 1)); + } + return 0.5 * (-Math.pow(2, -10 * --t) + 2); + }, + + easeInCirc: function(t) { + if (t >= 1) { + return t; + } + return -(Math.sqrt(1 - t * t) - 1); + }, + + easeOutCirc: function(t) { + return Math.sqrt(1 - (t = t - 1) * t); + }, + + easeInOutCirc: function(t) { + if ((t /= 0.5) < 1) { + return -0.5 * (Math.sqrt(1 - t * t) - 1); + } + return 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + + easeInElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + if (!p) { + p = 0.3; + } + if (a < 1) { + a = 1; + s = p / 4; + } else { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * (2 * Math.PI) / p)); + }, + + easeOutElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + if (!p) { + p = 0.3; + } + if (a < 1) { + a = 1; + s = p / 4; + } else { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + return a * Math.pow(2, -10 * t) * Math.sin((t - s) * (2 * Math.PI) / p) + 1; + }, + + easeInOutElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) { + return 0; + } + if ((t /= 0.5) === 2) { + return 1; + } + if (!p) { + p = 0.45; + } + if (a < 1) { + a = 1; + s = p / 4; + } else { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + if (t < 1) { + return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * (2 * Math.PI) / p)); + } + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function(t) { + var s = 1.70158; + return t * t * ((s + 1) * t - s); + }, + + easeOutBack: function(t) { + var s = 1.70158; + return (t = t - 1) * t * ((s + 1) * t + s) + 1; + }, + + easeInOutBack: function(t) { + var s = 1.70158; + if ((t /= 0.5) < 1) { + return 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s)); + } + return 0.5 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + + easeInBounce: function(t) { + return 1 - effects.easeOutBounce(1 - t); + }, + + easeOutBounce: function(t) { + if (t < (1 / 2.75)) { + return 7.5625 * t * t; + } + if (t < (2 / 2.75)) { + return 7.5625 * (t -= (1.5 / 2.75)) * t + 0.75; + } + if (t < (2.5 / 2.75)) { + return 7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375; + } + return 7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375; + }, + + easeInOutBounce: function(t) { + if (t < 0.5) { + return effects.easeInBounce(t * 2) * 0.5; + } + return effects.easeOutBounce(t * 2 - 1) * 0.5 + 0.5; + } +}; + +var helpers_easing = { + effects: effects +}; + +// DEPRECATIONS + +/** + * Provided for backward compatibility, use Chart.helpers.easing.effects instead. + * @function Chart.helpers.easingEffects + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ +helpers_core.easingEffects = effects; + +var PI = Math.PI; +var RAD_PER_DEG = PI / 180; +var DOUBLE_PI = PI * 2; +var HALF_PI = PI / 2; +var QUARTER_PI = PI / 4; +var TWO_THIRDS_PI = PI * 2 / 3; + +/** + * @namespace Chart.helpers.canvas + */ +var exports$1 = { + /** + * Clears the entire canvas associated to the given `chart`. + * @param {Chart} chart - The chart for which to clear the canvas. + */ + clear: function(chart) { + chart.ctx.clearRect(0, 0, chart.width, chart.height); + }, + + /** + * Creates a "path" for a rectangle with rounded corners at position (x, y) with a + * given size (width, height) and the same `radius` for all corners. + * @param {CanvasRenderingContext2D} ctx - The canvas 2D Context. + * @param {number} x - The x axis of the coordinate for the rectangle starting point. + * @param {number} y - The y axis of the coordinate for the rectangle starting point. + * @param {number} width - The rectangle's width. + * @param {number} height - The rectangle's height. + * @param {number} radius - The rounded amount (in pixels) for the four corners. + * @todo handle `radius` as top-left, top-right, bottom-right, bottom-left array/object? + */ + roundedRect: function(ctx, x, y, width, height, radius) { + if (radius) { + var r = Math.min(radius, height / 2, width / 2); + var left = x + r; + var top = y + r; + var right = x + width - r; + var bottom = y + height - r; + + ctx.moveTo(x, top); + if (left < right && top < bottom) { + ctx.arc(left, top, r, -PI, -HALF_PI); + ctx.arc(right, top, r, -HALF_PI, 0); + ctx.arc(right, bottom, r, 0, HALF_PI); + ctx.arc(left, bottom, r, HALF_PI, PI); + } else if (left < right) { + ctx.moveTo(left, y); + ctx.arc(right, top, r, -HALF_PI, HALF_PI); + ctx.arc(left, top, r, HALF_PI, PI + HALF_PI); + } else if (top < bottom) { + ctx.arc(left, top, r, -PI, 0); + ctx.arc(left, bottom, r, 0, PI); + } else { + ctx.arc(left, top, r, -PI, PI); + } + ctx.closePath(); + ctx.moveTo(x, y); + } else { + ctx.rect(x, y, width, height); + } + }, + + drawPoint: function(ctx, style, radius, x, y, rotation) { + var type, xOffset, yOffset, size, cornerRadius; + var rad = (rotation || 0) * RAD_PER_DEG; + + if (style && typeof style === 'object') { + type = style.toString(); + if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { + ctx.drawImage(style, x - style.width / 2, y - style.height / 2, style.width, style.height); + return; + } + } + + if (isNaN(radius) || radius <= 0) { + return; + } + + ctx.beginPath(); + + switch (style) { + // Default includes circle + default: + ctx.arc(x, y, radius, 0, DOUBLE_PI); + ctx.closePath(); + break; + case 'triangle': + ctx.moveTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + ctx.closePath(); + break; + case 'rectRounded': + // NOTE: the rounded rect implementation changed to use `arc` instead of + // `quadraticCurveTo` since it generates better results when rect is + // almost a circle. 0.516 (instead of 0.5) produces results with visually + // closer proportion to the previous impl and it is inscribed in the + // circle with `radius`. For more details, see the following PRs: + // https://github.com/chartjs/Chart.js/issues/5597 + // https://github.com/chartjs/Chart.js/issues/5858 + cornerRadius = radius * 0.516; + size = radius - cornerRadius; + xOffset = Math.cos(rad + QUARTER_PI) * size; + yOffset = Math.sin(rad + QUARTER_PI) * size; + ctx.arc(x - xOffset, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI); + ctx.arc(x + yOffset, y - xOffset, cornerRadius, rad - HALF_PI, rad); + ctx.arc(x + xOffset, y + yOffset, cornerRadius, rad, rad + HALF_PI); + ctx.arc(x - yOffset, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI); + ctx.closePath(); + break; + case 'rect': + if (!rotation) { + size = Math.SQRT1_2 * radius; + ctx.rect(x - size, y - size, 2 * size, 2 * size); + break; + } + rad += QUARTER_PI; + /* falls through */ + case 'rectRot': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + yOffset, y - xOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.lineTo(x - yOffset, y + xOffset); + ctx.closePath(); + break; + case 'crossRot': + rad += QUARTER_PI; + /* falls through */ + case 'cross': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + break; + case 'star': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + rad += QUARTER_PI; + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + break; + case 'line': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + break; + case 'dash': + ctx.moveTo(x, y); + ctx.lineTo(x + Math.cos(rad) * radius, y + Math.sin(rad) * radius); + break; + } + + ctx.fill(); + ctx.stroke(); + }, + + /** + * Returns true if the point is inside the rectangle + * @param {object} point - The point to test + * @param {object} area - The rectangle + * @returns {boolean} + * @private + */ + _isPointInArea: function(point, area) { + var epsilon = 1e-6; // 1e-6 is margin in pixels for accumulated error. + + return point.x > area.left - epsilon && point.x < area.right + epsilon && + point.y > area.top - epsilon && point.y < area.bottom + epsilon; + }, + + clipArea: function(ctx, area) { + ctx.save(); + ctx.beginPath(); + ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top); + ctx.clip(); + }, + + unclipArea: function(ctx) { + ctx.restore(); + }, + + lineTo: function(ctx, previous, target, flip) { + var stepped = target.steppedLine; + if (stepped) { + if (stepped === 'middle') { + var midpoint = (previous.x + target.x) / 2.0; + ctx.lineTo(midpoint, flip ? target.y : previous.y); + ctx.lineTo(midpoint, flip ? previous.y : target.y); + } else if ((stepped === 'after' && !flip) || (stepped !== 'after' && flip)) { + ctx.lineTo(previous.x, target.y); + } else { + ctx.lineTo(target.x, previous.y); + } + ctx.lineTo(target.x, target.y); + return; + } + + if (!target.tension) { + ctx.lineTo(target.x, target.y); + return; + } + + ctx.bezierCurveTo( + flip ? previous.controlPointPreviousX : previous.controlPointNextX, + flip ? previous.controlPointPreviousY : previous.controlPointNextY, + flip ? target.controlPointNextX : target.controlPointPreviousX, + flip ? target.controlPointNextY : target.controlPointPreviousY, + target.x, + target.y); + } +}; + +var helpers_canvas = exports$1; + +// DEPRECATIONS + +/** + * Provided for backward compatibility, use Chart.helpers.canvas.clear instead. + * @namespace Chart.helpers.clear + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ +helpers_core.clear = exports$1.clear; + +/** + * Provided for backward compatibility, use Chart.helpers.canvas.roundedRect instead. + * @namespace Chart.helpers.drawRoundedRectangle + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ +helpers_core.drawRoundedRectangle = function(ctx) { + ctx.beginPath(); + exports$1.roundedRect.apply(exports$1, arguments); +}; + +var defaults = { + /** + * @private + */ + _set: function(scope, values) { + return helpers_core.merge(this[scope] || (this[scope] = {}), values); + } +}; + +defaults._set('global', { + defaultColor: 'rgba(0,0,0,0.1)', + defaultFontColor: '#666', + defaultFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + defaultFontSize: 12, + defaultFontStyle: 'normal', + defaultLineHeight: 1.2, + showLines: true +}); + +var core_defaults = defaults; + +var valueOrDefault = helpers_core.valueOrDefault; + +/** + * Converts the given font object into a CSS font string. + * @param {object} font - A font object. + * @return {string} The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font + * @private + */ +function toFontString(font) { + if (!font || helpers_core.isNullOrUndef(font.size) || helpers_core.isNullOrUndef(font.family)) { + return null; + } + + return (font.style ? font.style + ' ' : '') + + (font.weight ? font.weight + ' ' : '') + + font.size + 'px ' + + font.family; +} + +/** + * @alias Chart.helpers.options + * @namespace + */ +var helpers_options = { + /** + * Converts the given line height `value` in pixels for a specific font `size`. + * @param {number|string} value - The lineHeight to parse (eg. 1.6, '14px', '75%', '1.6em'). + * @param {number} size - The font size (in pixels) used to resolve relative `value`. + * @returns {number} The effective line height in pixels (size * 1.2 if value is invalid). + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height + * @since 2.7.0 + */ + toLineHeight: function(value, size) { + var matches = ('' + value).match(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/); + if (!matches || matches[1] === 'normal') { + return size * 1.2; + } + + value = +matches[2]; + + switch (matches[3]) { + case 'px': + return value; + case '%': + value /= 100; + break; + default: + break; + } + + return size * value; + }, + + /** + * Converts the given value into a padding object with pre-computed width/height. + * @param {number|object} value - If a number, set the value to all TRBL component, + * else, if and object, use defined properties and sets undefined ones to 0. + * @returns {object} The padding values (top, right, bottom, left, width, height) + * @since 2.7.0 + */ + toPadding: function(value) { + var t, r, b, l; + + if (helpers_core.isObject(value)) { + t = +value.top || 0; + r = +value.right || 0; + b = +value.bottom || 0; + l = +value.left || 0; + } else { + t = r = b = l = +value || 0; + } + + return { + top: t, + right: r, + bottom: b, + left: l, + height: t + b, + width: l + r + }; + }, + + /** + * Parses font options and returns the font object. + * @param {object} options - A object that contains font options to be parsed. + * @return {object} The font object. + * @todo Support font.* options and renamed to toFont(). + * @private + */ + _parseFont: function(options) { + var globalDefaults = core_defaults.global; + var size = valueOrDefault(options.fontSize, globalDefaults.defaultFontSize); + var font = { + family: valueOrDefault(options.fontFamily, globalDefaults.defaultFontFamily), + lineHeight: helpers_core.options.toLineHeight(valueOrDefault(options.lineHeight, globalDefaults.defaultLineHeight), size), + size: size, + style: valueOrDefault(options.fontStyle, globalDefaults.defaultFontStyle), + weight: null, + string: '' + }; + + font.string = toFontString(font); + return font; + }, + + /** + * Evaluates the given `inputs` sequentially and returns the first defined value. + * @param {Array} inputs - An array of values, falling back to the last value. + * @param {object} [context] - If defined and the current value is a function, the value + * is called with `context` as first argument and the result becomes the new input. + * @param {number} [index] - If defined and the current value is an array, the value + * at `index` become the new input. + * @since 2.7.0 + */ + resolve: function(inputs, context, index) { + var i, ilen, value; + + for (i = 0, ilen = inputs.length; i < ilen; ++i) { + value = inputs[i]; + if (value === undefined) { + continue; + } + if (context !== undefined && typeof value === 'function') { + value = value(context); + } + if (index !== undefined && helpers_core.isArray(value)) { + value = value[index]; + } + if (value !== undefined) { + return value; + } + } + } +}; + +var helpers$1 = helpers_core; +var easing = helpers_easing; +var canvas = helpers_canvas; +var options = helpers_options; +helpers$1.easing = easing; +helpers$1.canvas = canvas; +helpers$1.options = options; + +function interpolate(start, view, model, ease) { + var keys = Object.keys(model); + var i, ilen, key, actual, origin, target, type, c0, c1; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + + target = model[key]; + + // if a value is added to the model after pivot() has been called, the view + // doesn't contain it, so let's initialize the view to the target value. + if (!view.hasOwnProperty(key)) { + view[key] = target; + } + + actual = view[key]; + + if (actual === target || key[0] === '_') { + continue; + } + + if (!start.hasOwnProperty(key)) { + start[key] = actual; + } + + origin = start[key]; + + type = typeof target; + + if (type === typeof origin) { + if (type === 'string') { + c0 = chartjsColor(origin); + if (c0.valid) { + c1 = chartjsColor(target); + if (c1.valid) { + view[key] = c1.mix(c0, ease).rgbString(); + continue; + } + } + } else if (helpers$1.isFinite(origin) && helpers$1.isFinite(target)) { + view[key] = origin + (target - origin) * ease; + continue; + } + } + + view[key] = target; + } +} + +var Element = function(configuration) { + helpers$1.extend(this, configuration); + this.initialize.apply(this, arguments); +}; + +helpers$1.extend(Element.prototype, { + + initialize: function() { + this.hidden = false; + }, + + pivot: function() { + var me = this; + if (!me._view) { + me._view = helpers$1.clone(me._model); + } + me._start = {}; + return me; + }, + + transition: function(ease) { + var me = this; + var model = me._model; + var start = me._start; + var view = me._view; + + // No animation -> No Transition + if (!model || ease === 1) { + me._view = model; + me._start = null; + return me; + } + + if (!view) { + view = me._view = {}; + } + + if (!start) { + start = me._start = {}; + } + + interpolate(start, view, model, ease); + + return me; + }, + + tooltipPosition: function() { + return { + x: this._model.x, + y: this._model.y + }; + }, + + hasValue: function() { + return helpers$1.isNumber(this._model.x) && helpers$1.isNumber(this._model.y); + } +}); + +Element.extend = helpers$1.inherits; + +var core_element = Element; + +var exports$2 = core_element.extend({ + chart: null, // the animation associated chart instance + currentStep: 0, // the current animation step + numSteps: 60, // default number of steps + easing: '', // the easing to use for this animation + render: null, // render function used by the animation service + + onAnimationProgress: null, // user specified callback to fire on each step of the animation + onAnimationComplete: null, // user specified callback to fire when the animation finishes +}); + +var core_animation = exports$2; + +// DEPRECATIONS + +/** + * Provided for backward compatibility, use Chart.Animation instead + * @prop Chart.Animation#animationObject + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ +Object.defineProperty(exports$2.prototype, 'animationObject', { + get: function() { + return this; + } +}); + +/** + * Provided for backward compatibility, use Chart.Animation#chart instead + * @prop Chart.Animation#chartInstance + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ +Object.defineProperty(exports$2.prototype, 'chartInstance', { + get: function() { + return this.chart; + }, + set: function(value) { + this.chart = value; + } +}); + +core_defaults._set('global', { + animation: { + duration: 1000, + easing: 'easeOutQuart', + onProgress: helpers$1.noop, + onComplete: helpers$1.noop + } +}); + +var core_animations = { + animations: [], + request: null, + + /** + * @param {Chart} chart - The chart to animate. + * @param {Chart.Animation} animation - The animation that we will animate. + * @param {number} duration - The animation duration in ms. + * @param {boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions + */ + addAnimation: function(chart, animation, duration, lazy) { + var animations = this.animations; + var i, ilen; + + animation.chart = chart; + animation.startTime = Date.now(); + animation.duration = duration; + + if (!lazy) { + chart.animating = true; + } + + for (i = 0, ilen = animations.length; i < ilen; ++i) { + if (animations[i].chart === chart) { + animations[i] = animation; + return; + } + } + + animations.push(animation); + + // If there are no animations queued, manually kickstart a digest, for lack of a better word + if (animations.length === 1) { + this.requestAnimationFrame(); + } + }, + + cancelAnimation: function(chart) { + var index = helpers$1.findIndex(this.animations, function(animation) { + return animation.chart === chart; + }); + + if (index !== -1) { + this.animations.splice(index, 1); + chart.animating = false; + } + }, + + requestAnimationFrame: function() { + var me = this; + if (me.request === null) { + // Skip animation frame requests until the active one is executed. + // This can happen when processing mouse events, e.g. 'mousemove' + // and 'mouseout' events will trigger multiple renders. + me.request = helpers$1.requestAnimFrame.call(window, function() { + me.request = null; + me.startDigest(); + }); + } + }, + + /** + * @private + */ + startDigest: function() { + var me = this; + + me.advance(); + + // Do we have more stuff to animate? + if (me.animations.length > 0) { + me.requestAnimationFrame(); + } + }, + + /** + * @private + */ + advance: function() { + var animations = this.animations; + var animation, chart, numSteps, nextStep; + var i = 0; + + // 1 animation per chart, so we are looping charts here + while (i < animations.length) { + animation = animations[i]; + chart = animation.chart; + numSteps = animation.numSteps; + + // Make sure that currentStep starts at 1 + // https://github.com/chartjs/Chart.js/issues/6104 + nextStep = Math.floor((Date.now() - animation.startTime) / animation.duration * numSteps) + 1; + animation.currentStep = Math.min(nextStep, numSteps); + + helpers$1.callback(animation.render, [chart, animation], chart); + helpers$1.callback(animation.onAnimationProgress, [animation], chart); + + if (animation.currentStep >= numSteps) { + helpers$1.callback(animation.onAnimationComplete, [animation], chart); + chart.animating = false; + animations.splice(i, 1); + } else { + ++i; + } + } + } +}; + +var resolve = helpers$1.options.resolve; + +var arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift']; + +/** + * Hooks the array methods that add or remove values ('push', pop', 'shift', 'splice', + * 'unshift') and notify the listener AFTER the array has been altered. Listeners are + * called on the 'onData*' callbacks (e.g. onDataPush, etc.) with same arguments. + */ +function listenArrayEvents(array, listener) { + if (array._chartjs) { + array._chartjs.listeners.push(listener); + return; + } + + Object.defineProperty(array, '_chartjs', { + configurable: true, + enumerable: false, + value: { + listeners: [listener] + } + }); + + arrayEvents.forEach(function(key) { + var method = 'onData' + key.charAt(0).toUpperCase() + key.slice(1); + var base = array[key]; + + Object.defineProperty(array, key, { + configurable: true, + enumerable: false, + value: function() { + var args = Array.prototype.slice.call(arguments); + var res = base.apply(this, args); + + helpers$1.each(array._chartjs.listeners, function(object) { + if (typeof object[method] === 'function') { + object[method].apply(object, args); + } + }); + + return res; + } + }); + }); +} + +/** + * Removes the given array event listener and cleanup extra attached properties (such as + * the _chartjs stub and overridden methods) if array doesn't have any more listeners. + */ +function unlistenArrayEvents(array, listener) { + var stub = array._chartjs; + if (!stub) { + return; + } + + var listeners = stub.listeners; + var index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + + if (listeners.length > 0) { + return; + } + + arrayEvents.forEach(function(key) { + delete array[key]; + }); + + delete array._chartjs; +} + +// Base class for all dataset controllers (line, bar, etc) +var DatasetController = function(chart, datasetIndex) { + this.initialize(chart, datasetIndex); +}; + +helpers$1.extend(DatasetController.prototype, { + + /** + * Element type used to generate a meta dataset (e.g. Chart.element.Line). + * @type {Chart.core.element} + */ + datasetElementType: null, + + /** + * Element type used to generate a meta data (e.g. Chart.element.Point). + * @type {Chart.core.element} + */ + dataElementType: null, + + initialize: function(chart, datasetIndex) { + var me = this; + me.chart = chart; + me.index = datasetIndex; + me.linkScales(); + me.addElements(); + }, + + updateIndex: function(datasetIndex) { + this.index = datasetIndex; + }, + + linkScales: function() { + var me = this; + var meta = me.getMeta(); + var dataset = me.getDataset(); + + if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) { + meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id; + } + if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) { + meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id; + } + }, + + getDataset: function() { + return this.chart.data.datasets[this.index]; + }, + + getMeta: function() { + return this.chart.getDatasetMeta(this.index); + }, + + getScaleForId: function(scaleID) { + return this.chart.scales[scaleID]; + }, + + /** + * @private + */ + _getValueScaleId: function() { + return this.getMeta().yAxisID; + }, + + /** + * @private + */ + _getIndexScaleId: function() { + return this.getMeta().xAxisID; + }, + + /** + * @private + */ + _getValueScale: function() { + return this.getScaleForId(this._getValueScaleId()); + }, + + /** + * @private + */ + _getIndexScale: function() { + return this.getScaleForId(this._getIndexScaleId()); + }, + + reset: function() { + this.update(true); + }, + + /** + * @private + */ + destroy: function() { + if (this._data) { + unlistenArrayEvents(this._data, this); + } + }, + + createMetaDataset: function() { + var me = this; + var type = me.datasetElementType; + return type && new type({ + _chart: me.chart, + _datasetIndex: me.index + }); + }, + + createMetaData: function(index) { + var me = this; + var type = me.dataElementType; + return type && new type({ + _chart: me.chart, + _datasetIndex: me.index, + _index: index + }); + }, + + addElements: function() { + var me = this; + var meta = me.getMeta(); + var data = me.getDataset().data || []; + var metaData = meta.data; + var i, ilen; + + for (i = 0, ilen = data.length; i < ilen; ++i) { + metaData[i] = metaData[i] || me.createMetaData(i); + } + + meta.dataset = meta.dataset || me.createMetaDataset(); + }, + + addElementAndReset: function(index) { + var element = this.createMetaData(index); + this.getMeta().data.splice(index, 0, element); + this.updateElement(element, index, true); + }, + + buildOrUpdateElements: function() { + var me = this; + var dataset = me.getDataset(); + var data = dataset.data || (dataset.data = []); + + // In order to correctly handle data addition/deletion animation (an thus simulate + // real-time charts), we need to monitor these data modifications and synchronize + // the internal meta data accordingly. + if (me._data !== data) { + if (me._data) { + // This case happens when the user replaced the data array instance. + unlistenArrayEvents(me._data, me); + } + + if (data && Object.isExtensible(data)) { + listenArrayEvents(data, me); + } + me._data = data; + } + + // Re-sync meta data in case the user replaced the data array or if we missed + // any updates and so make sure that we handle number of datapoints changing. + me.resyncElements(); + }, + + update: helpers$1.noop, + + transition: function(easingValue) { + var meta = this.getMeta(); + var elements = meta.data || []; + var ilen = elements.length; + var i = 0; + + for (; i < ilen; ++i) { + elements[i].transition(easingValue); + } + + if (meta.dataset) { + meta.dataset.transition(easingValue); + } + }, + + draw: function() { + var meta = this.getMeta(); + var elements = meta.data || []; + var ilen = elements.length; + var i = 0; + + if (meta.dataset) { + meta.dataset.draw(); + } + + for (; i < ilen; ++i) { + elements[i].draw(); + } + }, + + removeHoverStyle: function(element) { + helpers$1.merge(element._model, element.$previousStyle || {}); + delete element.$previousStyle; + }, + + setHoverStyle: function(element) { + var dataset = this.chart.data.datasets[element._datasetIndex]; + var index = element._index; + var custom = element.custom || {}; + var model = element._model; + var getHoverColor = helpers$1.getHoverColor; + + element.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth + }; + + model.backgroundColor = resolve([custom.hoverBackgroundColor, dataset.hoverBackgroundColor, getHoverColor(model.backgroundColor)], undefined, index); + model.borderColor = resolve([custom.hoverBorderColor, dataset.hoverBorderColor, getHoverColor(model.borderColor)], undefined, index); + model.borderWidth = resolve([custom.hoverBorderWidth, dataset.hoverBorderWidth, model.borderWidth], undefined, index); + }, + + /** + * @private + */ + resyncElements: function() { + var me = this; + var meta = me.getMeta(); + var data = me.getDataset().data; + var numMeta = meta.data.length; + var numData = data.length; + + if (numData < numMeta) { + meta.data.splice(numData, numMeta - numData); + } else if (numData > numMeta) { + me.insertElements(numMeta, numData - numMeta); + } + }, + + /** + * @private + */ + insertElements: function(start, count) { + for (var i = 0; i < count; ++i) { + this.addElementAndReset(start + i); + } + }, + + /** + * @private + */ + onDataPush: function() { + var count = arguments.length; + this.insertElements(this.getDataset().data.length - count, count); + }, + + /** + * @private + */ + onDataPop: function() { + this.getMeta().data.pop(); + }, + + /** + * @private + */ + onDataShift: function() { + this.getMeta().data.shift(); + }, + + /** + * @private + */ + onDataSplice: function(start, count) { + this.getMeta().data.splice(start, count); + this.insertElements(start, arguments.length - 2); + }, + + /** + * @private + */ + onDataUnshift: function() { + this.insertElements(0, arguments.length); + } +}); + +DatasetController.extend = helpers$1.inherits; + +var core_datasetController = DatasetController; + +core_defaults._set('global', { + elements: { + arc: { + backgroundColor: core_defaults.global.defaultColor, + borderColor: '#fff', + borderWidth: 2, + borderAlign: 'center' + } + } +}); + +var element_arc = core_element.extend({ + inLabelRange: function(mouseX) { + var vm = this._view; + + if (vm) { + return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hoverRadius, 2)); + } + return false; + }, + + inRange: function(chartX, chartY) { + var vm = this._view; + + if (vm) { + var pointRelativePosition = helpers$1.getAngleFromPoint(vm, {x: chartX, y: chartY}); + var angle = pointRelativePosition.angle; + var distance = pointRelativePosition.distance; + + // Sanitise angle range + var startAngle = vm.startAngle; + var endAngle = vm.endAngle; + while (endAngle < startAngle) { + endAngle += 2.0 * Math.PI; + } + while (angle > endAngle) { + angle -= 2.0 * Math.PI; + } + while (angle < startAngle) { + angle += 2.0 * Math.PI; + } + + // Check if within the range of the open/close angle + var betweenAngles = (angle >= startAngle && angle <= endAngle); + var withinRadius = (distance >= vm.innerRadius && distance <= vm.outerRadius); + + return (betweenAngles && withinRadius); + } + return false; + }, + + getCenterPoint: function() { + var vm = this._view; + var halfAngle = (vm.startAngle + vm.endAngle) / 2; + var halfRadius = (vm.innerRadius + vm.outerRadius) / 2; + return { + x: vm.x + Math.cos(halfAngle) * halfRadius, + y: vm.y + Math.sin(halfAngle) * halfRadius + }; + }, + + getArea: function() { + var vm = this._view; + return Math.PI * ((vm.endAngle - vm.startAngle) / (2 * Math.PI)) * (Math.pow(vm.outerRadius, 2) - Math.pow(vm.innerRadius, 2)); + }, + + tooltipPosition: function() { + var vm = this._view; + var centreAngle = vm.startAngle + ((vm.endAngle - vm.startAngle) / 2); + var rangeFromCentre = (vm.outerRadius - vm.innerRadius) / 2 + vm.innerRadius; + + return { + x: vm.x + (Math.cos(centreAngle) * rangeFromCentre), + y: vm.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + + draw: function() { + var ctx = this._chart.ctx; + var vm = this._view; + var sA = vm.startAngle; + var eA = vm.endAngle; + var pixelMargin = (vm.borderAlign === 'inner') ? 0.33 : 0; + var angleMargin; + + ctx.save(); + + ctx.beginPath(); + ctx.arc(vm.x, vm.y, Math.max(vm.outerRadius - pixelMargin, 0), sA, eA); + ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true); + ctx.closePath(); + + ctx.fillStyle = vm.backgroundColor; + ctx.fill(); + + if (vm.borderWidth) { + if (vm.borderAlign === 'inner') { + // Draw an inner border by cliping the arc and drawing a double-width border + // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders + ctx.beginPath(); + angleMargin = pixelMargin / vm.outerRadius; + ctx.arc(vm.x, vm.y, vm.outerRadius, sA - angleMargin, eA + angleMargin); + if (vm.innerRadius > pixelMargin) { + angleMargin = pixelMargin / vm.innerRadius; + ctx.arc(vm.x, vm.y, vm.innerRadius - pixelMargin, eA + angleMargin, sA - angleMargin, true); + } else { + ctx.arc(vm.x, vm.y, pixelMargin, eA + Math.PI / 2, sA - Math.PI / 2); + } + ctx.closePath(); + ctx.clip(); + + ctx.beginPath(); + ctx.arc(vm.x, vm.y, vm.outerRadius, sA, eA); + ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true); + ctx.closePath(); + + ctx.lineWidth = vm.borderWidth * 2; + ctx.lineJoin = 'round'; + } else { + ctx.lineWidth = vm.borderWidth; + ctx.lineJoin = 'bevel'; + } + + ctx.strokeStyle = vm.borderColor; + ctx.stroke(); + } + + ctx.restore(); + } +}); + +var valueOrDefault$1 = helpers$1.valueOrDefault; + +var defaultColor = core_defaults.global.defaultColor; + +core_defaults._set('global', { + elements: { + line: { + tension: 0.4, + backgroundColor: defaultColor, + borderWidth: 3, + borderColor: defaultColor, + borderCapStyle: 'butt', + borderDash: [], + borderDashOffset: 0.0, + borderJoinStyle: 'miter', + capBezierPoints: true, + fill: true, // do we fill in the area between the line and its base axis + } + } +}); + +var element_line = core_element.extend({ + draw: function() { + var me = this; + var vm = me._view; + var ctx = me._chart.ctx; + var spanGaps = vm.spanGaps; + var points = me._children.slice(); // clone array + var globalDefaults = core_defaults.global; + var globalOptionLineElements = globalDefaults.elements.line; + var lastDrawnIndex = -1; + var index, current, previous, currentVM; + + // If we are looping, adding the first point again + if (me._loop && points.length) { + points.push(points[0]); + } + + ctx.save(); + + // Stroke Line Options + ctx.lineCap = vm.borderCapStyle || globalOptionLineElements.borderCapStyle; + + // IE 9 and 10 do not support line dash + if (ctx.setLineDash) { + ctx.setLineDash(vm.borderDash || globalOptionLineElements.borderDash); + } + + ctx.lineDashOffset = valueOrDefault$1(vm.borderDashOffset, globalOptionLineElements.borderDashOffset); + ctx.lineJoin = vm.borderJoinStyle || globalOptionLineElements.borderJoinStyle; + ctx.lineWidth = valueOrDefault$1(vm.borderWidth, globalOptionLineElements.borderWidth); + ctx.strokeStyle = vm.borderColor || globalDefaults.defaultColor; + + // Stroke Line + ctx.beginPath(); + lastDrawnIndex = -1; + + for (index = 0; index < points.length; ++index) { + current = points[index]; + previous = helpers$1.previousItem(points, index); + currentVM = current._view; + + // First point moves to it's starting position no matter what + if (index === 0) { + if (!currentVM.skip) { + ctx.moveTo(currentVM.x, currentVM.y); + lastDrawnIndex = index; + } + } else { + previous = lastDrawnIndex === -1 ? previous : points[lastDrawnIndex]; + + if (!currentVM.skip) { + if ((lastDrawnIndex !== (index - 1) && !spanGaps) || lastDrawnIndex === -1) { + // There was a gap and this is the first point after the gap + ctx.moveTo(currentVM.x, currentVM.y); + } else { + // Line to next point + helpers$1.canvas.lineTo(ctx, previous._view, current._view); + } + lastDrawnIndex = index; + } + } + } + + ctx.stroke(); + ctx.restore(); + } +}); + +var valueOrDefault$2 = helpers$1.valueOrDefault; + +var defaultColor$1 = core_defaults.global.defaultColor; + +core_defaults._set('global', { + elements: { + point: { + radius: 3, + pointStyle: 'circle', + backgroundColor: defaultColor$1, + borderColor: defaultColor$1, + borderWidth: 1, + // Hover + hitRadius: 1, + hoverRadius: 4, + hoverBorderWidth: 1 + } + } +}); + +function xRange(mouseX) { + var vm = this._view; + return vm ? (Math.abs(mouseX - vm.x) < vm.radius + vm.hitRadius) : false; +} + +function yRange(mouseY) { + var vm = this._view; + return vm ? (Math.abs(mouseY - vm.y) < vm.radius + vm.hitRadius) : false; +} + +var element_point = core_element.extend({ + inRange: function(mouseX, mouseY) { + var vm = this._view; + return vm ? ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(vm.hitRadius + vm.radius, 2)) : false; + }, + + inLabelRange: xRange, + inXRange: xRange, + inYRange: yRange, + + getCenterPoint: function() { + var vm = this._view; + return { + x: vm.x, + y: vm.y + }; + }, + + getArea: function() { + return Math.PI * Math.pow(this._view.radius, 2); + }, + + tooltipPosition: function() { + var vm = this._view; + return { + x: vm.x, + y: vm.y, + padding: vm.radius + vm.borderWidth + }; + }, + + draw: function(chartArea) { + var vm = this._view; + var ctx = this._chart.ctx; + var pointStyle = vm.pointStyle; + var rotation = vm.rotation; + var radius = vm.radius; + var x = vm.x; + var y = vm.y; + var globalDefaults = core_defaults.global; + var defaultColor = globalDefaults.defaultColor; // eslint-disable-line no-shadow + + if (vm.skip) { + return; + } + + // Clipping for Points. + if (chartArea === undefined || helpers$1.canvas._isPointInArea(vm, chartArea)) { + ctx.strokeStyle = vm.borderColor || defaultColor; + ctx.lineWidth = valueOrDefault$2(vm.borderWidth, globalDefaults.elements.point.borderWidth); + ctx.fillStyle = vm.backgroundColor || defaultColor; + helpers$1.canvas.drawPoint(ctx, pointStyle, radius, x, y, rotation); + } + } +}); + +var defaultColor$2 = core_defaults.global.defaultColor; + +core_defaults._set('global', { + elements: { + rectangle: { + backgroundColor: defaultColor$2, + borderColor: defaultColor$2, + borderSkipped: 'bottom', + borderWidth: 0 + } + } +}); + +function isVertical(vm) { + return vm && vm.width !== undefined; +} + +/** + * Helper function to get the bounds of the bar regardless of the orientation + * @param bar {Chart.Element.Rectangle} the bar + * @return {Bounds} bounds of the bar + * @private + */ +function getBarBounds(vm) { + var x1, x2, y1, y2, half; + + if (isVertical(vm)) { + half = vm.width / 2; + x1 = vm.x - half; + x2 = vm.x + half; + y1 = Math.min(vm.y, vm.base); + y2 = Math.max(vm.y, vm.base); + } else { + half = vm.height / 2; + x1 = Math.min(vm.x, vm.base); + x2 = Math.max(vm.x, vm.base); + y1 = vm.y - half; + y2 = vm.y + half; + } + + return { + left: x1, + top: y1, + right: x2, + bottom: y2 + }; +} + +function swap(orig, v1, v2) { + return orig === v1 ? v2 : orig === v2 ? v1 : orig; +} + +function parseBorderSkipped(vm) { + var edge = vm.borderSkipped; + var res = {}; + + if (!edge) { + return res; + } + + if (vm.horizontal) { + if (vm.base > vm.x) { + edge = swap(edge, 'left', 'right'); + } + } else if (vm.base < vm.y) { + edge = swap(edge, 'bottom', 'top'); + } + + res[edge] = true; + return res; +} + +function parseBorderWidth(vm, maxW, maxH) { + var value = vm.borderWidth; + var skip = parseBorderSkipped(vm); + var t, r, b, l; + + if (helpers$1.isObject(value)) { + t = +value.top || 0; + r = +value.right || 0; + b = +value.bottom || 0; + l = +value.left || 0; + } else { + t = r = b = l = +value || 0; + } + + return { + t: skip.top || (t < 0) ? 0 : t > maxH ? maxH : t, + r: skip.right || (r < 0) ? 0 : r > maxW ? maxW : r, + b: skip.bottom || (b < 0) ? 0 : b > maxH ? maxH : b, + l: skip.left || (l < 0) ? 0 : l > maxW ? maxW : l + }; +} + +function boundingRects(vm) { + var bounds = getBarBounds(vm); + var width = bounds.right - bounds.left; + var height = bounds.bottom - bounds.top; + var border = parseBorderWidth(vm, width / 2, height / 2); + + return { + outer: { + x: bounds.left, + y: bounds.top, + w: width, + h: height + }, + inner: { + x: bounds.left + border.l, + y: bounds.top + border.t, + w: width - border.l - border.r, + h: height - border.t - border.b + } + }; +} + +function inRange(vm, x, y) { + var skipX = x === null; + var skipY = y === null; + var bounds = !vm || (skipX && skipY) ? false : getBarBounds(vm); + + return bounds + && (skipX || x >= bounds.left && x <= bounds.right) + && (skipY || y >= bounds.top && y <= bounds.bottom); +} + +var element_rectangle = core_element.extend({ + draw: function() { + var ctx = this._chart.ctx; + var vm = this._view; + var rects = boundingRects(vm); + var outer = rects.outer; + var inner = rects.inner; + + ctx.fillStyle = vm.backgroundColor; + ctx.fillRect(outer.x, outer.y, outer.w, outer.h); + + if (outer.w === inner.w && outer.h === inner.h) { + return; + } + + ctx.save(); + ctx.beginPath(); + ctx.rect(outer.x, outer.y, outer.w, outer.h); + ctx.clip(); + ctx.fillStyle = vm.borderColor; + ctx.rect(inner.x, inner.y, inner.w, inner.h); + ctx.fill('evenodd'); + ctx.restore(); + }, + + height: function() { + var vm = this._view; + return vm.base - vm.y; + }, + + inRange: function(mouseX, mouseY) { + return inRange(this._view, mouseX, mouseY); + }, + + inLabelRange: function(mouseX, mouseY) { + var vm = this._view; + return isVertical(vm) + ? inRange(vm, mouseX, null) + : inRange(vm, null, mouseY); + }, + + inXRange: function(mouseX) { + return inRange(this._view, mouseX, null); + }, + + inYRange: function(mouseY) { + return inRange(this._view, null, mouseY); + }, + + getCenterPoint: function() { + var vm = this._view; + var x, y; + if (isVertical(vm)) { + x = vm.x; + y = (vm.y + vm.base) / 2; + } else { + x = (vm.x + vm.base) / 2; + y = vm.y; + } + + return {x: x, y: y}; + }, + + getArea: function() { + var vm = this._view; + + return isVertical(vm) + ? vm.width * Math.abs(vm.y - vm.base) + : vm.height * Math.abs(vm.x - vm.base); + }, + + tooltipPosition: function() { + var vm = this._view; + return { + x: vm.x, + y: vm.y + }; + } +}); + +var elements = {}; +var Arc = element_arc; +var Line = element_line; +var Point = element_point; +var Rectangle = element_rectangle; +elements.Arc = Arc; +elements.Line = Line; +elements.Point = Point; +elements.Rectangle = Rectangle; + +var resolve$1 = helpers$1.options.resolve; + +core_defaults._set('bar', { + hover: { + mode: 'label' + }, + + scales: { + xAxes: [{ + type: 'category', + categoryPercentage: 0.8, + barPercentage: 0.9, + offset: true, + gridLines: { + offsetGridLines: true + } + }], + + yAxes: [{ + type: 'linear' + }] + } +}); + +/** + * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. + * @private + */ +function computeMinSampleSize(scale, pixels) { + var min = scale.isHorizontal() ? scale.width : scale.height; + var ticks = scale.getTicks(); + var prev, curr, i, ilen; + + for (i = 1, ilen = pixels.length; i < ilen; ++i) { + min = Math.min(min, Math.abs(pixels[i] - pixels[i - 1])); + } + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + min = i > 0 ? Math.min(min, curr - prev) : min; + prev = curr; + } + + return min; +} + +/** + * Computes an "ideal" category based on the absolute bar thickness or, if undefined or null, + * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This + * mode currently always generates bars equally sized (until we introduce scriptable options?). + * @private + */ +function computeFitCategoryTraits(index, ruler, options) { + var thickness = options.barThickness; + var count = ruler.stackCount; + var curr = ruler.pixels[index]; + var size, ratio; + + if (helpers$1.isNullOrUndef(thickness)) { + size = ruler.min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + // When bar thickness is enforced, category and bar percentages are ignored. + // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') + // and deprecate barPercentage since this value is ignored when thickness is absolute. + size = thickness * count; + ratio = 1; + } + + return { + chunk: size / count, + ratio: ratio, + start: curr - (size / 2) + }; +} + +/** + * Computes an "optimal" category that globally arranges bars side by side (no gap when + * percentage options are 1), based on the previous and following categories. This mode + * generates bars with different widths when data are not evenly spaced. + * @private + */ +function computeFlexCategoryTraits(index, ruler, options) { + var pixels = ruler.pixels; + var curr = pixels[index]; + var prev = index > 0 ? pixels[index - 1] : null; + var next = index < pixels.length - 1 ? pixels[index + 1] : null; + var percent = options.categoryPercentage; + var start, size; + + if (prev === null) { + // first data: its size is double based on the next point or, + // if it's also the last data, we use the scale size. + prev = curr - (next === null ? ruler.end - ruler.start : next - curr); + } + + if (next === null) { + // last data: its size is also double based on the previous point. + next = curr + curr - prev; + } + + start = curr - (curr - Math.min(prev, next)) / 2 * percent; + size = Math.abs(next - prev) / 2 * percent; + + return { + chunk: size / ruler.stackCount, + ratio: options.barPercentage, + start: start + }; +} + +var controller_bar = core_datasetController.extend({ + + dataElementType: elements.Rectangle, + + initialize: function() { + var me = this; + var meta; + + core_datasetController.prototype.initialize.apply(me, arguments); + + meta = me.getMeta(); + meta.stack = me.getDataset().stack; + meta.bar = true; + }, + + update: function(reset) { + var me = this; + var rects = me.getMeta().data; + var i, ilen; + + me._ruler = me.getRuler(); + + for (i = 0, ilen = rects.length; i < ilen; ++i) { + me.updateElement(rects[i], i, reset); + } + }, + + updateElement: function(rectangle, index, reset) { + var me = this; + var meta = me.getMeta(); + var dataset = me.getDataset(); + var options = me._resolveElementOptions(rectangle, index); + + rectangle._xScale = me.getScaleForId(meta.xAxisID); + rectangle._yScale = me.getScaleForId(meta.yAxisID); + rectangle._datasetIndex = me.index; + rectangle._index = index; + rectangle._model = { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderSkipped: options.borderSkipped, + borderWidth: options.borderWidth, + datasetLabel: dataset.label, + label: me.chart.data.labels[index] + }; + + me._updateElementGeometry(rectangle, index, reset); + + rectangle.pivot(); + }, + + /** + * @private + */ + _updateElementGeometry: function(rectangle, index, reset) { + var me = this; + var model = rectangle._model; + var vscale = me._getValueScale(); + var base = vscale.getBasePixel(); + var horizontal = vscale.isHorizontal(); + var ruler = me._ruler || me.getRuler(); + var vpixels = me.calculateBarValuePixels(me.index, index); + var ipixels = me.calculateBarIndexPixels(me.index, index, ruler); + + model.horizontal = horizontal; + model.base = reset ? base : vpixels.base; + model.x = horizontal ? reset ? base : vpixels.head : ipixels.center; + model.y = horizontal ? ipixels.center : reset ? base : vpixels.head; + model.height = horizontal ? ipixels.size : undefined; + model.width = horizontal ? undefined : ipixels.size; + }, + + /** + * Returns the stacks based on groups and bar visibility. + * @param {number} [last] - The dataset index + * @returns {string[]} The list of stack IDs + * @private + */ + _getStacks: function(last) { + var me = this; + var chart = me.chart; + var scale = me._getIndexScale(); + var stacked = scale.options.stacked; + var ilen = last === undefined ? chart.data.datasets.length : last + 1; + var stacks = []; + var i, meta; + + for (i = 0; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + if (meta.bar && chart.isDatasetVisible(i) && + (stacked === false || + (stacked === true && stacks.indexOf(meta.stack) === -1) || + (stacked === undefined && (meta.stack === undefined || stacks.indexOf(meta.stack) === -1)))) { + stacks.push(meta.stack); + } + } + + return stacks; + }, + + /** + * Returns the effective number of stacks based on groups and bar visibility. + * @private + */ + getStackCount: function() { + return this._getStacks().length; + }, + + /** + * Returns the stack index for the given dataset based on groups and bar visibility. + * @param {number} [datasetIndex] - The dataset index + * @param {string} [name] - The stack name to find + * @returns {number} The stack index + * @private + */ + getStackIndex: function(datasetIndex, name) { + var stacks = this._getStacks(datasetIndex); + var index = (name !== undefined) + ? stacks.indexOf(name) + : -1; // indexOf returns -1 if element is not present + + return (index === -1) + ? stacks.length - 1 + : index; + }, + + /** + * @private + */ + getRuler: function() { + var me = this; + var scale = me._getIndexScale(); + var stackCount = me.getStackCount(); + var datasetIndex = me.index; + var isHorizontal = scale.isHorizontal(); + var start = isHorizontal ? scale.left : scale.top; + var end = start + (isHorizontal ? scale.width : scale.height); + var pixels = []; + var i, ilen, min; + + for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { + pixels.push(scale.getPixelForValue(null, i, datasetIndex)); + } + + min = helpers$1.isNullOrUndef(scale.options.barThickness) + ? computeMinSampleSize(scale, pixels) + : -1; + + return { + min: min, + pixels: pixels, + start: start, + end: end, + stackCount: stackCount, + scale: scale + }; + }, + + /** + * Note: pixel values are not clamped to the scale area. + * @private + */ + calculateBarValuePixels: function(datasetIndex, index) { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var scale = me._getValueScale(); + var isHorizontal = scale.isHorizontal(); + var datasets = chart.data.datasets; + var value = +scale.getRightValue(datasets[datasetIndex].data[index]); + var minBarLength = scale.options.minBarLength; + var stacked = scale.options.stacked; + var stack = meta.stack; + var start = 0; + var i, imeta, ivalue, base, head, size; + + if (stacked || (stacked === undefined && stack !== undefined)) { + for (i = 0; i < datasetIndex; ++i) { + imeta = chart.getDatasetMeta(i); + + if (imeta.bar && + imeta.stack === stack && + imeta.controller._getValueScaleId() === scale.id && + chart.isDatasetVisible(i)) { + + ivalue = +scale.getRightValue(datasets[i].data[index]); + if ((value < 0 && ivalue < 0) || (value >= 0 && ivalue > 0)) { + start += ivalue; + } + } + } + } + + base = scale.getPixelForValue(start); + head = scale.getPixelForValue(start + value); + size = head - base; + + if (minBarLength !== undefined && Math.abs(size) < minBarLength) { + size = minBarLength; + if (value >= 0 && !isHorizontal || value < 0 && isHorizontal) { + head = base - minBarLength; + } else { + head = base + minBarLength; + } + } + + return { + size: size, + base: base, + head: head, + center: head + size / 2 + }; + }, + + /** + * @private + */ + calculateBarIndexPixels: function(datasetIndex, index, ruler) { + var me = this; + var options = ruler.scale.options; + var range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options) + : computeFitCategoryTraits(index, ruler, options); + + var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); + var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + var size = Math.min( + helpers$1.valueOrDefault(options.maxBarThickness, Infinity), + range.chunk * range.ratio); + + return { + base: center - size / 2, + head: center + size / 2, + center: center, + size: size + }; + }, + + draw: function() { + var me = this; + var chart = me.chart; + var scale = me._getValueScale(); + var rects = me.getMeta().data; + var dataset = me.getDataset(); + var ilen = rects.length; + var i = 0; + + helpers$1.canvas.clipArea(chart.ctx, chart.chartArea); + + for (; i < ilen; ++i) { + if (!isNaN(scale.getRightValue(dataset.data[i]))) { + rects[i].draw(); + } + } + + helpers$1.canvas.unclipArea(chart.ctx); + }, + + /** + * @private + */ + _resolveElementOptions: function(rectangle, index) { + var me = this; + var chart = me.chart; + var datasets = chart.data.datasets; + var dataset = datasets[me.index]; + var custom = rectangle.custom || {}; + var options = chart.options.elements.rectangle; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var keys = [ + 'backgroundColor', + 'borderColor', + 'borderSkipped', + 'borderWidth' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve$1([ + custom[key], + dataset[key], + options[key] + ], context, index); + } + + return values; + } +}); + +var valueOrDefault$3 = helpers$1.valueOrDefault; +var resolve$2 = helpers$1.options.resolve; + +core_defaults._set('bubble', { + hover: { + mode: 'single' + }, + + scales: { + xAxes: [{ + type: 'linear', // bubble should probably use a linear scale by default + position: 'bottom', + id: 'x-axis-0' // need an ID so datasets can reference the scale + }], + yAxes: [{ + type: 'linear', + position: 'left', + id: 'y-axis-0' + }] + }, + + tooltips: { + callbacks: { + title: function() { + // Title doesn't make sense for scatter since we format the data as a point + return ''; + }, + label: function(item, data) { + var datasetLabel = data.datasets[item.datasetIndex].label || ''; + var dataPoint = data.datasets[item.datasetIndex].data[item.index]; + return datasetLabel + ': (' + item.xLabel + ', ' + item.yLabel + ', ' + dataPoint.r + ')'; + } + } + } +}); + +var controller_bubble = core_datasetController.extend({ + /** + * @protected + */ + dataElementType: elements.Point, + + /** + * @protected + */ + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var points = meta.data; + + // Update Points + helpers$1.each(points, function(point, index) { + me.updateElement(point, index, reset); + }); + }, + + /** + * @protected + */ + updateElement: function(point, index, reset) { + var me = this; + var meta = me.getMeta(); + var custom = point.custom || {}; + var xScale = me.getScaleForId(meta.xAxisID); + var yScale = me.getScaleForId(meta.yAxisID); + var options = me._resolveElementOptions(point, index); + var data = me.getDataset().data[index]; + var dsIndex = me.index; + + var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex); + var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex); + + point._xScale = xScale; + point._yScale = yScale; + point._options = options; + point._datasetIndex = dsIndex; + point._index = index; + point._model = { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + hitRadius: options.hitRadius, + pointStyle: options.pointStyle, + rotation: options.rotation, + radius: reset ? 0 : options.radius, + skip: custom.skip || isNaN(x) || isNaN(y), + x: x, + y: y, + }; + + point.pivot(); + }, + + /** + * @protected + */ + setHoverStyle: function(point) { + var model = point._model; + var options = point._options; + var getHoverColor = helpers$1.getHoverColor; + + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + + model.backgroundColor = valueOrDefault$3(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$3(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$3(options.hoverBorderWidth, options.borderWidth); + model.radius = options.radius + options.hoverRadius; + }, + + /** + * @private + */ + _resolveElementOptions: function(point, index) { + var me = this; + var chart = me.chart; + var datasets = chart.data.datasets; + var dataset = datasets[me.index]; + var custom = point.custom || {}; + var options = chart.options.elements.point; + var data = dataset.data[index]; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var keys = [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'pointStyle', + 'rotation' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve$2([ + custom[key], + dataset[key], + options[key] + ], context, index); + } + + // Custom radius resolution + values.radius = resolve$2([ + custom.radius, + data ? data.r : undefined, + dataset.radius, + options.radius + ], context, index); + + return values; + } +}); + +var resolve$3 = helpers$1.options.resolve; +var valueOrDefault$4 = helpers$1.valueOrDefault; + +core_defaults._set('doughnut', { + animation: { + // Boolean - Whether we animate the rotation of the Doughnut + animateRotate: true, + // Boolean - Whether we animate scaling the Doughnut from the centre + animateScale: false + }, + hover: { + mode: 'single' + }, + legendCallback: function(chart) { + var text = []; + text.push('
    '); + + var data = chart.data; + var datasets = data.datasets; + var labels = data.labels; + + if (datasets.length) { + for (var i = 0; i < datasets[0].data.length; ++i) { + text.push('
  • '); + if (labels[i]) { + text.push(labels[i]); + } + text.push('
  • '); + } + } + + text.push('
'); + return text.join(''); + }, + legend: { + labels: { + generateLabels: function(chart) { + var data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map(function(label, i) { + var meta = chart.getDatasetMeta(0); + var ds = data.datasets[0]; + var arc = meta.data[i]; + var custom = arc && arc.custom || {}; + var arcOpts = chart.options.elements.arc; + var fill = resolve$3([custom.backgroundColor, ds.backgroundColor, arcOpts.backgroundColor], undefined, i); + var stroke = resolve$3([custom.borderColor, ds.borderColor, arcOpts.borderColor], undefined, i); + var bw = resolve$3([custom.borderWidth, ds.borderWidth, arcOpts.borderWidth], undefined, i); + + return { + text: label, + fillStyle: fill, + strokeStyle: stroke, + lineWidth: bw, + hidden: isNaN(ds.data[i]) || meta.data[i].hidden, + + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick: function(e, legendItem) { + var index = legendItem.index; + var chart = this.chart; + var i, ilen, meta; + + for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + // toggle visibility of index if exists + if (meta.data[index]) { + meta.data[index].hidden = !meta.data[index].hidden; + } + } + + chart.update(); + } + }, + + // The percentage of the chart that we cut out of the middle. + cutoutPercentage: 50, + + // The rotation of the chart, where the first data arc begins. + rotation: Math.PI * -0.5, + + // The total circumference of the chart. + circumference: Math.PI * 2.0, + + // Need to override these to give a nice default + tooltips: { + callbacks: { + title: function() { + return ''; + }, + label: function(tooltipItem, data) { + var dataLabel = data.labels[tooltipItem.index]; + var value = ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + + if (helpers$1.isArray(dataLabel)) { + // show value on first line of multiline label + // need to clone because we are changing the value + dataLabel = dataLabel.slice(); + dataLabel[0] += value; + } else { + dataLabel += value; + } + + return dataLabel; + } + } + } +}); + +var controller_doughnut = core_datasetController.extend({ + + dataElementType: elements.Arc, + + linkScales: helpers$1.noop, + + // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly + getRingIndex: function(datasetIndex) { + var ringIndex = 0; + + for (var j = 0; j < datasetIndex; ++j) { + if (this.chart.isDatasetVisible(j)) { + ++ringIndex; + } + } + + return ringIndex; + }, + + update: function(reset) { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var opts = chart.options; + var availableWidth = chartArea.right - chartArea.left; + var availableHeight = chartArea.bottom - chartArea.top; + var minSize = Math.min(availableWidth, availableHeight); + var offset = {x: 0, y: 0}; + var meta = me.getMeta(); + var arcs = meta.data; + var cutoutPercentage = opts.cutoutPercentage; + var circumference = opts.circumference; + var chartWeight = me._getRingWeight(me.index); + var i, ilen; + + // If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc + if (circumference < Math.PI * 2.0) { + var startAngle = opts.rotation % (Math.PI * 2.0); + startAngle += Math.PI * 2.0 * (startAngle >= Math.PI ? -1 : startAngle < -Math.PI ? 1 : 0); + var endAngle = startAngle + circumference; + var start = {x: Math.cos(startAngle), y: Math.sin(startAngle)}; + var end = {x: Math.cos(endAngle), y: Math.sin(endAngle)}; + var contains0 = (startAngle <= 0 && endAngle >= 0) || (startAngle <= Math.PI * 2.0 && Math.PI * 2.0 <= endAngle); + var contains90 = (startAngle <= Math.PI * 0.5 && Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 2.5 && Math.PI * 2.5 <= endAngle); + var contains180 = (startAngle <= -Math.PI && -Math.PI <= endAngle) || (startAngle <= Math.PI && Math.PI <= endAngle); + var contains270 = (startAngle <= -Math.PI * 0.5 && -Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 1.5 && Math.PI * 1.5 <= endAngle); + var cutout = cutoutPercentage / 100.0; + var min = {x: contains180 ? -1 : Math.min(start.x * (start.x < 0 ? 1 : cutout), end.x * (end.x < 0 ? 1 : cutout)), y: contains270 ? -1 : Math.min(start.y * (start.y < 0 ? 1 : cutout), end.y * (end.y < 0 ? 1 : cutout))}; + var max = {x: contains0 ? 1 : Math.max(start.x * (start.x > 0 ? 1 : cutout), end.x * (end.x > 0 ? 1 : cutout)), y: contains90 ? 1 : Math.max(start.y * (start.y > 0 ? 1 : cutout), end.y * (end.y > 0 ? 1 : cutout))}; + var size = {width: (max.x - min.x) * 0.5, height: (max.y - min.y) * 0.5}; + minSize = Math.min(availableWidth / size.width, availableHeight / size.height); + offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5}; + } + + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + arcs[i]._options = me._resolveElementOptions(arcs[i], i); + } + + chart.borderWidth = me.getMaxBorderWidth(); + chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); + chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); + chart.radiusLength = (chart.outerRadius - chart.innerRadius) / (me._getVisibleDatasetWeightTotal() || 1); + chart.offsetX = offset.x * chart.outerRadius; + chart.offsetY = offset.y * chart.outerRadius; + + meta.total = me.calculateTotal(); + + me.outerRadius = chart.outerRadius - chart.radiusLength * me._getRingWeightOffset(me.index); + me.innerRadius = Math.max(me.outerRadius - chart.radiusLength * chartWeight, 0); + + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + me.updateElement(arcs[i], i, reset); + } + }, + + updateElement: function(arc, index, reset) { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var opts = chart.options; + var animationOpts = opts.animation; + var centerX = (chartArea.left + chartArea.right) / 2; + var centerY = (chartArea.top + chartArea.bottom) / 2; + var startAngle = opts.rotation; // non reset case handled later + var endAngle = opts.rotation; // non reset case handled later + var dataset = me.getDataset(); + var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / (2.0 * Math.PI)); + var innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius; + var outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius; + var options = arc._options || {}; + + helpers$1.extend(arc, { + // Utility + _datasetIndex: me.index, + _index: index, + + // Desired view properties + _model: { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + borderAlign: options.borderAlign, + x: centerX + chart.offsetX, + y: centerY + chart.offsetY, + startAngle: startAngle, + endAngle: endAngle, + circumference: circumference, + outerRadius: outerRadius, + innerRadius: innerRadius, + label: helpers$1.valueAtIndexOrDefault(dataset.label, index, chart.data.labels[index]) + } + }); + + var model = arc._model; + + // Set correct angles if not resetting + if (!reset || !animationOpts.animateRotate) { + if (index === 0) { + model.startAngle = opts.rotation; + } else { + model.startAngle = me.getMeta().data[index - 1]._model.endAngle; + } + + model.endAngle = model.startAngle + model.circumference; + } + + arc.pivot(); + }, + + calculateTotal: function() { + var dataset = this.getDataset(); + var meta = this.getMeta(); + var total = 0; + var value; + + helpers$1.each(meta.data, function(element, index) { + value = dataset.data[index]; + if (!isNaN(value) && !element.hidden) { + total += Math.abs(value); + } + }); + + /* if (total === 0) { + total = NaN; + }*/ + + return total; + }, + + calculateCircumference: function(value) { + var total = this.getMeta().total; + if (total > 0 && !isNaN(value)) { + return (Math.PI * 2.0) * (Math.abs(value) / total); + } + return 0; + }, + + // gets the max border or hover width to properly scale pie charts + getMaxBorderWidth: function(arcs) { + var me = this; + var max = 0; + var chart = me.chart; + var i, ilen, meta, arc, controller, options, borderWidth, hoverWidth; + + if (!arcs) { + // Find the outmost visible dataset + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + if (chart.isDatasetVisible(i)) { + meta = chart.getDatasetMeta(i); + arcs = meta.data; + if (i !== me.index) { + controller = meta.controller; + } + break; + } + } + } + + if (!arcs) { + return 0; + } + + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + arc = arcs[i]; + options = controller ? controller._resolveElementOptions(arc, i) : arc._options; + if (options.borderAlign !== 'inner') { + borderWidth = options.borderWidth; + hoverWidth = options.hoverBorderWidth; + + max = borderWidth > max ? borderWidth : max; + max = hoverWidth > max ? hoverWidth : max; + } + } + return max; + }, + + /** + * @protected + */ + setHoverStyle: function(arc) { + var model = arc._model; + var options = arc._options; + var getHoverColor = helpers$1.getHoverColor; + + arc.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + }; + + model.backgroundColor = valueOrDefault$4(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$4(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$4(options.hoverBorderWidth, options.borderWidth); + }, + + /** + * @private + */ + _resolveElementOptions: function(arc, index) { + var me = this; + var chart = me.chart; + var dataset = me.getDataset(); + var custom = arc.custom || {}; + var options = chart.options.elements.arc; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var keys = [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'borderAlign', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve$3([ + custom[key], + dataset[key], + options[key] + ], context, index); + } + + return values; + }, + + /** + * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly + * @private + */ + _getRingWeightOffset: function(datasetIndex) { + var ringWeightOffset = 0; + + for (var i = 0; i < datasetIndex; ++i) { + if (this.chart.isDatasetVisible(i)) { + ringWeightOffset += this._getRingWeight(i); + } + } + + return ringWeightOffset; + }, + + /** + * @private + */ + _getRingWeight: function(dataSetIndex) { + return Math.max(valueOrDefault$4(this.chart.data.datasets[dataSetIndex].weight, 1), 0); + }, + + /** + * Returns the sum of all visibile data set weights. This value can be 0. + * @private + */ + _getVisibleDatasetWeightTotal: function() { + return this._getRingWeightOffset(this.chart.data.datasets.length); + } +}); + +core_defaults._set('horizontalBar', { + hover: { + mode: 'index', + axis: 'y' + }, + + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom' + }], + + yAxes: [{ + type: 'category', + position: 'left', + categoryPercentage: 0.8, + barPercentage: 0.9, + offset: true, + gridLines: { + offsetGridLines: true + } + }] + }, + + elements: { + rectangle: { + borderSkipped: 'left' + } + }, + + tooltips: { + mode: 'index', + axis: 'y' + } +}); + +var controller_horizontalBar = controller_bar.extend({ + /** + * @private + */ + _getValueScaleId: function() { + return this.getMeta().xAxisID; + }, + + /** + * @private + */ + _getIndexScaleId: function() { + return this.getMeta().yAxisID; + } +}); + +var valueOrDefault$5 = helpers$1.valueOrDefault; +var resolve$4 = helpers$1.options.resolve; +var isPointInArea = helpers$1.canvas._isPointInArea; + +core_defaults._set('line', { + showLines: true, + spanGaps: false, + + hover: { + mode: 'label' + }, + + scales: { + xAxes: [{ + type: 'category', + id: 'x-axis-0' + }], + yAxes: [{ + type: 'linear', + id: 'y-axis-0' + }] + } +}); + +function lineEnabled(dataset, options) { + return valueOrDefault$5(dataset.showLine, options.showLines); +} + +var controller_line = core_datasetController.extend({ + + datasetElementType: elements.Line, + + dataElementType: elements.Point, + + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var line = meta.dataset; + var points = meta.data || []; + var scale = me.getScaleForId(meta.yAxisID); + var dataset = me.getDataset(); + var showLine = lineEnabled(dataset, me.chart.options); + var i, ilen; + + // Update Line + if (showLine) { + // Compatibility: If the properties are defined with only the old name, use those values + if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { + dataset.lineTension = dataset.tension; + } + + // Utility + line._scale = scale; + line._datasetIndex = me.index; + // Data + line._children = points; + // Model + line._model = me._resolveLineOptions(line); + + line.pivot(); + } + + // Update Points + for (i = 0, ilen = points.length; i < ilen; ++i) { + me.updateElement(points[i], i, reset); + } + + if (showLine && line._model.tension !== 0) { + me.updateBezierControlPoints(); + } + + // Now pivot the point for animation + for (i = 0, ilen = points.length; i < ilen; ++i) { + points[i].pivot(); + } + }, + + updateElement: function(point, index, reset) { + var me = this; + var meta = me.getMeta(); + var custom = point.custom || {}; + var dataset = me.getDataset(); + var datasetIndex = me.index; + var value = dataset.data[index]; + var yScale = me.getScaleForId(meta.yAxisID); + var xScale = me.getScaleForId(meta.xAxisID); + var lineModel = meta.dataset._model; + var x, y; + + var options = me._resolvePointOptions(point, index); + + x = xScale.getPixelForValue(typeof value === 'object' ? value : NaN, index, datasetIndex); + y = reset ? yScale.getBasePixel() : me.calculatePointY(value, index, datasetIndex); + + // Utility + point._xScale = xScale; + point._yScale = yScale; + point._options = options; + point._datasetIndex = datasetIndex; + point._index = index; + + // Desired view properties + point._model = { + x: x, + y: y, + skip: custom.skip || isNaN(x) || isNaN(y), + // Appearance + radius: options.radius, + pointStyle: options.pointStyle, + rotation: options.rotation, + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + tension: valueOrDefault$5(custom.tension, lineModel ? lineModel.tension : 0), + steppedLine: lineModel ? lineModel.steppedLine : false, + // Tooltip + hitRadius: options.hitRadius + }; + }, + + /** + * @private + */ + _resolvePointOptions: function(element, index) { + var me = this; + var chart = me.chart; + var dataset = chart.data.datasets[me.index]; + var custom = element.custom || {}; + var options = chart.options.elements.point; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var ELEMENT_OPTIONS = { + backgroundColor: 'pointBackgroundColor', + borderColor: 'pointBorderColor', + borderWidth: 'pointBorderWidth', + hitRadius: 'pointHitRadius', + hoverBackgroundColor: 'pointHoverBackgroundColor', + hoverBorderColor: 'pointHoverBorderColor', + hoverBorderWidth: 'pointHoverBorderWidth', + hoverRadius: 'pointHoverRadius', + pointStyle: 'pointStyle', + radius: 'pointRadius', + rotation: 'pointRotation' + }; + var keys = Object.keys(ELEMENT_OPTIONS); + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve$4([ + custom[key], + dataset[ELEMENT_OPTIONS[key]], + dataset[key], + options[key] + ], context, index); + } + + return values; + }, + + /** + * @private + */ + _resolveLineOptions: function(element) { + var me = this; + var chart = me.chart; + var dataset = chart.data.datasets[me.index]; + var custom = element.custom || {}; + var options = chart.options; + var elementOptions = options.elements.line; + var values = {}; + var i, ilen, key; + + var keys = [ + 'backgroundColor', + 'borderWidth', + 'borderColor', + 'borderCapStyle', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'fill', + 'cubicInterpolationMode' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve$4([ + custom[key], + dataset[key], + elementOptions[key] + ]); + } + + // The default behavior of lines is to break at null values, according + // to https://github.com/chartjs/Chart.js/issues/2435#issuecomment-216718158 + // This option gives lines the ability to span gaps + values.spanGaps = valueOrDefault$5(dataset.spanGaps, options.spanGaps); + values.tension = valueOrDefault$5(dataset.lineTension, elementOptions.tension); + values.steppedLine = resolve$4([custom.steppedLine, dataset.steppedLine, elementOptions.stepped]); + + return values; + }, + + calculatePointY: function(value, index, datasetIndex) { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); + var sumPos = 0; + var sumNeg = 0; + var i, ds, dsMeta; + + if (yScale.options.stacked) { + for (i = 0; i < datasetIndex; i++) { + ds = chart.data.datasets[i]; + dsMeta = chart.getDatasetMeta(i); + if (dsMeta.type === 'line' && dsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) { + var stackedRightValue = Number(yScale.getRightValue(ds.data[index])); + if (stackedRightValue < 0) { + sumNeg += stackedRightValue || 0; + } else { + sumPos += stackedRightValue || 0; + } + } + } + + var rightValue = Number(yScale.getRightValue(value)); + if (rightValue < 0) { + return yScale.getPixelForValue(sumNeg + rightValue); + } + return yScale.getPixelForValue(sumPos + rightValue); + } + + return yScale.getPixelForValue(value); + }, + + updateBezierControlPoints: function() { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var lineModel = meta.dataset._model; + var area = chart.chartArea; + var points = meta.data || []; + var i, ilen, model, controlPoints; + + // Only consider points that are drawn in case the spanGaps option is used + if (lineModel.spanGaps) { + points = points.filter(function(pt) { + return !pt._model.skip; + }); + } + + function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); + } + + if (lineModel.cubicInterpolationMode === 'monotone') { + helpers$1.splineCurveMonotone(points); + } else { + for (i = 0, ilen = points.length; i < ilen; ++i) { + model = points[i]._model; + controlPoints = helpers$1.splineCurve( + helpers$1.previousItem(points, i)._model, + model, + helpers$1.nextItem(points, i)._model, + lineModel.tension + ); + model.controlPointPreviousX = controlPoints.previous.x; + model.controlPointPreviousY = controlPoints.previous.y; + model.controlPointNextX = controlPoints.next.x; + model.controlPointNextY = controlPoints.next.y; + } + } + + if (chart.options.elements.line.capBezierPoints) { + for (i = 0, ilen = points.length; i < ilen; ++i) { + model = points[i]._model; + if (isPointInArea(model, area)) { + if (i > 0 && isPointInArea(points[i - 1]._model, area)) { + model.controlPointPreviousX = capControlPoint(model.controlPointPreviousX, area.left, area.right); + model.controlPointPreviousY = capControlPoint(model.controlPointPreviousY, area.top, area.bottom); + } + if (i < points.length - 1 && isPointInArea(points[i + 1]._model, area)) { + model.controlPointNextX = capControlPoint(model.controlPointNextX, area.left, area.right); + model.controlPointNextY = capControlPoint(model.controlPointNextY, area.top, area.bottom); + } + } + } + } + }, + + draw: function() { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var points = meta.data || []; + var area = chart.chartArea; + var ilen = points.length; + var halfBorderWidth; + var i = 0; + + if (lineEnabled(me.getDataset(), chart.options)) { + halfBorderWidth = (meta.dataset._model.borderWidth || 0) / 2; + + helpers$1.canvas.clipArea(chart.ctx, { + left: area.left, + right: area.right, + top: area.top - halfBorderWidth, + bottom: area.bottom + halfBorderWidth + }); + + meta.dataset.draw(); + + helpers$1.canvas.unclipArea(chart.ctx); + } + + // Draw the points + for (; i < ilen; ++i) { + points[i].draw(area); + } + }, + + /** + * @protected + */ + setHoverStyle: function(point) { + var model = point._model; + var options = point._options; + var getHoverColor = helpers$1.getHoverColor; + + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + + model.backgroundColor = valueOrDefault$5(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$5(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$5(options.hoverBorderWidth, options.borderWidth); + model.radius = valueOrDefault$5(options.hoverRadius, options.radius); + }, +}); + +var resolve$5 = helpers$1.options.resolve; + +core_defaults._set('polarArea', { + scale: { + type: 'radialLinear', + angleLines: { + display: false + }, + gridLines: { + circular: true + }, + pointLabels: { + display: false + }, + ticks: { + beginAtZero: true + } + }, + + // Boolean - Whether to animate the rotation of the chart + animation: { + animateRotate: true, + animateScale: true + }, + + startAngle: -0.5 * Math.PI, + legendCallback: function(chart) { + var text = []; + text.push('
    '); + + var data = chart.data; + var datasets = data.datasets; + var labels = data.labels; + + if (datasets.length) { + for (var i = 0; i < datasets[0].data.length; ++i) { + text.push('
  • '); + if (labels[i]) { + text.push(labels[i]); + } + text.push('
  • '); + } + } + + text.push('
'); + return text.join(''); + }, + legend: { + labels: { + generateLabels: function(chart) { + var data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map(function(label, i) { + var meta = chart.getDatasetMeta(0); + var ds = data.datasets[0]; + var arc = meta.data[i]; + var custom = arc.custom || {}; + var arcOpts = chart.options.elements.arc; + var fill = resolve$5([custom.backgroundColor, ds.backgroundColor, arcOpts.backgroundColor], undefined, i); + var stroke = resolve$5([custom.borderColor, ds.borderColor, arcOpts.borderColor], undefined, i); + var bw = resolve$5([custom.borderWidth, ds.borderWidth, arcOpts.borderWidth], undefined, i); + + return { + text: label, + fillStyle: fill, + strokeStyle: stroke, + lineWidth: bw, + hidden: isNaN(ds.data[i]) || meta.data[i].hidden, + + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick: function(e, legendItem) { + var index = legendItem.index; + var chart = this.chart; + var i, ilen, meta; + + for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + meta.data[index].hidden = !meta.data[index].hidden; + } + + chart.update(); + } + }, + + // Need to override these to give a nice default + tooltips: { + callbacks: { + title: function() { + return ''; + }, + label: function(item, data) { + return data.labels[item.index] + ': ' + item.yLabel; + } + } + } +}); + +var controller_polarArea = core_datasetController.extend({ + + dataElementType: elements.Arc, + + linkScales: helpers$1.noop, + + update: function(reset) { + var me = this; + var dataset = me.getDataset(); + var meta = me.getMeta(); + var start = me.chart.options.startAngle || 0; + var starts = me._starts = []; + var angles = me._angles = []; + var arcs = meta.data; + var i, ilen, angle; + + me._updateRadius(); + + meta.count = me.countVisibleElements(); + + for (i = 0, ilen = dataset.data.length; i < ilen; i++) { + starts[i] = start; + angle = me._computeAngle(i); + angles[i] = angle; + start += angle; + } + + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + arcs[i]._options = me._resolveElementOptions(arcs[i], i); + me.updateElement(arcs[i], i, reset); + } + }, + + /** + * @private + */ + _updateRadius: function() { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var opts = chart.options; + var minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); + + chart.outerRadius = Math.max(minSize / 2, 0); + chart.innerRadius = Math.max(opts.cutoutPercentage ? (chart.outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); + chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); + + me.outerRadius = chart.outerRadius - (chart.radiusLength * me.index); + me.innerRadius = me.outerRadius - chart.radiusLength; + }, + + updateElement: function(arc, index, reset) { + var me = this; + var chart = me.chart; + var dataset = me.getDataset(); + var opts = chart.options; + var animationOpts = opts.animation; + var scale = chart.scale; + var labels = chart.data.labels; + + var centerX = scale.xCenter; + var centerY = scale.yCenter; + + // var negHalfPI = -0.5 * Math.PI; + var datasetStartAngle = opts.startAngle; + var distance = arc.hidden ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); + var startAngle = me._starts[index]; + var endAngle = startAngle + (arc.hidden ? 0 : me._angles[index]); + + var resetRadius = animationOpts.animateScale ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); + var options = arc._options || {}; + + helpers$1.extend(arc, { + // Utility + _datasetIndex: me.index, + _index: index, + _scale: scale, + + // Desired view properties + _model: { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + borderAlign: options.borderAlign, + x: centerX, + y: centerY, + innerRadius: 0, + outerRadius: reset ? resetRadius : distance, + startAngle: reset && animationOpts.animateRotate ? datasetStartAngle : startAngle, + endAngle: reset && animationOpts.animateRotate ? datasetStartAngle : endAngle, + label: helpers$1.valueAtIndexOrDefault(labels, index, labels[index]) + } + }); + + arc.pivot(); + }, + + countVisibleElements: function() { + var dataset = this.getDataset(); + var meta = this.getMeta(); + var count = 0; + + helpers$1.each(meta.data, function(element, index) { + if (!isNaN(dataset.data[index]) && !element.hidden) { + count++; + } + }); + + return count; + }, + + /** + * @protected + */ + setHoverStyle: function(arc) { + var model = arc._model; + var options = arc._options; + var getHoverColor = helpers$1.getHoverColor; + var valueOrDefault = helpers$1.valueOrDefault; + + arc.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + }; + + model.backgroundColor = valueOrDefault(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); + }, + + /** + * @private + */ + _resolveElementOptions: function(arc, index) { + var me = this; + var chart = me.chart; + var dataset = me.getDataset(); + var custom = arc.custom || {}; + var options = chart.options.elements.arc; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var keys = [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'borderAlign', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve$5([ + custom[key], + dataset[key], + options[key] + ], context, index); + } + + return values; + }, + + /** + * @private + */ + _computeAngle: function(index) { + var me = this; + var count = this.getMeta().count; + var dataset = me.getDataset(); + var meta = me.getMeta(); + + if (isNaN(dataset.data[index]) || meta.data[index].hidden) { + return 0; + } + + // Scriptable options + var context = { + chart: me.chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + return resolve$5([ + me.chart.options.elements.arc.angle, + (2 * Math.PI) / count + ], context, index); + } +}); + +core_defaults._set('pie', helpers$1.clone(core_defaults.doughnut)); +core_defaults._set('pie', { + cutoutPercentage: 0 +}); + +// Pie charts are Doughnut chart with different defaults +var controller_pie = controller_doughnut; + +var valueOrDefault$6 = helpers$1.valueOrDefault; +var resolve$6 = helpers$1.options.resolve; + +core_defaults._set('radar', { + scale: { + type: 'radialLinear' + }, + elements: { + line: { + tension: 0 // no bezier in radar + } + } +}); + +var controller_radar = core_datasetController.extend({ + + datasetElementType: elements.Line, + + dataElementType: elements.Point, + + linkScales: helpers$1.noop, + + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var line = meta.dataset; + var points = meta.data || []; + var scale = me.chart.scale; + var dataset = me.getDataset(); + var i, ilen; + + // Compatibility: If the properties are defined with only the old name, use those values + if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { + dataset.lineTension = dataset.tension; + } + + // Utility + line._scale = scale; + line._datasetIndex = me.index; + // Data + line._children = points; + line._loop = true; + // Model + line._model = me._resolveLineOptions(line); + + line.pivot(); + + // Update Points + for (i = 0, ilen = points.length; i < ilen; ++i) { + me.updateElement(points[i], i, reset); + } + + // Update bezier control points + me.updateBezierControlPoints(); + + // Now pivot the point for animation + for (i = 0, ilen = points.length; i < ilen; ++i) { + points[i].pivot(); + } + }, + + updateElement: function(point, index, reset) { + var me = this; + var custom = point.custom || {}; + var dataset = me.getDataset(); + var scale = me.chart.scale; + var pointPosition = scale.getPointPositionForValue(index, dataset.data[index]); + var options = me._resolvePointOptions(point, index); + var lineModel = me.getMeta().dataset._model; + var x = reset ? scale.xCenter : pointPosition.x; + var y = reset ? scale.yCenter : pointPosition.y; + + // Utility + point._scale = scale; + point._options = options; + point._datasetIndex = me.index; + point._index = index; + + // Desired view properties + point._model = { + x: x, // value not used in dataset scale, but we want a consistent API between scales + y: y, + skip: custom.skip || isNaN(x) || isNaN(y), + // Appearance + radius: options.radius, + pointStyle: options.pointStyle, + rotation: options.rotation, + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + tension: valueOrDefault$6(custom.tension, lineModel ? lineModel.tension : 0), + + // Tooltip + hitRadius: options.hitRadius + }; + }, + + /** + * @private + */ + _resolvePointOptions: function(element, index) { + var me = this; + var chart = me.chart; + var dataset = chart.data.datasets[me.index]; + var custom = element.custom || {}; + var options = chart.options.elements.point; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var ELEMENT_OPTIONS = { + backgroundColor: 'pointBackgroundColor', + borderColor: 'pointBorderColor', + borderWidth: 'pointBorderWidth', + hitRadius: 'pointHitRadius', + hoverBackgroundColor: 'pointHoverBackgroundColor', + hoverBorderColor: 'pointHoverBorderColor', + hoverBorderWidth: 'pointHoverBorderWidth', + hoverRadius: 'pointHoverRadius', + pointStyle: 'pointStyle', + radius: 'pointRadius', + rotation: 'pointRotation' + }; + var keys = Object.keys(ELEMENT_OPTIONS); + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve$6([ + custom[key], + dataset[ELEMENT_OPTIONS[key]], + dataset[key], + options[key] + ], context, index); + } + + return values; + }, + + /** + * @private + */ + _resolveLineOptions: function(element) { + var me = this; + var chart = me.chart; + var dataset = chart.data.datasets[me.index]; + var custom = element.custom || {}; + var options = chart.options.elements.line; + var values = {}; + var i, ilen, key; + + var keys = [ + 'backgroundColor', + 'borderWidth', + 'borderColor', + 'borderCapStyle', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'fill' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve$6([ + custom[key], + dataset[key], + options[key] + ]); + } + + values.tension = valueOrDefault$6(dataset.lineTension, options.tension); + + return values; + }, + + updateBezierControlPoints: function() { + var me = this; + var meta = me.getMeta(); + var area = me.chart.chartArea; + var points = meta.data || []; + var i, ilen, model, controlPoints; + + function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); + } + + for (i = 0, ilen = points.length; i < ilen; ++i) { + model = points[i]._model; + controlPoints = helpers$1.splineCurve( + helpers$1.previousItem(points, i, true)._model, + model, + helpers$1.nextItem(points, i, true)._model, + model.tension + ); + + // Prevent the bezier going outside of the bounds of the graph + model.controlPointPreviousX = capControlPoint(controlPoints.previous.x, area.left, area.right); + model.controlPointPreviousY = capControlPoint(controlPoints.previous.y, area.top, area.bottom); + model.controlPointNextX = capControlPoint(controlPoints.next.x, area.left, area.right); + model.controlPointNextY = capControlPoint(controlPoints.next.y, area.top, area.bottom); + } + }, + + setHoverStyle: function(point) { + var model = point._model; + var options = point._options; + var getHoverColor = helpers$1.getHoverColor; + + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + + model.backgroundColor = valueOrDefault$6(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$6(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$6(options.hoverBorderWidth, options.borderWidth); + model.radius = valueOrDefault$6(options.hoverRadius, options.radius); + } +}); + +core_defaults._set('scatter', { + hover: { + mode: 'single' + }, + + scales: { + xAxes: [{ + id: 'x-axis-1', // need an ID so datasets can reference the scale + type: 'linear', // scatter should not use a category axis + position: 'bottom' + }], + yAxes: [{ + id: 'y-axis-1', + type: 'linear', + position: 'left' + }] + }, + + showLines: false, + + tooltips: { + callbacks: { + title: function() { + return ''; // doesn't make sense for scatter since data are formatted as a point + }, + label: function(item) { + return '(' + item.xLabel + ', ' + item.yLabel + ')'; + } + } + } +}); + +// Scatter charts use line controllers +var controller_scatter = controller_line; + +// NOTE export a map in which the key represents the controller type, not +// the class, and so must be CamelCase in order to be correctly retrieved +// by the controller in core.controller.js (`controllers[meta.type]`). + +var controllers = { + bar: controller_bar, + bubble: controller_bubble, + doughnut: controller_doughnut, + horizontalBar: controller_horizontalBar, + line: controller_line, + polarArea: controller_polarArea, + pie: controller_pie, + radar: controller_radar, + scatter: controller_scatter +}; + +/** + * Helper function to get relative position for an event + * @param {Event|IEvent} event - The event to get the position for + * @param {Chart} chart - The chart + * @returns {object} the event position + */ +function getRelativePosition(e, chart) { + if (e.native) { + return { + x: e.x, + y: e.y + }; + } + + return helpers$1.getRelativePosition(e, chart); +} + +/** + * Helper function to traverse all of the visible elements in the chart + * @param {Chart} chart - the chart + * @param {function} handler - the callback to execute for each visible item + */ +function parseVisibleItems(chart, handler) { + var datasets = chart.data.datasets; + var meta, i, j, ilen, jlen; + + for (i = 0, ilen = datasets.length; i < ilen; ++i) { + if (!chart.isDatasetVisible(i)) { + continue; + } + + meta = chart.getDatasetMeta(i); + for (j = 0, jlen = meta.data.length; j < jlen; ++j) { + var element = meta.data[j]; + if (!element._view.skip) { + handler(element); + } + } + } +} + +/** + * Helper function to get the items that intersect the event position + * @param {ChartElement[]} items - elements to filter + * @param {object} position - the point to be nearest to + * @return {ChartElement[]} the nearest items + */ +function getIntersectItems(chart, position) { + var elements = []; + + parseVisibleItems(chart, function(element) { + if (element.inRange(position.x, position.y)) { + elements.push(element); + } + }); + + return elements; +} + +/** + * Helper function to get the items nearest to the event position considering all visible items in teh chart + * @param {Chart} chart - the chart to look at elements from + * @param {object} position - the point to be nearest to + * @param {boolean} intersect - if true, only consider items that intersect the position + * @param {function} distanceMetric - function to provide the distance between points + * @return {ChartElement[]} the nearest items + */ +function getNearestItems(chart, position, intersect, distanceMetric) { + var minDistance = Number.POSITIVE_INFINITY; + var nearestItems = []; + + parseVisibleItems(chart, function(element) { + if (intersect && !element.inRange(position.x, position.y)) { + return; + } + + var center = element.getCenterPoint(); + var distance = distanceMetric(position, center); + if (distance < minDistance) { + nearestItems = [element]; + minDistance = distance; + } else if (distance === minDistance) { + // Can have multiple items at the same distance in which case we sort by size + nearestItems.push(element); + } + }); + + return nearestItems; +} + +/** + * Get a distance metric function for two points based on the + * axis mode setting + * @param {string} axis - the axis mode. x|y|xy + */ +function getDistanceMetricForAxis(axis) { + var useX = axis.indexOf('x') !== -1; + var useY = axis.indexOf('y') !== -1; + + return function(pt1, pt2) { + var deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0; + var deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0; + return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); + }; +} + +function indexMode(chart, e, options) { + var position = getRelativePosition(e, chart); + // Default axis for index mode is 'x' to match old behaviour + options.axis = options.axis || 'x'; + var distanceMetric = getDistanceMetricForAxis(options.axis); + var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false, distanceMetric); + var elements = []; + + if (!items.length) { + return []; + } + + chart.data.datasets.forEach(function(dataset, datasetIndex) { + if (chart.isDatasetVisible(datasetIndex)) { + var meta = chart.getDatasetMeta(datasetIndex); + var element = meta.data[items[0]._index]; + + // don't count items that are skipped (null data) + if (element && !element._view.skip) { + elements.push(element); + } + } + }); + + return elements; +} + +/** + * @interface IInteractionOptions + */ +/** + * If true, only consider items that intersect the point + * @name IInterfaceOptions#boolean + * @type Boolean + */ + +/** + * Contains interaction related functions + * @namespace Chart.Interaction + */ +var core_interaction = { + // Helper function for different modes + modes: { + single: function(chart, e) { + var position = getRelativePosition(e, chart); + var elements = []; + + parseVisibleItems(chart, function(element) { + if (element.inRange(position.x, position.y)) { + elements.push(element); + return elements; + } + }); + + return elements.slice(0, 1); + }, + + /** + * @function Chart.Interaction.modes.label + * @deprecated since version 2.4.0 + * @todo remove at version 3 + * @private + */ + label: indexMode, + + /** + * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item + * @function Chart.Interaction.modes.index + * @since v2.4.0 + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use during interaction + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + index: indexMode, + + /** + * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect is false, we find the nearest item and return the items in that dataset + * @function Chart.Interaction.modes.dataset + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use during interaction + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + dataset: function(chart, e, options) { + var position = getRelativePosition(e, chart); + options.axis = options.axis || 'xy'; + var distanceMetric = getDistanceMetricForAxis(options.axis); + var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false, distanceMetric); + + if (items.length > 0) { + items = chart.getDatasetMeta(items[0]._datasetIndex).data; + } + + return items; + }, + + /** + * @function Chart.Interaction.modes.x-axis + * @deprecated since version 2.4.0. Use index mode and intersect == true + * @todo remove at version 3 + * @private + */ + 'x-axis': function(chart, e) { + return indexMode(chart, e, {intersect: false}); + }, + + /** + * Point mode returns all elements that hit test based on the event position + * of the event + * @function Chart.Interaction.modes.intersect + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + point: function(chart, e) { + var position = getRelativePosition(e, chart); + return getIntersectItems(chart, position); + }, + + /** + * nearest mode returns the element closest to the point + * @function Chart.Interaction.modes.intersect + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + nearest: function(chart, e, options) { + var position = getRelativePosition(e, chart); + options.axis = options.axis || 'xy'; + var distanceMetric = getDistanceMetricForAxis(options.axis); + return getNearestItems(chart, position, options.intersect, distanceMetric); + }, + + /** + * x mode returns the elements that hit-test at the current x coordinate + * @function Chart.Interaction.modes.x + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + x: function(chart, e, options) { + var position = getRelativePosition(e, chart); + var items = []; + var intersectsItem = false; + + parseVisibleItems(chart, function(element) { + if (element.inXRange(position.x)) { + items.push(element); + } + + if (element.inRange(position.x, position.y)) { + intersectsItem = true; + } + }); + + // If we want to trigger on an intersect and we don't have any items + // that intersect the position, return nothing + if (options.intersect && !intersectsItem) { + items = []; + } + return items; + }, + + /** + * y mode returns the elements that hit-test at the current y coordinate + * @function Chart.Interaction.modes.y + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + y: function(chart, e, options) { + var position = getRelativePosition(e, chart); + var items = []; + var intersectsItem = false; + + parseVisibleItems(chart, function(element) { + if (element.inYRange(position.y)) { + items.push(element); + } + + if (element.inRange(position.x, position.y)) { + intersectsItem = true; + } + }); + + // If we want to trigger on an intersect and we don't have any items + // that intersect the position, return nothing + if (options.intersect && !intersectsItem) { + items = []; + } + return items; + } + } +}; + +function filterByPosition(array, position) { + return helpers$1.where(array, function(v) { + return v.position === position; + }); +} + +function sortByWeight(array, reverse) { + array.forEach(function(v, i) { + v._tmpIndex_ = i; + return v; + }); + array.sort(function(a, b) { + var v0 = reverse ? b : a; + var v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0._tmpIndex_ - v1._tmpIndex_ : + v0.weight - v1.weight; + }); + array.forEach(function(v) { + delete v._tmpIndex_; + }); +} + +function findMaxPadding(boxes) { + var top = 0; + var left = 0; + var bottom = 0; + var right = 0; + helpers$1.each(boxes, function(box) { + if (box.getPadding) { + var boxPadding = box.getPadding(); + top = Math.max(top, boxPadding.top); + left = Math.max(left, boxPadding.left); + bottom = Math.max(bottom, boxPadding.bottom); + right = Math.max(right, boxPadding.right); + } + }); + return { + top: top, + left: left, + bottom: bottom, + right: right + }; +} + +function addSizeByPosition(boxes, size) { + helpers$1.each(boxes, function(box) { + size[box.position] += box.isHorizontal() ? box.height : box.width; + }); +} + +core_defaults._set('global', { + layout: { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + } +}); + +/** + * @interface ILayoutItem + * @prop {string} position - The position of the item in the chart layout. Possible values are + * 'left', 'top', 'right', 'bottom', and 'chartArea' + * @prop {number} weight - The weight used to sort the item. Higher weights are further away from the chart area + * @prop {boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down + * @prop {function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) + * @prop {function} update - Takes two parameters: width and height. Returns size of item + * @prop {function} getPadding - Returns an object with padding on the edges + * @prop {number} width - Width of item. Must be valid after update() + * @prop {number} height - Height of item. Must be valid after update() + * @prop {number} left - Left edge of the item. Set by layout system and cannot be used in update + * @prop {number} top - Top edge of the item. Set by layout system and cannot be used in update + * @prop {number} right - Right edge of the item. Set by layout system and cannot be used in update + * @prop {number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update + */ + +// The layout service is very self explanatory. It's responsible for the layout within a chart. +// Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need +// It is this service's responsibility of carrying out that layout. +var core_layouts = { + defaults: {}, + + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {ILayoutItem} item - the item to add to be layed out + */ + addBox: function(chart, item) { + if (!chart.boxes) { + chart.boxes = []; + } + + // initialize item with default values + item.fullWidth = item.fullWidth || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + + chart.boxes.push(item); + }, + + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {ILayoutItem} layoutItem - the item to remove from the layout + */ + removeBox: function(chart, layoutItem) { + var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; + if (index !== -1) { + chart.boxes.splice(index, 1); + } + }, + + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {ILayoutItem} item - the item to configure with the given options + * @param {object} options - the new item options. + */ + configure: function(chart, item, options) { + var props = ['fullWidth', 'position', 'weight']; + var ilen = props.length; + var i = 0; + var prop; + + for (; i < ilen; ++i) { + prop = props[i]; + if (options.hasOwnProperty(prop)) { + item[prop] = options[prop]; + } + } + }, + + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {number} width - the width to fit into + * @param {number} height - the height to fit into + */ + update: function(chart, width, height) { + if (!chart) { + return; + } + + var layoutOptions = chart.options.layout || {}; + var padding = helpers$1.options.toPadding(layoutOptions.padding); + var leftPadding = padding.left; + var rightPadding = padding.right; + var topPadding = padding.top; + var bottomPadding = padding.bottom; + + var leftBoxes = filterByPosition(chart.boxes, 'left'); + var rightBoxes = filterByPosition(chart.boxes, 'right'); + var topBoxes = filterByPosition(chart.boxes, 'top'); + var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); + var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); + + // Sort boxes by weight. A higher weight is further away from the chart area + sortByWeight(leftBoxes, true); + sortByWeight(rightBoxes, false); + sortByWeight(topBoxes, true); + sortByWeight(bottomBoxes, false); + + var verticalBoxes = leftBoxes.concat(rightBoxes); + var horizontalBoxes = topBoxes.concat(bottomBoxes); + var outerBoxes = verticalBoxes.concat(horizontalBoxes); + + // Essentially we now have any number of boxes on each of the 4 sides. + // Our canvas looks like the following. + // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and + // B1 is the bottom axis + // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays + // These locations are single-box locations only, when trying to register a chartArea location that is already taken, + // an error will be thrown. + // + // |----------------------------------------------------| + // | T1 (Full Width) | + // |----------------------------------------------------| + // | | | T2 | | + // | |----|-------------------------------------|----| + // | | | C1 | | C2 | | + // | | |----| |----| | + // | | | | | + // | L1 | L2 | ChartArea (C0) | R1 | + // | | | | | + // | | |----| |----| | + // | | | C3 | | C4 | | + // | |----|-------------------------------------|----| + // | | | B1 | | + // |----------------------------------------------------| + // | B2 (Full Width) | + // |----------------------------------------------------| + // + // What we do to find the best sizing, we do the following + // 1. Determine the minimum size of the chart area. + // 2. Split the remaining width equally between each vertical axis + // 3. Split the remaining height equally between each horizontal axis + // 4. Give each layout the maximum size it can be. The layout will return it's minimum size + // 5. Adjust the sizes of each axis based on it's minimum reported size. + // 6. Refit each axis + // 7. Position each axis in the final location + // 8. Tell the chart the final location of the chart area + // 9. Tell any axes that overlay the chart area the positions of the chart area + + // Step 1 + var chartWidth = width - leftPadding - rightPadding; + var chartHeight = height - topPadding - bottomPadding; + var chartAreaWidth = chartWidth / 2; // min 50% + + // Step 2 + var verticalBoxWidth = (width - chartAreaWidth) / verticalBoxes.length; + + // Step 3 + // TODO re-limit horizontal axis height (this limit has affected only padding calculation since PR 1837) + // var horizontalBoxHeight = (height - chartAreaHeight) / horizontalBoxes.length; + + // Step 4 + var maxChartAreaWidth = chartWidth; + var maxChartAreaHeight = chartHeight; + var outerBoxSizes = {top: topPadding, left: leftPadding, bottom: bottomPadding, right: rightPadding}; + var minBoxSizes = []; + var maxPadding; + + function getMinimumBoxSize(box) { + var minSize; + var isHorizontal = box.isHorizontal(); + + if (isHorizontal) { + minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2); + maxChartAreaHeight -= minSize.height; + } else { + minSize = box.update(verticalBoxWidth, maxChartAreaHeight); + maxChartAreaWidth -= minSize.width; + } + + minBoxSizes.push({ + horizontal: isHorizontal, + width: minSize.width, + box: box, + }); + } + + helpers$1.each(outerBoxes, getMinimumBoxSize); + + // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) + maxPadding = findMaxPadding(outerBoxes); + + // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could + // be if the axes are drawn at their minimum sizes. + // Steps 5 & 6 + + // Function to fit a box + function fitBox(box) { + var minBoxSize = helpers$1.findNextWhere(minBoxSizes, function(minBox) { + return minBox.box === box; + }); + + if (minBoxSize) { + if (minBoxSize.horizontal) { + var scaleMargin = { + left: Math.max(outerBoxSizes.left, maxPadding.left), + right: Math.max(outerBoxSizes.right, maxPadding.right), + top: 0, + bottom: 0 + }; + + // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends + // on the margin. Sometimes they need to increase in size slightly + box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); + } else { + box.update(minBoxSize.width, maxChartAreaHeight); + } + } + } + + // Update, and calculate the left and right margins for the horizontal boxes + helpers$1.each(verticalBoxes, fitBox); + addSizeByPosition(verticalBoxes, outerBoxSizes); + + // Set the Left and Right margins for the horizontal boxes + helpers$1.each(horizontalBoxes, fitBox); + addSizeByPosition(horizontalBoxes, outerBoxSizes); + + function finalFitVerticalBox(box) { + var minBoxSize = helpers$1.findNextWhere(minBoxSizes, function(minSize) { + return minSize.box === box; + }); + + var scaleMargin = { + left: 0, + right: 0, + top: outerBoxSizes.top, + bottom: outerBoxSizes.bottom + }; + + if (minBoxSize) { + box.update(minBoxSize.width, maxChartAreaHeight, scaleMargin); + } + } + + // Let the left layout know the final margin + helpers$1.each(verticalBoxes, finalFitVerticalBox); + + // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) + outerBoxSizes = {top: topPadding, left: leftPadding, bottom: bottomPadding, right: rightPadding}; + addSizeByPosition(outerBoxes, outerBoxSizes); + + // We may be adding some padding to account for rotated x axis labels + var leftPaddingAddition = Math.max(maxPadding.left - outerBoxSizes.left, 0); + outerBoxSizes.left += leftPaddingAddition; + outerBoxSizes.right += Math.max(maxPadding.right - outerBoxSizes.right, 0); + + var topPaddingAddition = Math.max(maxPadding.top - outerBoxSizes.top, 0); + outerBoxSizes.top += topPaddingAddition; + outerBoxSizes.bottom += Math.max(maxPadding.bottom - outerBoxSizes.bottom, 0); + + // Figure out if our chart area changed. This would occur if the dataset layout label rotation + // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do + // without calling `fit` again + var newMaxChartAreaHeight = height - outerBoxSizes.top - outerBoxSizes.bottom; + var newMaxChartAreaWidth = width - outerBoxSizes.left - outerBoxSizes.right; + + if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { + helpers$1.each(verticalBoxes, function(box) { + box.height = newMaxChartAreaHeight; + }); + + helpers$1.each(horizontalBoxes, function(box) { + if (!box.fullWidth) { + box.width = newMaxChartAreaWidth; + } + }); + + maxChartAreaHeight = newMaxChartAreaHeight; + maxChartAreaWidth = newMaxChartAreaWidth; + } + + // Step 7 - Position the boxes + var left = leftPadding + leftPaddingAddition; + var top = topPadding + topPaddingAddition; + + function placeBox(box) { + if (box.isHorizontal()) { + box.left = box.fullWidth ? leftPadding : outerBoxSizes.left; + box.right = box.fullWidth ? width - rightPadding : outerBoxSizes.left + maxChartAreaWidth; + box.top = top; + box.bottom = top + box.height; + + // Move to next point + top = box.bottom; + + } else { + + box.left = left; + box.right = left + box.width; + box.top = outerBoxSizes.top; + box.bottom = outerBoxSizes.top + maxChartAreaHeight; + + // Move to next point + left = box.right; + } + } + + helpers$1.each(leftBoxes.concat(topBoxes), placeBox); + + // Account for chart width and height + left += maxChartAreaWidth; + top += maxChartAreaHeight; + + helpers$1.each(rightBoxes, placeBox); + helpers$1.each(bottomBoxes, placeBox); + + // Step 8 + chart.chartArea = { + left: outerBoxSizes.left, + top: outerBoxSizes.top, + right: outerBoxSizes.left + maxChartAreaWidth, + bottom: outerBoxSizes.top + maxChartAreaHeight + }; + + // Step 9 + helpers$1.each(chartAreaBoxes, function(box) { + box.left = chart.chartArea.left; + box.top = chart.chartArea.top; + box.right = chart.chartArea.right; + box.bottom = chart.chartArea.bottom; + + box.update(maxChartAreaWidth, maxChartAreaHeight); + }); + } +}; + +/** + * Platform fallback implementation (minimal). + * @see https://github.com/chartjs/Chart.js/pull/4591#issuecomment-319575939 + */ + +var platform_basic = { + acquireContext: function(item) { + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + + return item && item.getContext('2d') || null; + } +}; + +var platform_dom = "/*\n * DOM element rendering detection\n * https://davidwalsh.name/detect-node-insertion\n */\n@keyframes chartjs-render-animation {\n\tfrom { opacity: 0.99; }\n\tto { opacity: 1; }\n}\n\n.chartjs-render-monitor {\n\tanimation: chartjs-render-animation 0.001s;\n}\n\n/*\n * DOM element resizing detection\n * https://github.com/marcj/css-element-queries\n */\n.chartjs-size-monitor,\n.chartjs-size-monitor-expand,\n.chartjs-size-monitor-shrink {\n\tposition: absolute;\n\tdirection: ltr;\n\tleft: 0;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\toverflow: hidden;\n\tpointer-events: none;\n\tvisibility: hidden;\n\tz-index: -1;\n}\n\n.chartjs-size-monitor-expand > div {\n\tposition: absolute;\n\twidth: 1000000px;\n\theight: 1000000px;\n\tleft: 0;\n\ttop: 0;\n}\n\n.chartjs-size-monitor-shrink > div {\n\tposition: absolute;\n\twidth: 200%;\n\theight: 200%;\n\tleft: 0;\n\ttop: 0;\n}\n"; + +var platform_dom$1 = /*#__PURE__*/Object.freeze({ +default: platform_dom +}); + +function getCjsExportFromNamespace (n) { + return n && n.default || n; +} + +var stylesheet = getCjsExportFromNamespace(platform_dom$1); + +var EXPANDO_KEY = '$chartjs'; +var CSS_PREFIX = 'chartjs-'; +var CSS_SIZE_MONITOR = CSS_PREFIX + 'size-monitor'; +var CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor'; +var CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation'; +var ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart']; + +/** + * DOM event types -> Chart.js event types. + * Note: only events with different types are mapped. + * @see https://developer.mozilla.org/en-US/docs/Web/Events + */ +var EVENT_TYPES = { + touchstart: 'mousedown', + touchmove: 'mousemove', + touchend: 'mouseup', + pointerenter: 'mouseenter', + pointerdown: 'mousedown', + pointermove: 'mousemove', + pointerup: 'mouseup', + pointerleave: 'mouseout', + pointerout: 'mouseout' +}; + +/** + * The "used" size is the final value of a dimension property after all calculations have + * been performed. This method uses the computed style of `element` but returns undefined + * if the computed style is not expressed in pixels. That can happen in some cases where + * `element` has a size relative to its parent and this last one is not yet displayed, + * for example because of `display: none` on a parent node. + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + * @returns {number} Size in pixels or undefined if unknown. + */ +function readUsedSize(element, property) { + var value = helpers$1.getStyle(element, property); + var matches = value && value.match(/^(\d+)(\.\d+)?px$/); + return matches ? Number(matches[1]) : undefined; +} + +/** + * Initializes the canvas style and render size without modifying the canvas display size, + * since responsiveness is handled by the controller.resize() method. The config is used + * to determine the aspect ratio to apply in case no explicit height has been specified. + */ +function initCanvas(canvas, config) { + var style = canvas.style; + + // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it + // returns null or '' if no explicit value has been set to the canvas attribute. + var renderHeight = canvas.getAttribute('height'); + var renderWidth = canvas.getAttribute('width'); + + // Chart.js modifies some canvas values that we want to restore on destroy + canvas[EXPANDO_KEY] = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + + // Force canvas to display as block to avoid extra space caused by inline + // elements, which would interfere with the responsive resize process. + // https://github.com/chartjs/Chart.js/issues/2538 + style.display = style.display || 'block'; + + if (renderWidth === null || renderWidth === '') { + var displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + + if (renderHeight === null || renderHeight === '') { + if (canvas.style.height === '') { + // If no explicit render height and style height, let's apply the aspect ratio, + // which one can be specified by the user but also by charts as default option + // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. + canvas.height = canvas.width / (config.options.aspectRatio || 2); + } else { + var displayHeight = readUsedSize(canvas, 'height'); + if (displayWidth !== undefined) { + canvas.height = displayHeight; + } + } + } + + return canvas; +} + +/** + * Detects support for options object argument in addEventListener. + * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support + * @private + */ +var supportsEventListenerOptions = (function() { + var supports = false; + try { + var options = Object.defineProperty({}, 'passive', { + // eslint-disable-next-line getter-return + get: function() { + supports = true; + } + }); + window.addEventListener('e', null, options); + } catch (e) { + // continue regardless of error + } + return supports; +}()); + +// Default passive to true as expected by Chrome for 'touchstart' and 'touchend' events. +// https://github.com/chartjs/Chart.js/issues/4287 +var eventListenerOptions = supportsEventListenerOptions ? {passive: true} : false; + +function addListener(node, type, listener) { + node.addEventListener(type, listener, eventListenerOptions); +} + +function removeListener(node, type, listener) { + node.removeEventListener(type, listener, eventListenerOptions); +} + +function createEvent(type, chart, x, y, nativeEvent) { + return { + type: type, + chart: chart, + native: nativeEvent || null, + x: x !== undefined ? x : null, + y: y !== undefined ? y : null, + }; +} + +function fromNativeEvent(event, chart) { + var type = EVENT_TYPES[event.type] || event.type; + var pos = helpers$1.getRelativePosition(event, chart); + return createEvent(type, chart, pos.x, pos.y, event); +} + +function throttled(fn, thisArg) { + var ticking = false; + var args = []; + + return function() { + args = Array.prototype.slice.call(arguments); + thisArg = thisArg || this; + + if (!ticking) { + ticking = true; + helpers$1.requestAnimFrame.call(window, function() { + ticking = false; + fn.apply(thisArg, args); + }); + } + }; +} + +function createDiv(cls) { + var el = document.createElement('div'); + el.className = cls || ''; + return el; +} + +// Implementation based on https://github.com/marcj/css-element-queries +function createResizer(handler) { + var maxSize = 1000000; + + // NOTE(SB) Don't use innerHTML because it could be considered unsafe. + // https://github.com/chartjs/Chart.js/issues/5902 + var resizer = createDiv(CSS_SIZE_MONITOR); + var expand = createDiv(CSS_SIZE_MONITOR + '-expand'); + var shrink = createDiv(CSS_SIZE_MONITOR + '-shrink'); + + expand.appendChild(createDiv()); + shrink.appendChild(createDiv()); + + resizer.appendChild(expand); + resizer.appendChild(shrink); + resizer._reset = function() { + expand.scrollLeft = maxSize; + expand.scrollTop = maxSize; + shrink.scrollLeft = maxSize; + shrink.scrollTop = maxSize; + }; + + var onScroll = function() { + resizer._reset(); + handler(); + }; + + addListener(expand, 'scroll', onScroll.bind(expand, 'expand')); + addListener(shrink, 'scroll', onScroll.bind(shrink, 'shrink')); + + return resizer; +} + +// https://davidwalsh.name/detect-node-insertion +function watchForRender(node, handler) { + var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {}); + var proxy = expando.renderProxy = function(e) { + if (e.animationName === CSS_RENDER_ANIMATION) { + handler(); + } + }; + + helpers$1.each(ANIMATION_START_EVENTS, function(type) { + addListener(node, type, proxy); + }); + + // #4737: Chrome might skip the CSS animation when the CSS_RENDER_MONITOR class + // is removed then added back immediately (same animation frame?). Accessing the + // `offsetParent` property will force a reflow and re-evaluate the CSS animation. + // https://gist.github.com/paulirish/5d52fb081b3570c81e3a#box-metrics + // https://github.com/chartjs/Chart.js/issues/4737 + expando.reflow = !!node.offsetParent; + + node.classList.add(CSS_RENDER_MONITOR); +} + +function unwatchForRender(node) { + var expando = node[EXPANDO_KEY] || {}; + var proxy = expando.renderProxy; + + if (proxy) { + helpers$1.each(ANIMATION_START_EVENTS, function(type) { + removeListener(node, type, proxy); + }); + + delete expando.renderProxy; + } + + node.classList.remove(CSS_RENDER_MONITOR); +} + +function addResizeListener(node, listener, chart) { + var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {}); + + // Let's keep track of this added resizer and thus avoid DOM query when removing it. + var resizer = expando.resizer = createResizer(throttled(function() { + if (expando.resizer) { + var container = chart.options.maintainAspectRatio && node.parentNode; + var w = container ? container.clientWidth : 0; + listener(createEvent('resize', chart)); + if (container && container.clientWidth < w && chart.canvas) { + // If the container size shrank during chart resize, let's assume + // scrollbar appeared. So we resize again with the scrollbar visible - + // effectively making chart smaller and the scrollbar hidden again. + // Because we are inside `throttled`, and currently `ticking`, scroll + // events are ignored during this whole 2 resize process. + // If we assumed wrong and something else happened, we are resizing + // twice in a frame (potential performance issue) + listener(createEvent('resize', chart)); + } + } + })); + + // The resizer needs to be attached to the node parent, so we first need to be + // sure that `node` is attached to the DOM before injecting the resizer element. + watchForRender(node, function() { + if (expando.resizer) { + var container = node.parentNode; + if (container && container !== resizer.parentNode) { + container.insertBefore(resizer, container.firstChild); + } + + // The container size might have changed, let's reset the resizer state. + resizer._reset(); + } + }); +} + +function removeResizeListener(node) { + var expando = node[EXPANDO_KEY] || {}; + var resizer = expando.resizer; + + delete expando.resizer; + unwatchForRender(node); + + if (resizer && resizer.parentNode) { + resizer.parentNode.removeChild(resizer); + } +} + +function injectCSS(platform, css) { + // https://stackoverflow.com/q/3922139 + var style = platform._style || document.createElement('style'); + if (!platform._style) { + platform._style = style; + css = '/* Chart.js */\n' + css; + style.setAttribute('type', 'text/css'); + document.getElementsByTagName('head')[0].appendChild(style); + } + + style.appendChild(document.createTextNode(css)); +} + +var platform_dom$2 = { + /** + * When `true`, prevents the automatic injection of the stylesheet required to + * correctly detect when the chart is added to the DOM and then resized. This + * switch has been added to allow external stylesheet (`dist/Chart(.min)?.js`) + * to be manually imported to make this library compatible with any CSP. + * See https://github.com/chartjs/Chart.js/issues/5208 + */ + disableCSSInjection: false, + + /** + * This property holds whether this platform is enabled for the current environment. + * Currently used by platform.js to select the proper implementation. + * @private + */ + _enabled: typeof window !== 'undefined' && typeof document !== 'undefined', + + /** + * @private + */ + _ensureLoaded: function() { + if (this._loaded) { + return; + } + + this._loaded = true; + + // https://github.com/chartjs/Chart.js/issues/5208 + if (!this.disableCSSInjection) { + injectCSS(this, stylesheet); + } + }, + + acquireContext: function(item, config) { + if (typeof item === 'string') { + item = document.getElementById(item); + } else if (item.length) { + // Support for array based queries (such as jQuery) + item = item[0]; + } + + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + + // To prevent canvas fingerprinting, some add-ons undefine the getContext + // method, for example: https://github.com/kkapsner/CanvasBlocker + // https://github.com/chartjs/Chart.js/issues/2807 + var context = item && item.getContext && item.getContext('2d'); + + // Load platform resources on first chart creation, to make possible to change + // platform options after importing the library (e.g. `disableCSSInjection`). + this._ensureLoaded(); + + // `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the item is + // inside an iframe or when running in a protected environment. We could guess the + // types from their toString() value but let's keep things flexible and assume it's + // a sufficient condition if the item has a context2D which has item as `canvas`. + // https://github.com/chartjs/Chart.js/issues/3887 + // https://github.com/chartjs/Chart.js/issues/4102 + // https://github.com/chartjs/Chart.js/issues/4152 + if (context && context.canvas === item) { + initCanvas(item, config); + return context; + } + + return null; + }, + + releaseContext: function(context) { + var canvas = context.canvas; + if (!canvas[EXPANDO_KEY]) { + return; + } + + var initial = canvas[EXPANDO_KEY].initial; + ['height', 'width'].forEach(function(prop) { + var value = initial[prop]; + if (helpers$1.isNullOrUndef(value)) { + canvas.removeAttribute(prop); + } else { + canvas.setAttribute(prop, value); + } + }); + + helpers$1.each(initial.style || {}, function(value, key) { + canvas.style[key] = value; + }); + + // The canvas render size might have been changed (and thus the state stack discarded), + // we can't use save() and restore() to restore the initial state. So make sure that at + // least the canvas context is reset to the default state by setting the canvas width. + // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html + // eslint-disable-next-line no-self-assign + canvas.width = canvas.width; + + delete canvas[EXPANDO_KEY]; + }, + + addEventListener: function(chart, type, listener) { + var canvas = chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + addResizeListener(canvas, listener, chart); + return; + } + + var expando = listener[EXPANDO_KEY] || (listener[EXPANDO_KEY] = {}); + var proxies = expando.proxies || (expando.proxies = {}); + var proxy = proxies[chart.id + '_' + type] = function(event) { + listener(fromNativeEvent(event, chart)); + }; + + addListener(canvas, type, proxy); + }, + + removeEventListener: function(chart, type, listener) { + var canvas = chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + removeResizeListener(canvas); + return; + } + + var expando = listener[EXPANDO_KEY] || {}; + var proxies = expando.proxies || {}; + var proxy = proxies[chart.id + '_' + type]; + if (!proxy) { + return; + } + + removeListener(canvas, type, proxy); + } +}; + +// DEPRECATIONS + +/** + * Provided for backward compatibility, use EventTarget.addEventListener instead. + * EventTarget.addEventListener compatibility: Chrome, Opera 7, Safari, FF1.5+, IE9+ + * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + * @function Chart.helpers.addEvent + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ +helpers$1.addEvent = addListener; + +/** + * Provided for backward compatibility, use EventTarget.removeEventListener instead. + * EventTarget.removeEventListener compatibility: Chrome, Opera 7, Safari, FF1.5+, IE9+ + * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener + * @function Chart.helpers.removeEvent + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ +helpers$1.removeEvent = removeListener; + +// @TODO Make possible to select another platform at build time. +var implementation = platform_dom$2._enabled ? platform_dom$2 : platform_basic; + +/** + * @namespace Chart.platform + * @see https://chartjs.gitbooks.io/proposals/content/Platform.html + * @since 2.4.0 + */ +var platform = helpers$1.extend({ + /** + * @since 2.7.0 + */ + initialize: function() {}, + + /** + * Called at chart construction time, returns a context2d instance implementing + * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. + * @param {*} item - The native item from which to acquire context (platform specific) + * @param {object} options - The chart options + * @returns {CanvasRenderingContext2D} context2d instance + */ + acquireContext: function() {}, + + /** + * Called at chart destruction time, releases any resources associated to the context + * previously returned by the acquireContext() method. + * @param {CanvasRenderingContext2D} context - The context2d instance + * @returns {boolean} true if the method succeeded, else false + */ + releaseContext: function() {}, + + /** + * Registers the specified listener on the given chart. + * @param {Chart} chart - Chart from which to listen for event + * @param {string} type - The ({@link IEvent}) type to listen for + * @param {function} listener - Receives a notification (an object that implements + * the {@link IEvent} interface) when an event of the specified type occurs. + */ + addEventListener: function() {}, + + /** + * Removes the specified listener previously registered with addEventListener. + * @param {Chart} chart - Chart from which to remove the listener + * @param {string} type - The ({@link IEvent}) type to remove + * @param {function} listener - The listener function to remove from the event target. + */ + removeEventListener: function() {} + +}, implementation); + +core_defaults._set('global', { + plugins: {} +}); + +/** + * The plugin service singleton + * @namespace Chart.plugins + * @since 2.1.0 + */ +var core_plugins = { + /** + * Globally registered plugins. + * @private + */ + _plugins: [], + + /** + * This identifier is used to invalidate the descriptors cache attached to each chart + * when a global plugin is registered or unregistered. In this case, the cache ID is + * incremented and descriptors are regenerated during following API calls. + * @private + */ + _cacheId: 0, + + /** + * Registers the given plugin(s) if not already registered. + * @param {IPlugin[]|IPlugin} plugins plugin instance(s). + */ + register: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + if (p.indexOf(plugin) === -1) { + p.push(plugin); + } + }); + + this._cacheId++; + }, + + /** + * Unregisters the given plugin(s) only if registered. + * @param {IPlugin[]|IPlugin} plugins plugin instance(s). + */ + unregister: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + var idx = p.indexOf(plugin); + if (idx !== -1) { + p.splice(idx, 1); + } + }); + + this._cacheId++; + }, + + /** + * Remove all registered plugins. + * @since 2.1.5 + */ + clear: function() { + this._plugins = []; + this._cacheId++; + }, + + /** + * Returns the number of registered plugins? + * @returns {number} + * @since 2.1.5 + */ + count: function() { + return this._plugins.length; + }, + + /** + * Returns all registered plugin instances. + * @returns {IPlugin[]} array of plugin objects. + * @since 2.1.5 + */ + getAll: function() { + return this._plugins; + }, + + /** + * Calls enabled plugins for `chart` on the specified hook and with the given args. + * This method immediately returns as soon as a plugin explicitly returns false. The + * returned value can be used, for instance, to interrupt the current action. + * @param {Chart} chart - The chart instance for which plugins should be called. + * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Array} [args] - Extra arguments to apply to the hook call. + * @returns {boolean} false if any of the plugins return false, else returns true. + */ + notify: function(chart, hook, args) { + var descriptors = this.descriptors(chart); + var ilen = descriptors.length; + var i, descriptor, plugin, params, method; + + for (i = 0; i < ilen; ++i) { + descriptor = descriptors[i]; + plugin = descriptor.plugin; + method = plugin[hook]; + if (typeof method === 'function') { + params = [chart].concat(args || []); + params.push(descriptor.options); + if (method.apply(plugin, params) === false) { + return false; + } + } + } + + return true; + }, + + /** + * Returns descriptors of enabled plugins for the given chart. + * @returns {object[]} [{ plugin, options }] + * @private + */ + descriptors: function(chart) { + var cache = chart.$plugins || (chart.$plugins = {}); + if (cache.id === this._cacheId) { + return cache.descriptors; + } + + var plugins = []; + var descriptors = []; + var config = (chart && chart.config) || {}; + var options = (config.options && config.options.plugins) || {}; + + this._plugins.concat(config.plugins || []).forEach(function(plugin) { + var idx = plugins.indexOf(plugin); + if (idx !== -1) { + return; + } + + var id = plugin.id; + var opts = options[id]; + if (opts === false) { + return; + } + + if (opts === true) { + opts = helpers$1.clone(core_defaults.global.plugins[id]); + } + + plugins.push(plugin); + descriptors.push({ + plugin: plugin, + options: opts || {} + }); + }); + + cache.descriptors = descriptors; + cache.id = this._cacheId; + return descriptors; + }, + + /** + * Invalidates cache for the given chart: descriptors hold a reference on plugin option, + * but in some cases, this reference can be changed by the user when updating options. + * https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + * @private + */ + _invalidate: function(chart) { + delete chart.$plugins; + } +}; + +var core_scaleService = { + // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then + // use the new chart options to grab the correct scale + constructors: {}, + // Use a registration function so that we can move to an ES6 map when we no longer need to support + // old browsers + + // Scale config defaults + defaults: {}, + registerScaleType: function(type, scaleConstructor, scaleDefaults) { + this.constructors[type] = scaleConstructor; + this.defaults[type] = helpers$1.clone(scaleDefaults); + }, + getScaleConstructor: function(type) { + return this.constructors.hasOwnProperty(type) ? this.constructors[type] : undefined; + }, + getScaleDefaults: function(type) { + // Return the scale defaults merged with the global settings so that we always use the latest ones + return this.defaults.hasOwnProperty(type) ? helpers$1.merge({}, [core_defaults.scale, this.defaults[type]]) : {}; + }, + updateScaleDefaults: function(type, additions) { + var me = this; + if (me.defaults.hasOwnProperty(type)) { + me.defaults[type] = helpers$1.extend(me.defaults[type], additions); + } + }, + addScalesToLayout: function(chart) { + // Adds each scale to the chart.boxes array to be sized accordingly + helpers$1.each(chart.scales, function(scale) { + // Set ILayoutItem parameters for backwards compatibility + scale.fullWidth = scale.options.fullWidth; + scale.position = scale.options.position; + scale.weight = scale.options.weight; + core_layouts.addBox(chart, scale); + }); + } +}; + +var valueOrDefault$7 = helpers$1.valueOrDefault; + +core_defaults._set('global', { + tooltips: { + enabled: true, + custom: null, + mode: 'nearest', + position: 'average', + intersect: true, + backgroundColor: 'rgba(0,0,0,0.8)', + titleFontStyle: 'bold', + titleSpacing: 2, + titleMarginBottom: 6, + titleFontColor: '#fff', + titleAlign: 'left', + bodySpacing: 2, + bodyFontColor: '#fff', + bodyAlign: 'left', + footerFontStyle: 'bold', + footerSpacing: 2, + footerMarginTop: 6, + footerFontColor: '#fff', + footerAlign: 'left', + yPadding: 6, + xPadding: 6, + caretPadding: 2, + caretSize: 5, + cornerRadius: 6, + multiKeyBackground: '#fff', + displayColors: true, + borderColor: 'rgba(0,0,0,0)', + borderWidth: 0, + callbacks: { + // Args are: (tooltipItems, data) + beforeTitle: helpers$1.noop, + title: function(tooltipItems, data) { + var title = ''; + var labels = data.labels; + var labelCount = labels ? labels.length : 0; + + if (tooltipItems.length > 0) { + var item = tooltipItems[0]; + if (item.label) { + title = item.label; + } else if (item.xLabel) { + title = item.xLabel; + } else if (labelCount > 0 && item.index < labelCount) { + title = labels[item.index]; + } + } + + return title; + }, + afterTitle: helpers$1.noop, + + // Args are: (tooltipItems, data) + beforeBody: helpers$1.noop, + + // Args are: (tooltipItem, data) + beforeLabel: helpers$1.noop, + label: function(tooltipItem, data) { + var label = data.datasets[tooltipItem.datasetIndex].label || ''; + + if (label) { + label += ': '; + } + if (!helpers$1.isNullOrUndef(tooltipItem.value)) { + label += tooltipItem.value; + } else { + label += tooltipItem.yLabel; + } + return label; + }, + labelColor: function(tooltipItem, chart) { + var meta = chart.getDatasetMeta(tooltipItem.datasetIndex); + var activeElement = meta.data[tooltipItem.index]; + var view = activeElement._view; + return { + borderColor: view.borderColor, + backgroundColor: view.backgroundColor + }; + }, + labelTextColor: function() { + return this._options.bodyFontColor; + }, + afterLabel: helpers$1.noop, + + // Args are: (tooltipItems, data) + afterBody: helpers$1.noop, + + // Args are: (tooltipItems, data) + beforeFooter: helpers$1.noop, + footer: helpers$1.noop, + afterFooter: helpers$1.noop + } + } +}); + +var positioners = { + /** + * Average mode places the tooltip at the average position of the elements shown + * @function Chart.Tooltip.positioners.average + * @param elements {ChartElement[]} the elements being displayed in the tooltip + * @returns {object} tooltip position + */ + average: function(elements) { + if (!elements.length) { + return false; + } + + var i, len; + var x = 0; + var y = 0; + var count = 0; + + for (i = 0, len = elements.length; i < len; ++i) { + var el = elements[i]; + if (el && el.hasValue()) { + var pos = el.tooltipPosition(); + x += pos.x; + y += pos.y; + ++count; + } + } + + return { + x: x / count, + y: y / count + }; + }, + + /** + * Gets the tooltip position nearest of the item nearest to the event position + * @function Chart.Tooltip.positioners.nearest + * @param elements {Chart.Element[]} the tooltip elements + * @param eventPosition {object} the position of the event in canvas coordinates + * @returns {object} the tooltip position + */ + nearest: function(elements, eventPosition) { + var x = eventPosition.x; + var y = eventPosition.y; + var minDistance = Number.POSITIVE_INFINITY; + var i, len, nearestElement; + + for (i = 0, len = elements.length; i < len; ++i) { + var el = elements[i]; + if (el && el.hasValue()) { + var center = el.getCenterPoint(); + var d = helpers$1.distanceBetweenPoints(eventPosition, center); + + if (d < minDistance) { + minDistance = d; + nearestElement = el; + } + } + } + + if (nearestElement) { + var tp = nearestElement.tooltipPosition(); + x = tp.x; + y = tp.y; + } + + return { + x: x, + y: y + }; + } +}; + +// Helper to push or concat based on if the 2nd parameter is an array or not +function pushOrConcat(base, toPush) { + if (toPush) { + if (helpers$1.isArray(toPush)) { + // base = base.concat(toPush); + Array.prototype.push.apply(base, toPush); + } else { + base.push(toPush); + } + } + + return base; +} + +/** + * Returns array of strings split by newline + * @param {string} value - The value to split by newline. + * @returns {string[]} value if newline present - Returned from String split() method + * @function + */ +function splitNewlines(str) { + if ((typeof str === 'string' || str instanceof String) && str.indexOf('\n') > -1) { + return str.split('\n'); + } + return str; +} + + +/** + * Private helper to create a tooltip item model + * @param element - the chart element (point, arc, bar) to create the tooltip item for + * @return new tooltip item + */ +function createTooltipItem(element) { + var xScale = element._xScale; + var yScale = element._yScale || element._scale; // handle radar || polarArea charts + var index = element._index; + var datasetIndex = element._datasetIndex; + var controller = element._chart.getDatasetMeta(datasetIndex).controller; + var indexScale = controller._getIndexScale(); + var valueScale = controller._getValueScale(); + + return { + xLabel: xScale ? xScale.getLabelForIndex(index, datasetIndex) : '', + yLabel: yScale ? yScale.getLabelForIndex(index, datasetIndex) : '', + label: indexScale ? '' + indexScale.getLabelForIndex(index, datasetIndex) : '', + value: valueScale ? '' + valueScale.getLabelForIndex(index, datasetIndex) : '', + index: index, + datasetIndex: datasetIndex, + x: element._model.x, + y: element._model.y + }; +} + +/** + * Helper to get the reset model for the tooltip + * @param tooltipOpts {object} the tooltip options + */ +function getBaseModel(tooltipOpts) { + var globalDefaults = core_defaults.global; + + return { + // Positioning + xPadding: tooltipOpts.xPadding, + yPadding: tooltipOpts.yPadding, + xAlign: tooltipOpts.xAlign, + yAlign: tooltipOpts.yAlign, + + // Body + bodyFontColor: tooltipOpts.bodyFontColor, + _bodyFontFamily: valueOrDefault$7(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), + _bodyFontStyle: valueOrDefault$7(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), + _bodyAlign: tooltipOpts.bodyAlign, + bodyFontSize: valueOrDefault$7(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), + bodySpacing: tooltipOpts.bodySpacing, + + // Title + titleFontColor: tooltipOpts.titleFontColor, + _titleFontFamily: valueOrDefault$7(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), + _titleFontStyle: valueOrDefault$7(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), + titleFontSize: valueOrDefault$7(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), + _titleAlign: tooltipOpts.titleAlign, + titleSpacing: tooltipOpts.titleSpacing, + titleMarginBottom: tooltipOpts.titleMarginBottom, + + // Footer + footerFontColor: tooltipOpts.footerFontColor, + _footerFontFamily: valueOrDefault$7(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), + _footerFontStyle: valueOrDefault$7(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), + footerFontSize: valueOrDefault$7(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize), + _footerAlign: tooltipOpts.footerAlign, + footerSpacing: tooltipOpts.footerSpacing, + footerMarginTop: tooltipOpts.footerMarginTop, + + // Appearance + caretSize: tooltipOpts.caretSize, + cornerRadius: tooltipOpts.cornerRadius, + backgroundColor: tooltipOpts.backgroundColor, + opacity: 0, + legendColorBackground: tooltipOpts.multiKeyBackground, + displayColors: tooltipOpts.displayColors, + borderColor: tooltipOpts.borderColor, + borderWidth: tooltipOpts.borderWidth + }; +} + +/** + * Get the size of the tooltip + */ +function getTooltipSize(tooltip, model) { + var ctx = tooltip._chart.ctx; + + var height = model.yPadding * 2; // Tooltip Padding + var width = 0; + + // Count of all lines in the body + var body = model.body; + var combinedBodyLength = body.reduce(function(count, bodyItem) { + return count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length; + }, 0); + combinedBodyLength += model.beforeBody.length + model.afterBody.length; + + var titleLineCount = model.title.length; + var footerLineCount = model.footer.length; + var titleFontSize = model.titleFontSize; + var bodyFontSize = model.bodyFontSize; + var footerFontSize = model.footerFontSize; + + height += titleLineCount * titleFontSize; // Title Lines + height += titleLineCount ? (titleLineCount - 1) * model.titleSpacing : 0; // Title Line Spacing + height += titleLineCount ? model.titleMarginBottom : 0; // Title's bottom Margin + height += combinedBodyLength * bodyFontSize; // Body Lines + height += combinedBodyLength ? (combinedBodyLength - 1) * model.bodySpacing : 0; // Body Line Spacing + height += footerLineCount ? model.footerMarginTop : 0; // Footer Margin + height += footerLineCount * (footerFontSize); // Footer Lines + height += footerLineCount ? (footerLineCount - 1) * model.footerSpacing : 0; // Footer Line Spacing + + // Title width + var widthPadding = 0; + var maxLineWidth = function(line) { + width = Math.max(width, ctx.measureText(line).width + widthPadding); + }; + + ctx.font = helpers$1.fontString(titleFontSize, model._titleFontStyle, model._titleFontFamily); + helpers$1.each(model.title, maxLineWidth); + + // Body width + ctx.font = helpers$1.fontString(bodyFontSize, model._bodyFontStyle, model._bodyFontFamily); + helpers$1.each(model.beforeBody.concat(model.afterBody), maxLineWidth); + + // Body lines may include some extra width due to the color box + widthPadding = model.displayColors ? (bodyFontSize + 2) : 0; + helpers$1.each(body, function(bodyItem) { + helpers$1.each(bodyItem.before, maxLineWidth); + helpers$1.each(bodyItem.lines, maxLineWidth); + helpers$1.each(bodyItem.after, maxLineWidth); + }); + + // Reset back to 0 + widthPadding = 0; + + // Footer width + ctx.font = helpers$1.fontString(footerFontSize, model._footerFontStyle, model._footerFontFamily); + helpers$1.each(model.footer, maxLineWidth); + + // Add padding + width += 2 * model.xPadding; + + return { + width: width, + height: height + }; +} + +/** + * Helper to get the alignment of a tooltip given the size + */ +function determineAlignment(tooltip, size) { + var model = tooltip._model; + var chart = tooltip._chart; + var chartArea = tooltip._chart.chartArea; + var xAlign = 'center'; + var yAlign = 'center'; + + if (model.y < size.height) { + yAlign = 'top'; + } else if (model.y > (chart.height - size.height)) { + yAlign = 'bottom'; + } + + var lf, rf; // functions to determine left, right alignment + var olf, orf; // functions to determine if left/right alignment causes tooltip to go outside chart + var yf; // function to get the y alignment if the tooltip goes outside of the left or right edges + var midX = (chartArea.left + chartArea.right) / 2; + var midY = (chartArea.top + chartArea.bottom) / 2; + + if (yAlign === 'center') { + lf = function(x) { + return x <= midX; + }; + rf = function(x) { + return x > midX; + }; + } else { + lf = function(x) { + return x <= (size.width / 2); + }; + rf = function(x) { + return x >= (chart.width - (size.width / 2)); + }; + } + + olf = function(x) { + return x + size.width + model.caretSize + model.caretPadding > chart.width; + }; + orf = function(x) { + return x - size.width - model.caretSize - model.caretPadding < 0; + }; + yf = function(y) { + return y <= midY ? 'top' : 'bottom'; + }; + + if (lf(model.x)) { + xAlign = 'left'; + + // Is tooltip too wide and goes over the right side of the chart.? + if (olf(model.x)) { + xAlign = 'center'; + yAlign = yf(model.y); + } + } else if (rf(model.x)) { + xAlign = 'right'; + + // Is tooltip too wide and goes outside left edge of canvas? + if (orf(model.x)) { + xAlign = 'center'; + yAlign = yf(model.y); + } + } + + var opts = tooltip._options; + return { + xAlign: opts.xAlign ? opts.xAlign : xAlign, + yAlign: opts.yAlign ? opts.yAlign : yAlign + }; +} + +/** + * Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment + */ +function getBackgroundPoint(vm, size, alignment, chart) { + // Background Position + var x = vm.x; + var y = vm.y; + + var caretSize = vm.caretSize; + var caretPadding = vm.caretPadding; + var cornerRadius = vm.cornerRadius; + var xAlign = alignment.xAlign; + var yAlign = alignment.yAlign; + var paddingAndSize = caretSize + caretPadding; + var radiusAndPadding = cornerRadius + caretPadding; + + if (xAlign === 'right') { + x -= size.width; + } else if (xAlign === 'center') { + x -= (size.width / 2); + if (x + size.width > chart.width) { + x = chart.width - size.width; + } + if (x < 0) { + x = 0; + } + } + + if (yAlign === 'top') { + y += paddingAndSize; + } else if (yAlign === 'bottom') { + y -= size.height + paddingAndSize; + } else { + y -= (size.height / 2); + } + + if (yAlign === 'center') { + if (xAlign === 'left') { + x += paddingAndSize; + } else if (xAlign === 'right') { + x -= paddingAndSize; + } + } else if (xAlign === 'left') { + x -= radiusAndPadding; + } else if (xAlign === 'right') { + x += radiusAndPadding; + } + + return { + x: x, + y: y + }; +} + +function getAlignedX(vm, align) { + return align === 'center' + ? vm.x + vm.width / 2 + : align === 'right' + ? vm.x + vm.width - vm.xPadding + : vm.x + vm.xPadding; +} + +/** + * Helper to build before and after body lines + */ +function getBeforeAfterBodyLines(callback) { + return pushOrConcat([], splitNewlines(callback)); +} + +var exports$3 = core_element.extend({ + initialize: function() { + this._model = getBaseModel(this._options); + this._lastActive = []; + }, + + // Get the title + // Args are: (tooltipItem, data) + getTitle: function() { + var me = this; + var opts = me._options; + var callbacks = opts.callbacks; + + var beforeTitle = callbacks.beforeTitle.apply(me, arguments); + var title = callbacks.title.apply(me, arguments); + var afterTitle = callbacks.afterTitle.apply(me, arguments); + + var lines = []; + lines = pushOrConcat(lines, splitNewlines(beforeTitle)); + lines = pushOrConcat(lines, splitNewlines(title)); + lines = pushOrConcat(lines, splitNewlines(afterTitle)); + + return lines; + }, + + // Args are: (tooltipItem, data) + getBeforeBody: function() { + return getBeforeAfterBodyLines(this._options.callbacks.beforeBody.apply(this, arguments)); + }, + + // Args are: (tooltipItem, data) + getBody: function(tooltipItems, data) { + var me = this; + var callbacks = me._options.callbacks; + var bodyItems = []; + + helpers$1.each(tooltipItems, function(tooltipItem) { + var bodyItem = { + before: [], + lines: [], + after: [] + }; + pushOrConcat(bodyItem.before, splitNewlines(callbacks.beforeLabel.call(me, tooltipItem, data))); + pushOrConcat(bodyItem.lines, callbacks.label.call(me, tooltipItem, data)); + pushOrConcat(bodyItem.after, splitNewlines(callbacks.afterLabel.call(me, tooltipItem, data))); + + bodyItems.push(bodyItem); + }); + + return bodyItems; + }, + + // Args are: (tooltipItem, data) + getAfterBody: function() { + return getBeforeAfterBodyLines(this._options.callbacks.afterBody.apply(this, arguments)); + }, + + // Get the footer and beforeFooter and afterFooter lines + // Args are: (tooltipItem, data) + getFooter: function() { + var me = this; + var callbacks = me._options.callbacks; + + var beforeFooter = callbacks.beforeFooter.apply(me, arguments); + var footer = callbacks.footer.apply(me, arguments); + var afterFooter = callbacks.afterFooter.apply(me, arguments); + + var lines = []; + lines = pushOrConcat(lines, splitNewlines(beforeFooter)); + lines = pushOrConcat(lines, splitNewlines(footer)); + lines = pushOrConcat(lines, splitNewlines(afterFooter)); + + return lines; + }, + + update: function(changed) { + var me = this; + var opts = me._options; + + // Need to regenerate the model because its faster than using extend and it is necessary due to the optimization in Chart.Element.transition + // that does _view = _model if ease === 1. This causes the 2nd tooltip update to set properties in both the view and model at the same time + // which breaks any animations. + var existingModel = me._model; + var model = me._model = getBaseModel(opts); + var active = me._active; + + var data = me._data; + + // In the case where active.length === 0 we need to keep these at existing values for good animations + var alignment = { + xAlign: existingModel.xAlign, + yAlign: existingModel.yAlign + }; + var backgroundPoint = { + x: existingModel.x, + y: existingModel.y + }; + var tooltipSize = { + width: existingModel.width, + height: existingModel.height + }; + var tooltipPosition = { + x: existingModel.caretX, + y: existingModel.caretY + }; + + var i, len; + + if (active.length) { + model.opacity = 1; + + var labelColors = []; + var labelTextColors = []; + tooltipPosition = positioners[opts.position].call(me, active, me._eventPosition); + + var tooltipItems = []; + for (i = 0, len = active.length; i < len; ++i) { + tooltipItems.push(createTooltipItem(active[i])); + } + + // If the user provided a filter function, use it to modify the tooltip items + if (opts.filter) { + tooltipItems = tooltipItems.filter(function(a) { + return opts.filter(a, data); + }); + } + + // If the user provided a sorting function, use it to modify the tooltip items + if (opts.itemSort) { + tooltipItems = tooltipItems.sort(function(a, b) { + return opts.itemSort(a, b, data); + }); + } + + // Determine colors for boxes + helpers$1.each(tooltipItems, function(tooltipItem) { + labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, me._chart)); + labelTextColors.push(opts.callbacks.labelTextColor.call(me, tooltipItem, me._chart)); + }); + + + // Build the Text Lines + model.title = me.getTitle(tooltipItems, data); + model.beforeBody = me.getBeforeBody(tooltipItems, data); + model.body = me.getBody(tooltipItems, data); + model.afterBody = me.getAfterBody(tooltipItems, data); + model.footer = me.getFooter(tooltipItems, data); + + // Initial positioning and colors + model.x = tooltipPosition.x; + model.y = tooltipPosition.y; + model.caretPadding = opts.caretPadding; + model.labelColors = labelColors; + model.labelTextColors = labelTextColors; + + // data points + model.dataPoints = tooltipItems; + + // We need to determine alignment of the tooltip + tooltipSize = getTooltipSize(this, model); + alignment = determineAlignment(this, tooltipSize); + // Final Size and Position + backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment, me._chart); + } else { + model.opacity = 0; + } + + model.xAlign = alignment.xAlign; + model.yAlign = alignment.yAlign; + model.x = backgroundPoint.x; + model.y = backgroundPoint.y; + model.width = tooltipSize.width; + model.height = tooltipSize.height; + + // Point where the caret on the tooltip points to + model.caretX = tooltipPosition.x; + model.caretY = tooltipPosition.y; + + me._model = model; + + if (changed && opts.custom) { + opts.custom.call(me, model); + } + + return me; + }, + + drawCaret: function(tooltipPoint, size) { + var ctx = this._chart.ctx; + var vm = this._view; + var caretPosition = this.getCaretPosition(tooltipPoint, size, vm); + + ctx.lineTo(caretPosition.x1, caretPosition.y1); + ctx.lineTo(caretPosition.x2, caretPosition.y2); + ctx.lineTo(caretPosition.x3, caretPosition.y3); + }, + getCaretPosition: function(tooltipPoint, size, vm) { + var x1, x2, x3, y1, y2, y3; + var caretSize = vm.caretSize; + var cornerRadius = vm.cornerRadius; + var xAlign = vm.xAlign; + var yAlign = vm.yAlign; + var ptX = tooltipPoint.x; + var ptY = tooltipPoint.y; + var width = size.width; + var height = size.height; + + if (yAlign === 'center') { + y2 = ptY + (height / 2); + + if (xAlign === 'left') { + x1 = ptX; + x2 = x1 - caretSize; + x3 = x1; + + y1 = y2 + caretSize; + y3 = y2 - caretSize; + } else { + x1 = ptX + width; + x2 = x1 + caretSize; + x3 = x1; + + y1 = y2 - caretSize; + y3 = y2 + caretSize; + } + } else { + if (xAlign === 'left') { + x2 = ptX + cornerRadius + (caretSize); + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } else if (xAlign === 'right') { + x2 = ptX + width - cornerRadius - caretSize; + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } else { + x2 = vm.caretX; + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } + if (yAlign === 'top') { + y1 = ptY; + y2 = y1 - caretSize; + y3 = y1; + } else { + y1 = ptY + height; + y2 = y1 + caretSize; + y3 = y1; + // invert drawing order + var tmp = x3; + x3 = x1; + x1 = tmp; + } + } + return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3}; + }, + + drawTitle: function(pt, vm, ctx) { + var title = vm.title; + + if (title.length) { + pt.x = getAlignedX(vm, vm._titleAlign); + + ctx.textAlign = vm._titleAlign; + ctx.textBaseline = 'top'; + + var titleFontSize = vm.titleFontSize; + var titleSpacing = vm.titleSpacing; + + ctx.fillStyle = vm.titleFontColor; + ctx.font = helpers$1.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); + + var i, len; + for (i = 0, len = title.length; i < len; ++i) { + ctx.fillText(title[i], pt.x, pt.y); + pt.y += titleFontSize + titleSpacing; // Line Height and spacing + + if (i + 1 === title.length) { + pt.y += vm.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing + } + } + } + }, + + drawBody: function(pt, vm, ctx) { + var bodyFontSize = vm.bodyFontSize; + var bodySpacing = vm.bodySpacing; + var bodyAlign = vm._bodyAlign; + var body = vm.body; + var drawColorBoxes = vm.displayColors; + var labelColors = vm.labelColors; + var xLinePadding = 0; + var colorX = drawColorBoxes ? getAlignedX(vm, 'left') : 0; + var textColor; + + ctx.textAlign = bodyAlign; + ctx.textBaseline = 'top'; + ctx.font = helpers$1.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); + + pt.x = getAlignedX(vm, bodyAlign); + + // Before Body + var fillLineOfText = function(line) { + ctx.fillText(line, pt.x + xLinePadding, pt.y); + pt.y += bodyFontSize + bodySpacing; + }; + + // Before body lines + ctx.fillStyle = vm.bodyFontColor; + helpers$1.each(vm.beforeBody, fillLineOfText); + + xLinePadding = drawColorBoxes && bodyAlign !== 'right' + ? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2) + : 0; + + // Draw body lines now + helpers$1.each(body, function(bodyItem, i) { + textColor = vm.labelTextColors[i]; + ctx.fillStyle = textColor; + helpers$1.each(bodyItem.before, fillLineOfText); + + helpers$1.each(bodyItem.lines, function(line) { + // Draw Legend-like boxes if needed + if (drawColorBoxes) { + // Fill a white rect so that colours merge nicely if the opacity is < 1 + ctx.fillStyle = vm.legendColorBackground; + ctx.fillRect(colorX, pt.y, bodyFontSize, bodyFontSize); + + // Border + ctx.lineWidth = 1; + ctx.strokeStyle = labelColors[i].borderColor; + ctx.strokeRect(colorX, pt.y, bodyFontSize, bodyFontSize); + + // Inner square + ctx.fillStyle = labelColors[i].backgroundColor; + ctx.fillRect(colorX + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); + ctx.fillStyle = textColor; + } + + fillLineOfText(line); + }); + + helpers$1.each(bodyItem.after, fillLineOfText); + }); + + // Reset back to 0 for after body + xLinePadding = 0; + + // After body lines + helpers$1.each(vm.afterBody, fillLineOfText); + pt.y -= bodySpacing; // Remove last body spacing + }, + + drawFooter: function(pt, vm, ctx) { + var footer = vm.footer; + + if (footer.length) { + pt.x = getAlignedX(vm, vm._footerAlign); + pt.y += vm.footerMarginTop; + + ctx.textAlign = vm._footerAlign; + ctx.textBaseline = 'top'; + + ctx.fillStyle = vm.footerFontColor; + ctx.font = helpers$1.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); + + helpers$1.each(footer, function(line) { + ctx.fillText(line, pt.x, pt.y); + pt.y += vm.footerFontSize + vm.footerSpacing; + }); + } + }, + + drawBackground: function(pt, vm, ctx, tooltipSize) { + ctx.fillStyle = vm.backgroundColor; + ctx.strokeStyle = vm.borderColor; + ctx.lineWidth = vm.borderWidth; + var xAlign = vm.xAlign; + var yAlign = vm.yAlign; + var x = pt.x; + var y = pt.y; + var width = tooltipSize.width; + var height = tooltipSize.height; + var radius = vm.cornerRadius; + + ctx.beginPath(); + ctx.moveTo(x + radius, y); + if (yAlign === 'top') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + if (yAlign === 'center' && xAlign === 'right') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + if (yAlign === 'bottom') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + if (yAlign === 'center' && xAlign === 'left') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + + ctx.fill(); + + if (vm.borderWidth > 0) { + ctx.stroke(); + } + }, + + draw: function() { + var ctx = this._chart.ctx; + var vm = this._view; + + if (vm.opacity === 0) { + return; + } + + var tooltipSize = { + width: vm.width, + height: vm.height + }; + var pt = { + x: vm.x, + y: vm.y + }; + + // IE11/Edge does not like very small opacities, so snap to 0 + var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity; + + // Truthy/falsey value for empty tooltip + var hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length; + + if (this._options.enabled && hasTooltipContent) { + ctx.save(); + ctx.globalAlpha = opacity; + + // Draw Background + this.drawBackground(pt, vm, ctx, tooltipSize); + + // Draw Title, Body, and Footer + pt.y += vm.yPadding; + + // Titles + this.drawTitle(pt, vm, ctx); + + // Body + this.drawBody(pt, vm, ctx); + + // Footer + this.drawFooter(pt, vm, ctx); + + ctx.restore(); + } + }, + + /** + * Handle an event + * @private + * @param {IEvent} event - The event to handle + * @returns {boolean} true if the tooltip changed + */ + handleEvent: function(e) { + var me = this; + var options = me._options; + var changed = false; + + me._lastActive = me._lastActive || []; + + // Find Active Elements for tooltips + if (e.type === 'mouseout') { + me._active = []; + } else { + me._active = me._chart.getElementsAtEventForMode(e, options.mode, options); + } + + // Remember Last Actives + changed = !helpers$1.arrayEquals(me._active, me._lastActive); + + // Only handle target event on tooltip change + if (changed) { + me._lastActive = me._active; + + if (options.enabled || options.custom) { + me._eventPosition = { + x: e.x, + y: e.y + }; + + me.update(true); + me.pivot(); + } + } + + return changed; + } +}); + +/** + * @namespace Chart.Tooltip.positioners + */ +var positioners_1 = positioners; + +var core_tooltip = exports$3; +core_tooltip.positioners = positioners_1; + +var valueOrDefault$8 = helpers$1.valueOrDefault; + +core_defaults._set('global', { + elements: {}, + events: [ + 'mousemove', + 'mouseout', + 'click', + 'touchstart', + 'touchmove' + ], + hover: { + onHover: null, + mode: 'nearest', + intersect: true, + animationDuration: 400 + }, + onClick: null, + maintainAspectRatio: true, + responsive: true, + responsiveAnimationDuration: 0 +}); + +/** + * Recursively merge the given config objects representing the `scales` option + * by incorporating scale defaults in `xAxes` and `yAxes` array items, then + * returns a deep copy of the result, thus doesn't alter inputs. + */ +function mergeScaleConfig(/* config objects ... */) { + return helpers$1.merge({}, [].slice.call(arguments), { + merger: function(key, target, source, options) { + if (key === 'xAxes' || key === 'yAxes') { + var slen = source[key].length; + var i, type, scale; + + if (!target[key]) { + target[key] = []; + } + + for (i = 0; i < slen; ++i) { + scale = source[key][i]; + type = valueOrDefault$8(scale.type, key === 'xAxes' ? 'category' : 'linear'); + + if (i >= target[key].length) { + target[key].push({}); + } + + if (!target[key][i].type || (scale.type && scale.type !== target[key][i].type)) { + // new/untyped scale or type changed: let's apply the new defaults + // then merge source scale to correctly overwrite the defaults. + helpers$1.merge(target[key][i], [core_scaleService.getScaleDefaults(type), scale]); + } else { + // scales type are the same + helpers$1.merge(target[key][i], scale); + } + } + } else { + helpers$1._merger(key, target, source, options); + } + } + }); +} + +/** + * Recursively merge the given config objects as the root options by handling + * default scale options for the `scales` and `scale` properties, then returns + * a deep copy of the result, thus doesn't alter inputs. + */ +function mergeConfig(/* config objects ... */) { + return helpers$1.merge({}, [].slice.call(arguments), { + merger: function(key, target, source, options) { + var tval = target[key] || {}; + var sval = source[key]; + + if (key === 'scales') { + // scale config merging is complex. Add our own function here for that + target[key] = mergeScaleConfig(tval, sval); + } else if (key === 'scale') { + // used in polar area & radar charts since there is only one scale + target[key] = helpers$1.merge(tval, [core_scaleService.getScaleDefaults(sval.type), sval]); + } else { + helpers$1._merger(key, target, source, options); + } + } + }); +} + +function initConfig(config) { + config = config || {}; + + // Do NOT use mergeConfig for the data object because this method merges arrays + // and so would change references to labels and datasets, preventing data updates. + var data = config.data = config.data || {}; + data.datasets = data.datasets || []; + data.labels = data.labels || []; + + config.options = mergeConfig( + core_defaults.global, + core_defaults[config.type], + config.options || {}); + + return config; +} + +function updateConfig(chart) { + var newOptions = chart.options; + + helpers$1.each(chart.scales, function(scale) { + core_layouts.removeBox(chart, scale); + }); + + newOptions = mergeConfig( + core_defaults.global, + core_defaults[chart.config.type], + newOptions); + + chart.options = chart.config.options = newOptions; + chart.ensureScalesHaveIDs(); + chart.buildOrUpdateScales(); + + // Tooltip + chart.tooltip._options = newOptions.tooltips; + chart.tooltip.initialize(); +} + +function positionIsHorizontal(position) { + return position === 'top' || position === 'bottom'; +} + +var Chart = function(item, config) { + this.construct(item, config); + return this; +}; + +helpers$1.extend(Chart.prototype, /** @lends Chart */ { + /** + * @private + */ + construct: function(item, config) { + var me = this; + + config = initConfig(config); + + var context = platform.acquireContext(item, config); + var canvas = context && context.canvas; + var height = canvas && canvas.height; + var width = canvas && canvas.width; + + me.id = helpers$1.uid(); + me.ctx = context; + me.canvas = canvas; + me.config = config; + me.width = width; + me.height = height; + me.aspectRatio = height ? width / height : null; + me.options = config.options; + me._bufferedRender = false; + + /** + * Provided for backward compatibility, Chart and Chart.Controller have been merged, + * the "instance" still need to be defined since it might be called from plugins. + * @prop Chart#chart + * @deprecated since version 2.6.0 + * @todo remove at version 3 + * @private + */ + me.chart = me; + me.controller = me; // chart.chart.controller #inception + + // Add the chart instance to the global namespace + Chart.instances[me.id] = me; + + // Define alias to the config data: `chart.data === chart.config.data` + Object.defineProperty(me, 'data', { + get: function() { + return me.config.data; + }, + set: function(value) { + me.config.data = value; + } + }); + + if (!context || !canvas) { + // The given item is not a compatible context2d element, let's return before finalizing + // the chart initialization but after setting basic chart / controller properties that + // can help to figure out that the chart is not valid (e.g chart.canvas !== null); + // https://github.com/chartjs/Chart.js/issues/2807 + console.error("Failed to create chart: can't acquire context from the given item"); + return; + } + + me.initialize(); + me.update(); + }, + + /** + * @private + */ + initialize: function() { + var me = this; + + // Before init plugin notification + core_plugins.notify(me, 'beforeInit'); + + helpers$1.retinaScale(me, me.options.devicePixelRatio); + + me.bindEvents(); + + if (me.options.responsive) { + // Initial resize before chart draws (must be silent to preserve initial animations). + me.resize(true); + } + + // Make sure scales have IDs and are built before we build any controllers. + me.ensureScalesHaveIDs(); + me.buildOrUpdateScales(); + me.initToolTip(); + + // After init plugin notification + core_plugins.notify(me, 'afterInit'); + + return me; + }, + + clear: function() { + helpers$1.canvas.clear(this); + return this; + }, + + stop: function() { + // Stops any current animation loop occurring + core_animations.cancelAnimation(this); + return this; + }, + + resize: function(silent) { + var me = this; + var options = me.options; + var canvas = me.canvas; + var aspectRatio = (options.maintainAspectRatio && me.aspectRatio) || null; + + // the canvas render width and height will be casted to integers so make sure that + // the canvas display style uses the same integer values to avoid blurring effect. + + // Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed + var newWidth = Math.max(0, Math.floor(helpers$1.getMaximumWidth(canvas))); + var newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers$1.getMaximumHeight(canvas))); + + if (me.width === newWidth && me.height === newHeight) { + return; + } + + canvas.width = me.width = newWidth; + canvas.height = me.height = newHeight; + canvas.style.width = newWidth + 'px'; + canvas.style.height = newHeight + 'px'; + + helpers$1.retinaScale(me, options.devicePixelRatio); + + if (!silent) { + // Notify any plugins about the resize + var newSize = {width: newWidth, height: newHeight}; + core_plugins.notify(me, 'resize', [newSize]); + + // Notify of resize + if (options.onResize) { + options.onResize(me, newSize); + } + + me.stop(); + me.update({ + duration: options.responsiveAnimationDuration + }); + } + }, + + ensureScalesHaveIDs: function() { + var options = this.options; + var scalesOptions = options.scales || {}; + var scaleOptions = options.scale; + + helpers$1.each(scalesOptions.xAxes, function(xAxisOptions, index) { + xAxisOptions.id = xAxisOptions.id || ('x-axis-' + index); + }); + + helpers$1.each(scalesOptions.yAxes, function(yAxisOptions, index) { + yAxisOptions.id = yAxisOptions.id || ('y-axis-' + index); + }); + + if (scaleOptions) { + scaleOptions.id = scaleOptions.id || 'scale'; + } + }, + + /** + * Builds a map of scale ID to scale object for future lookup. + */ + buildOrUpdateScales: function() { + var me = this; + var options = me.options; + var scales = me.scales || {}; + var items = []; + var updated = Object.keys(scales).reduce(function(obj, id) { + obj[id] = false; + return obj; + }, {}); + + if (options.scales) { + items = items.concat( + (options.scales.xAxes || []).map(function(xAxisOptions) { + return {options: xAxisOptions, dtype: 'category', dposition: 'bottom'}; + }), + (options.scales.yAxes || []).map(function(yAxisOptions) { + return {options: yAxisOptions, dtype: 'linear', dposition: 'left'}; + }) + ); + } + + if (options.scale) { + items.push({ + options: options.scale, + dtype: 'radialLinear', + isDefault: true, + dposition: 'chartArea' + }); + } + + helpers$1.each(items, function(item) { + var scaleOptions = item.options; + var id = scaleOptions.id; + var scaleType = valueOrDefault$8(scaleOptions.type, item.dtype); + + if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) { + scaleOptions.position = item.dposition; + } + + updated[id] = true; + var scale = null; + if (id in scales && scales[id].type === scaleType) { + scale = scales[id]; + scale.options = scaleOptions; + scale.ctx = me.ctx; + scale.chart = me; + } else { + var scaleClass = core_scaleService.getScaleConstructor(scaleType); + if (!scaleClass) { + return; + } + scale = new scaleClass({ + id: id, + type: scaleType, + options: scaleOptions, + ctx: me.ctx, + chart: me + }); + scales[scale.id] = scale; + } + + scale.mergeTicksOptions(); + + // TODO(SB): I think we should be able to remove this custom case (options.scale) + // and consider it as a regular scale part of the "scales"" map only! This would + // make the logic easier and remove some useless? custom code. + if (item.isDefault) { + me.scale = scale; + } + }); + // clear up discarded scales + helpers$1.each(updated, function(hasUpdated, id) { + if (!hasUpdated) { + delete scales[id]; + } + }); + + me.scales = scales; + + core_scaleService.addScalesToLayout(this); + }, + + buildOrUpdateControllers: function() { + var me = this; + var newControllers = []; + + helpers$1.each(me.data.datasets, function(dataset, datasetIndex) { + var meta = me.getDatasetMeta(datasetIndex); + var type = dataset.type || me.config.type; + + if (meta.type && meta.type !== type) { + me.destroyDatasetMeta(datasetIndex); + meta = me.getDatasetMeta(datasetIndex); + } + meta.type = type; + + if (meta.controller) { + meta.controller.updateIndex(datasetIndex); + meta.controller.linkScales(); + } else { + var ControllerClass = controllers[meta.type]; + if (ControllerClass === undefined) { + throw new Error('"' + meta.type + '" is not a chart type.'); + } + + meta.controller = new ControllerClass(me, datasetIndex); + newControllers.push(meta.controller); + } + }, me); + + return newControllers; + }, + + /** + * Reset the elements of all datasets + * @private + */ + resetElements: function() { + var me = this; + helpers$1.each(me.data.datasets, function(dataset, datasetIndex) { + me.getDatasetMeta(datasetIndex).controller.reset(); + }, me); + }, + + /** + * Resets the chart back to it's state before the initial animation + */ + reset: function() { + this.resetElements(); + this.tooltip.initialize(); + }, + + update: function(config) { + var me = this; + + if (!config || typeof config !== 'object') { + // backwards compatibility + config = { + duration: config, + lazy: arguments[1] + }; + } + + updateConfig(me); + + // plugins options references might have change, let's invalidate the cache + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + core_plugins._invalidate(me); + + if (core_plugins.notify(me, 'beforeUpdate') === false) { + return; + } + + // In case the entire data object changed + me.tooltip._data = me.data; + + // Make sure dataset controllers are updated and new controllers are reset + var newControllers = me.buildOrUpdateControllers(); + + // Make sure all dataset controllers have correct meta data counts + helpers$1.each(me.data.datasets, function(dataset, datasetIndex) { + me.getDatasetMeta(datasetIndex).controller.buildOrUpdateElements(); + }, me); + + me.updateLayout(); + + // Can only reset the new controllers after the scales have been updated + if (me.options.animation && me.options.animation.duration) { + helpers$1.each(newControllers, function(controller) { + controller.reset(); + }); + } + + me.updateDatasets(); + + // Need to reset tooltip in case it is displayed with elements that are removed + // after update. + me.tooltip.initialize(); + + // Last active contains items that were previously in the tooltip. + // When we reset the tooltip, we need to clear it + me.lastActive = []; + + // Do this before render so that any plugins that need final scale updates can use it + core_plugins.notify(me, 'afterUpdate'); + + if (me._bufferedRender) { + me._bufferedRequest = { + duration: config.duration, + easing: config.easing, + lazy: config.lazy + }; + } else { + me.render(config); + } + }, + + /** + * Updates the chart layout unless a plugin returns `false` to the `beforeLayout` + * hook, in which case, plugins will not be called on `afterLayout`. + * @private + */ + updateLayout: function() { + var me = this; + + if (core_plugins.notify(me, 'beforeLayout') === false) { + return; + } + + core_layouts.update(this, this.width, this.height); + + /** + * Provided for backward compatibility, use `afterLayout` instead. + * @method IPlugin#afterScaleUpdate + * @deprecated since version 2.5.0 + * @todo remove at version 3 + * @private + */ + core_plugins.notify(me, 'afterScaleUpdate'); + core_plugins.notify(me, 'afterLayout'); + }, + + /** + * Updates all datasets unless a plugin returns `false` to the `beforeDatasetsUpdate` + * hook, in which case, plugins will not be called on `afterDatasetsUpdate`. + * @private + */ + updateDatasets: function() { + var me = this; + + if (core_plugins.notify(me, 'beforeDatasetsUpdate') === false) { + return; + } + + for (var i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me.updateDataset(i); + } + + core_plugins.notify(me, 'afterDatasetsUpdate'); + }, + + /** + * Updates dataset at index unless a plugin returns `false` to the `beforeDatasetUpdate` + * hook, in which case, plugins will not be called on `afterDatasetUpdate`. + * @private + */ + updateDataset: function(index) { + var me = this; + var meta = me.getDatasetMeta(index); + var args = { + meta: meta, + index: index + }; + + if (core_plugins.notify(me, 'beforeDatasetUpdate', [args]) === false) { + return; + } + + meta.controller.update(); + + core_plugins.notify(me, 'afterDatasetUpdate', [args]); + }, + + render: function(config) { + var me = this; + + if (!config || typeof config !== 'object') { + // backwards compatibility + config = { + duration: config, + lazy: arguments[1] + }; + } + + var animationOptions = me.options.animation; + var duration = valueOrDefault$8(config.duration, animationOptions && animationOptions.duration); + var lazy = config.lazy; + + if (core_plugins.notify(me, 'beforeRender') === false) { + return; + } + + var onComplete = function(animation) { + core_plugins.notify(me, 'afterRender'); + helpers$1.callback(animationOptions && animationOptions.onComplete, [animation], me); + }; + + if (animationOptions && duration) { + var animation = new core_animation({ + numSteps: duration / 16.66, // 60 fps + easing: config.easing || animationOptions.easing, + + render: function(chart, animationObject) { + var easingFunction = helpers$1.easing.effects[animationObject.easing]; + var currentStep = animationObject.currentStep; + var stepDecimal = currentStep / animationObject.numSteps; + + chart.draw(easingFunction(stepDecimal), stepDecimal, currentStep); + }, + + onAnimationProgress: animationOptions.onProgress, + onAnimationComplete: onComplete + }); + + core_animations.addAnimation(me, animation, duration, lazy); + } else { + me.draw(); + + // See https://github.com/chartjs/Chart.js/issues/3781 + onComplete(new core_animation({numSteps: 0, chart: me})); + } + + return me; + }, + + draw: function(easingValue) { + var me = this; + + me.clear(); + + if (helpers$1.isNullOrUndef(easingValue)) { + easingValue = 1; + } + + me.transition(easingValue); + + if (me.width <= 0 || me.height <= 0) { + return; + } + + if (core_plugins.notify(me, 'beforeDraw', [easingValue]) === false) { + return; + } + + // Draw all the scales + helpers$1.each(me.boxes, function(box) { + box.draw(me.chartArea); + }, me); + + me.drawDatasets(easingValue); + me._drawTooltip(easingValue); + + core_plugins.notify(me, 'afterDraw', [easingValue]); + }, + + /** + * @private + */ + transition: function(easingValue) { + var me = this; + + for (var i = 0, ilen = (me.data.datasets || []).length; i < ilen; ++i) { + if (me.isDatasetVisible(i)) { + me.getDatasetMeta(i).controller.transition(easingValue); + } + } + + me.tooltip.transition(easingValue); + }, + + /** + * Draws all datasets unless a plugin returns `false` to the `beforeDatasetsDraw` + * hook, in which case, plugins will not be called on `afterDatasetsDraw`. + * @private + */ + drawDatasets: function(easingValue) { + var me = this; + + if (core_plugins.notify(me, 'beforeDatasetsDraw', [easingValue]) === false) { + return; + } + + // Draw datasets reversed to support proper line stacking + for (var i = (me.data.datasets || []).length - 1; i >= 0; --i) { + if (me.isDatasetVisible(i)) { + me.drawDataset(i, easingValue); + } + } + + core_plugins.notify(me, 'afterDatasetsDraw', [easingValue]); + }, + + /** + * Draws dataset at index unless a plugin returns `false` to the `beforeDatasetDraw` + * hook, in which case, plugins will not be called on `afterDatasetDraw`. + * @private + */ + drawDataset: function(index, easingValue) { + var me = this; + var meta = me.getDatasetMeta(index); + var args = { + meta: meta, + index: index, + easingValue: easingValue + }; + + if (core_plugins.notify(me, 'beforeDatasetDraw', [args]) === false) { + return; + } + + meta.controller.draw(easingValue); + + core_plugins.notify(me, 'afterDatasetDraw', [args]); + }, + + /** + * Draws tooltip unless a plugin returns `false` to the `beforeTooltipDraw` + * hook, in which case, plugins will not be called on `afterTooltipDraw`. + * @private + */ + _drawTooltip: function(easingValue) { + var me = this; + var tooltip = me.tooltip; + var args = { + tooltip: tooltip, + easingValue: easingValue + }; + + if (core_plugins.notify(me, 'beforeTooltipDraw', [args]) === false) { + return; + } + + tooltip.draw(); + + core_plugins.notify(me, 'afterTooltipDraw', [args]); + }, + + /** + * Get the single element that was clicked on + * @return An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw + */ + getElementAtEvent: function(e) { + return core_interaction.modes.single(this, e); + }, + + getElementsAtEvent: function(e) { + return core_interaction.modes.label(this, e, {intersect: true}); + }, + + getElementsAtXAxis: function(e) { + return core_interaction.modes['x-axis'](this, e, {intersect: true}); + }, + + getElementsAtEventForMode: function(e, mode, options) { + var method = core_interaction.modes[mode]; + if (typeof method === 'function') { + return method(this, e, options); + } + + return []; + }, + + getDatasetAtEvent: function(e) { + return core_interaction.modes.dataset(this, e, {intersect: true}); + }, + + getDatasetMeta: function(datasetIndex) { + var me = this; + var dataset = me.data.datasets[datasetIndex]; + if (!dataset._meta) { + dataset._meta = {}; + } + + var meta = dataset._meta[me.id]; + if (!meta) { + meta = dataset._meta[me.id] = { + type: null, + data: [], + dataset: null, + controller: null, + hidden: null, // See isDatasetVisible() comment + xAxisID: null, + yAxisID: null + }; + } + + return meta; + }, + + getVisibleDatasetCount: function() { + var count = 0; + for (var i = 0, ilen = this.data.datasets.length; i < ilen; ++i) { + if (this.isDatasetVisible(i)) { + count++; + } + } + return count; + }, + + isDatasetVisible: function(datasetIndex) { + var meta = this.getDatasetMeta(datasetIndex); + + // meta.hidden is a per chart dataset hidden flag override with 3 states: if true or false, + // the dataset.hidden value is ignored, else if null, the dataset hidden state is returned. + return typeof meta.hidden === 'boolean' ? !meta.hidden : !this.data.datasets[datasetIndex].hidden; + }, + + generateLegend: function() { + return this.options.legendCallback(this); + }, + + /** + * @private + */ + destroyDatasetMeta: function(datasetIndex) { + var id = this.id; + var dataset = this.data.datasets[datasetIndex]; + var meta = dataset._meta && dataset._meta[id]; + + if (meta) { + meta.controller.destroy(); + delete dataset._meta[id]; + } + }, + + destroy: function() { + var me = this; + var canvas = me.canvas; + var i, ilen; + + me.stop(); + + // dataset controllers need to cleanup associated data + for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me.destroyDatasetMeta(i); + } + + if (canvas) { + me.unbindEvents(); + helpers$1.canvas.clear(me); + platform.releaseContext(me.ctx); + me.canvas = null; + me.ctx = null; + } + + core_plugins.notify(me, 'destroy'); + + delete Chart.instances[me.id]; + }, + + toBase64Image: function() { + return this.canvas.toDataURL.apply(this.canvas, arguments); + }, + + initToolTip: function() { + var me = this; + me.tooltip = new core_tooltip({ + _chart: me, + _chartInstance: me, // deprecated, backward compatibility + _data: me.data, + _options: me.options.tooltips + }, me); + }, + + /** + * @private + */ + bindEvents: function() { + var me = this; + var listeners = me._listeners = {}; + var listener = function() { + me.eventHandler.apply(me, arguments); + }; + + helpers$1.each(me.options.events, function(type) { + platform.addEventListener(me, type, listener); + listeners[type] = listener; + }); + + // Elements used to detect size change should not be injected for non responsive charts. + // See https://github.com/chartjs/Chart.js/issues/2210 + if (me.options.responsive) { + listener = function() { + me.resize(); + }; + + platform.addEventListener(me, 'resize', listener); + listeners.resize = listener; + } + }, + + /** + * @private + */ + unbindEvents: function() { + var me = this; + var listeners = me._listeners; + if (!listeners) { + return; + } + + delete me._listeners; + helpers$1.each(listeners, function(listener, type) { + platform.removeEventListener(me, type, listener); + }); + }, + + updateHoverStyle: function(elements, mode, enabled) { + var method = enabled ? 'setHoverStyle' : 'removeHoverStyle'; + var element, i, ilen; + + for (i = 0, ilen = elements.length; i < ilen; ++i) { + element = elements[i]; + if (element) { + this.getDatasetMeta(element._datasetIndex).controller[method](element); + } + } + }, + + /** + * @private + */ + eventHandler: function(e) { + var me = this; + var tooltip = me.tooltip; + + if (core_plugins.notify(me, 'beforeEvent', [e]) === false) { + return; + } + + // Buffer any update calls so that renders do not occur + me._bufferedRender = true; + me._bufferedRequest = null; + + var changed = me.handleEvent(e); + // for smooth tooltip animations issue #4989 + // the tooltip should be the source of change + // Animation check workaround: + // tooltip._start will be null when tooltip isn't animating + if (tooltip) { + changed = tooltip._start + ? tooltip.handleEvent(e) + : changed | tooltip.handleEvent(e); + } + + core_plugins.notify(me, 'afterEvent', [e]); + + var bufferedRequest = me._bufferedRequest; + if (bufferedRequest) { + // If we have an update that was triggered, we need to do a normal render + me.render(bufferedRequest); + } else if (changed && !me.animating) { + // If entering, leaving, or changing elements, animate the change via pivot + me.stop(); + + // We only need to render at this point. Updating will cause scales to be + // recomputed generating flicker & using more memory than necessary. + me.render({ + duration: me.options.hover.animationDuration, + lazy: true + }); + } + + me._bufferedRender = false; + me._bufferedRequest = null; + + return me; + }, + + /** + * Handle an event + * @private + * @param {IEvent} event the event to handle + * @return {boolean} true if the chart needs to re-render + */ + handleEvent: function(e) { + var me = this; + var options = me.options || {}; + var hoverOptions = options.hover; + var changed = false; + + me.lastActive = me.lastActive || []; + + // Find Active Elements for hover and tooltips + if (e.type === 'mouseout') { + me.active = []; + } else { + me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions); + } + + // Invoke onHover hook + // Need to call with native event here to not break backwards compatibility + helpers$1.callback(options.onHover || options.hover.onHover, [e.native, me.active], me); + + if (e.type === 'mouseup' || e.type === 'click') { + if (options.onClick) { + // Use e.native here for backwards compatibility + options.onClick.call(me, e.native, me.active); + } + } + + // Remove styling for last active (even if it may still be active) + if (me.lastActive.length) { + me.updateHoverStyle(me.lastActive, hoverOptions.mode, false); + } + + // Built in hover styling + if (me.active.length && hoverOptions.mode) { + me.updateHoverStyle(me.active, hoverOptions.mode, true); + } + + changed = !helpers$1.arrayEquals(me.active, me.lastActive); + + // Remember Last Actives + me.lastActive = me.active; + + return changed; + } +}); + +/** + * NOTE(SB) We actually don't use this container anymore but we need to keep it + * for backward compatibility. Though, it can still be useful for plugins that + * would need to work on multiple charts?! + */ +Chart.instances = {}; + +var core_controller = Chart; + +// DEPRECATIONS + +/** + * Provided for backward compatibility, use Chart instead. + * @class Chart.Controller + * @deprecated since version 2.6 + * @todo remove at version 3 + * @private + */ +Chart.Controller = Chart; + +/** + * Provided for backward compatibility, not available anymore. + * @namespace Chart + * @deprecated since version 2.8 + * @todo remove at version 3 + * @private + */ +Chart.types = {}; + +/** + * Provided for backward compatibility, not available anymore. + * @namespace Chart.helpers.configMerge + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ +helpers$1.configMerge = mergeConfig; + +/** + * Provided for backward compatibility, not available anymore. + * @namespace Chart.helpers.scaleMerge + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ +helpers$1.scaleMerge = mergeScaleConfig; + +var core_helpers = function() { + + // -- Basic js utility methods + + helpers$1.where = function(collection, filterCallback) { + if (helpers$1.isArray(collection) && Array.prototype.filter) { + return collection.filter(filterCallback); + } + var filtered = []; + + helpers$1.each(collection, function(item) { + if (filterCallback(item)) { + filtered.push(item); + } + }); + + return filtered; + }; + helpers$1.findIndex = Array.prototype.findIndex ? + function(array, callback, scope) { + return array.findIndex(callback, scope); + } : + function(array, callback, scope) { + scope = scope === undefined ? array : scope; + for (var i = 0, ilen = array.length; i < ilen; ++i) { + if (callback.call(scope, array[i], i, array)) { + return i; + } + } + return -1; + }; + helpers$1.findNextWhere = function(arrayToSearch, filterCallback, startIndex) { + // Default to start of the array + if (helpers$1.isNullOrUndef(startIndex)) { + startIndex = -1; + } + for (var i = startIndex + 1; i < arrayToSearch.length; i++) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)) { + return currentItem; + } + } + }; + helpers$1.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex) { + // Default to end of the array + if (helpers$1.isNullOrUndef(startIndex)) { + startIndex = arrayToSearch.length; + } + for (var i = startIndex - 1; i >= 0; i--) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)) { + return currentItem; + } + } + }; + + // -- Math methods + helpers$1.isNumber = function(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + }; + helpers$1.almostEquals = function(x, y, epsilon) { + return Math.abs(x - y) < epsilon; + }; + helpers$1.almostWhole = function(x, epsilon) { + var rounded = Math.round(x); + return (((rounded - epsilon) < x) && ((rounded + epsilon) > x)); + }; + helpers$1.max = function(array) { + return array.reduce(function(max, value) { + if (!isNaN(value)) { + return Math.max(max, value); + } + return max; + }, Number.NEGATIVE_INFINITY); + }; + helpers$1.min = function(array) { + return array.reduce(function(min, value) { + if (!isNaN(value)) { + return Math.min(min, value); + } + return min; + }, Number.POSITIVE_INFINITY); + }; + helpers$1.sign = Math.sign ? + function(x) { + return Math.sign(x); + } : + function(x) { + x = +x; // convert to a number + if (x === 0 || isNaN(x)) { + return x; + } + return x > 0 ? 1 : -1; + }; + helpers$1.log10 = Math.log10 ? + function(x) { + return Math.log10(x); + } : + function(x) { + var exponent = Math.log(x) * Math.LOG10E; // Math.LOG10E = 1 / Math.LN10. + // Check for whole powers of 10, + // which due to floating point rounding error should be corrected. + var powerOf10 = Math.round(exponent); + var isPowerOf10 = x === Math.pow(10, powerOf10); + + return isPowerOf10 ? powerOf10 : exponent; + }; + helpers$1.toRadians = function(degrees) { + return degrees * (Math.PI / 180); + }; + helpers$1.toDegrees = function(radians) { + return radians * (180 / Math.PI); + }; + + /** + * Returns the number of decimal places + * i.e. the number of digits after the decimal point, of the value of this Number. + * @param {number} x - A number. + * @returns {number} The number of decimal places. + * @private + */ + helpers$1._decimalPlaces = function(x) { + if (!helpers$1.isFinite(x)) { + return; + } + var e = 1; + var p = 0; + while (Math.round(x * e) / e !== x) { + e *= 10; + p++; + } + return p; + }; + + // Gets the angle from vertical upright to the point about a centre. + helpers$1.getAngleFromPoint = function(centrePoint, anglePoint) { + var distanceFromXCenter = anglePoint.x - centrePoint.x; + var distanceFromYCenter = anglePoint.y - centrePoint.y; + var radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + var angle = Math.atan2(distanceFromYCenter, distanceFromXCenter); + + if (angle < (-0.5 * Math.PI)) { + angle += 2.0 * Math.PI; // make sure the returned angle is in the range of (-PI/2, 3PI/2] + } + + return { + angle: angle, + distance: radialDistanceFromCenter + }; + }; + helpers$1.distanceBetweenPoints = function(pt1, pt2) { + return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2)); + }; + + /** + * Provided for backward compatibility, not available anymore + * @function Chart.helpers.aliasPixel + * @deprecated since version 2.8.0 + * @todo remove at version 3 + */ + helpers$1.aliasPixel = function(pixelWidth) { + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }; + + /** + * Returns the aligned pixel value to avoid anti-aliasing blur + * @param {Chart} chart - The chart instance. + * @param {number} pixel - A pixel value. + * @param {number} width - The width of the element. + * @returns {number} The aligned pixel value. + * @private + */ + helpers$1._alignPixel = function(chart, pixel, width) { + var devicePixelRatio = chart.currentDevicePixelRatio; + var halfWidth = width / 2; + return Math.round((pixel - halfWidth) * devicePixelRatio) / devicePixelRatio + halfWidth; + }; + + helpers$1.splineCurve = function(firstPoint, middlePoint, afterPoint, t) { + // Props to Rob Spencer at scaled innovation for his post on splining between points + // http://scaledinnovation.com/analytics/splines/aboutSplines.html + + // This function must also respect "skipped" points + + var previous = firstPoint.skip ? middlePoint : firstPoint; + var current = middlePoint; + var next = afterPoint.skip ? middlePoint : afterPoint; + + var d01 = Math.sqrt(Math.pow(current.x - previous.x, 2) + Math.pow(current.y - previous.y, 2)); + var d12 = Math.sqrt(Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2)); + + var s01 = d01 / (d01 + d12); + var s12 = d12 / (d01 + d12); + + // If all points are the same, s01 & s02 will be inf + s01 = isNaN(s01) ? 0 : s01; + s12 = isNaN(s12) ? 0 : s12; + + var fa = t * s01; // scaling factor for triangle Ta + var fb = t * s12; + + return { + previous: { + x: current.x - fa * (next.x - previous.x), + y: current.y - fa * (next.y - previous.y) + }, + next: { + x: current.x + fb * (next.x - previous.x), + y: current.y + fb * (next.y - previous.y) + } + }; + }; + helpers$1.EPSILON = Number.EPSILON || 1e-14; + helpers$1.splineCurveMonotone = function(points) { + // This function calculates Bézier control points in a similar way than |splineCurve|, + // but preserves monotonicity of the provided data and ensures no local extremums are added + // between the dataset discrete points due to the interpolation. + // See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation + + var pointsWithTangents = (points || []).map(function(point) { + return { + model: point._model, + deltaK: 0, + mK: 0 + }; + }); + + // Calculate slopes (deltaK) and initialize tangents (mK) + var pointsLen = pointsWithTangents.length; + var i, pointBefore, pointCurrent, pointAfter; + for (i = 0; i < pointsLen; ++i) { + pointCurrent = pointsWithTangents[i]; + if (pointCurrent.model.skip) { + continue; + } + + pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; + pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; + if (pointAfter && !pointAfter.model.skip) { + var slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x); + + // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 + pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0; + } + + if (!pointBefore || pointBefore.model.skip) { + pointCurrent.mK = pointCurrent.deltaK; + } else if (!pointAfter || pointAfter.model.skip) { + pointCurrent.mK = pointBefore.deltaK; + } else if (this.sign(pointBefore.deltaK) !== this.sign(pointCurrent.deltaK)) { + pointCurrent.mK = 0; + } else { + pointCurrent.mK = (pointBefore.deltaK + pointCurrent.deltaK) / 2; + } + } + + // Adjust tangents to ensure monotonic properties + var alphaK, betaK, tauK, squaredMagnitude; + for (i = 0; i < pointsLen - 1; ++i) { + pointCurrent = pointsWithTangents[i]; + pointAfter = pointsWithTangents[i + 1]; + if (pointCurrent.model.skip || pointAfter.model.skip) { + continue; + } + + if (helpers$1.almostEquals(pointCurrent.deltaK, 0, this.EPSILON)) { + pointCurrent.mK = pointAfter.mK = 0; + continue; + } + + alphaK = pointCurrent.mK / pointCurrent.deltaK; + betaK = pointAfter.mK / pointCurrent.deltaK; + squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2); + if (squaredMagnitude <= 9) { + continue; + } + + tauK = 3 / Math.sqrt(squaredMagnitude); + pointCurrent.mK = alphaK * tauK * pointCurrent.deltaK; + pointAfter.mK = betaK * tauK * pointCurrent.deltaK; + } + + // Compute control points + var deltaX; + for (i = 0; i < pointsLen; ++i) { + pointCurrent = pointsWithTangents[i]; + if (pointCurrent.model.skip) { + continue; + } + + pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; + pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; + if (pointBefore && !pointBefore.model.skip) { + deltaX = (pointCurrent.model.x - pointBefore.model.x) / 3; + pointCurrent.model.controlPointPreviousX = pointCurrent.model.x - deltaX; + pointCurrent.model.controlPointPreviousY = pointCurrent.model.y - deltaX * pointCurrent.mK; + } + if (pointAfter && !pointAfter.model.skip) { + deltaX = (pointAfter.model.x - pointCurrent.model.x) / 3; + pointCurrent.model.controlPointNextX = pointCurrent.model.x + deltaX; + pointCurrent.model.controlPointNextY = pointCurrent.model.y + deltaX * pointCurrent.mK; + } + } + }; + helpers$1.nextItem = function(collection, index, loop) { + if (loop) { + return index >= collection.length - 1 ? collection[0] : collection[index + 1]; + } + return index >= collection.length - 1 ? collection[collection.length - 1] : collection[index + 1]; + }; + helpers$1.previousItem = function(collection, index, loop) { + if (loop) { + return index <= 0 ? collection[collection.length - 1] : collection[index - 1]; + } + return index <= 0 ? collection[0] : collection[index - 1]; + }; + // Implementation of the nice number algorithm used in determining where axis labels will go + helpers$1.niceNum = function(range, round) { + var exponent = Math.floor(helpers$1.log10(range)); + var fraction = range / Math.pow(10, exponent); + var niceFraction; + + if (round) { + if (fraction < 1.5) { + niceFraction = 1; + } else if (fraction < 3) { + niceFraction = 2; + } else if (fraction < 7) { + niceFraction = 5; + } else { + niceFraction = 10; + } + } else if (fraction <= 1.0) { + niceFraction = 1; + } else if (fraction <= 2) { + niceFraction = 2; + } else if (fraction <= 5) { + niceFraction = 5; + } else { + niceFraction = 10; + } + + return niceFraction * Math.pow(10, exponent); + }; + // Request animation polyfill - https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + helpers$1.requestAnimFrame = (function() { + if (typeof window === 'undefined') { + return function(callback) { + callback(); + }; + } + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + }()); + // -- DOM methods + helpers$1.getRelativePosition = function(evt, chart) { + var mouseX, mouseY; + var e = evt.originalEvent || evt; + var canvas = evt.target || evt.srcElement; + var boundingRect = canvas.getBoundingClientRect(); + + var touches = e.touches; + if (touches && touches.length > 0) { + mouseX = touches[0].clientX; + mouseY = touches[0].clientY; + + } else { + mouseX = e.clientX; + mouseY = e.clientY; + } + + // Scale mouse coordinates into canvas coordinates + // by following the pattern laid out by 'jerryj' in the comments of + // https://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/ + var paddingLeft = parseFloat(helpers$1.getStyle(canvas, 'padding-left')); + var paddingTop = parseFloat(helpers$1.getStyle(canvas, 'padding-top')); + var paddingRight = parseFloat(helpers$1.getStyle(canvas, 'padding-right')); + var paddingBottom = parseFloat(helpers$1.getStyle(canvas, 'padding-bottom')); + var width = boundingRect.right - boundingRect.left - paddingLeft - paddingRight; + var height = boundingRect.bottom - boundingRect.top - paddingTop - paddingBottom; + + // We divide by the current device pixel ratio, because the canvas is scaled up by that amount in each direction. However + // the backend model is in unscaled coordinates. Since we are going to deal with our model coordinates, we go back here + mouseX = Math.round((mouseX - boundingRect.left - paddingLeft) / (width) * canvas.width / chart.currentDevicePixelRatio); + mouseY = Math.round((mouseY - boundingRect.top - paddingTop) / (height) * canvas.height / chart.currentDevicePixelRatio); + + return { + x: mouseX, + y: mouseY + }; + + }; + + // Private helper function to convert max-width/max-height values that may be percentages into a number + function parseMaxStyle(styleValue, node, parentProperty) { + var valueInPixels; + if (typeof styleValue === 'string') { + valueInPixels = parseInt(styleValue, 10); + + if (styleValue.indexOf('%') !== -1) { + // percentage * size in dimension + valueInPixels = valueInPixels / 100 * node.parentNode[parentProperty]; + } + } else { + valueInPixels = styleValue; + } + + return valueInPixels; + } + + /** + * Returns if the given value contains an effective constraint. + * @private + */ + function isConstrainedValue(value) { + return value !== undefined && value !== null && value !== 'none'; + } + + /** + * Returns the max width or height of the given DOM node in a cross-browser compatible fashion + * @param {HTMLElement} domNode - the node to check the constraint on + * @param {string} maxStyle - the style that defines the maximum for the direction we are using ('max-width' / 'max-height') + * @param {string} percentageProperty - property of parent to use when calculating width as a percentage + * @see {@link https://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser} + */ + function getConstraintDimension(domNode, maxStyle, percentageProperty) { + var view = document.defaultView; + var parentNode = helpers$1._getParentNode(domNode); + var constrainedNode = view.getComputedStyle(domNode)[maxStyle]; + var constrainedContainer = view.getComputedStyle(parentNode)[maxStyle]; + var hasCNode = isConstrainedValue(constrainedNode); + var hasCContainer = isConstrainedValue(constrainedContainer); + var infinity = Number.POSITIVE_INFINITY; + + if (hasCNode || hasCContainer) { + return Math.min( + hasCNode ? parseMaxStyle(constrainedNode, domNode, percentageProperty) : infinity, + hasCContainer ? parseMaxStyle(constrainedContainer, parentNode, percentageProperty) : infinity); + } + + return 'none'; + } + // returns Number or undefined if no constraint + helpers$1.getConstraintWidth = function(domNode) { + return getConstraintDimension(domNode, 'max-width', 'clientWidth'); + }; + // returns Number or undefined if no constraint + helpers$1.getConstraintHeight = function(domNode) { + return getConstraintDimension(domNode, 'max-height', 'clientHeight'); + }; + /** + * @private + */ + helpers$1._calculatePadding = function(container, padding, parentDimension) { + padding = helpers$1.getStyle(container, padding); + + return padding.indexOf('%') > -1 ? parentDimension * parseInt(padding, 10) / 100 : parseInt(padding, 10); + }; + /** + * @private + */ + helpers$1._getParentNode = function(domNode) { + var parent = domNode.parentNode; + if (parent && parent.toString() === '[object ShadowRoot]') { + parent = parent.host; + } + return parent; + }; + helpers$1.getMaximumWidth = function(domNode) { + var container = helpers$1._getParentNode(domNode); + if (!container) { + return domNode.clientWidth; + } + + var clientWidth = container.clientWidth; + var paddingLeft = helpers$1._calculatePadding(container, 'padding-left', clientWidth); + var paddingRight = helpers$1._calculatePadding(container, 'padding-right', clientWidth); + + var w = clientWidth - paddingLeft - paddingRight; + var cw = helpers$1.getConstraintWidth(domNode); + return isNaN(cw) ? w : Math.min(w, cw); + }; + helpers$1.getMaximumHeight = function(domNode) { + var container = helpers$1._getParentNode(domNode); + if (!container) { + return domNode.clientHeight; + } + + var clientHeight = container.clientHeight; + var paddingTop = helpers$1._calculatePadding(container, 'padding-top', clientHeight); + var paddingBottom = helpers$1._calculatePadding(container, 'padding-bottom', clientHeight); + + var h = clientHeight - paddingTop - paddingBottom; + var ch = helpers$1.getConstraintHeight(domNode); + return isNaN(ch) ? h : Math.min(h, ch); + }; + helpers$1.getStyle = function(el, property) { + return el.currentStyle ? + el.currentStyle[property] : + document.defaultView.getComputedStyle(el, null).getPropertyValue(property); + }; + helpers$1.retinaScale = function(chart, forceRatio) { + var pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1; + if (pixelRatio === 1) { + return; + } + + var canvas = chart.canvas; + var height = chart.height; + var width = chart.width; + + canvas.height = height * pixelRatio; + canvas.width = width * pixelRatio; + chart.ctx.scale(pixelRatio, pixelRatio); + + // If no style has been set on the canvas, the render size is used as display size, + // making the chart visually bigger, so let's enforce it to the "correct" values. + // See https://github.com/chartjs/Chart.js/issues/3575 + if (!canvas.style.height && !canvas.style.width) { + canvas.style.height = height + 'px'; + canvas.style.width = width + 'px'; + } + }; + // -- Canvas methods + helpers$1.fontString = function(pixelSize, fontStyle, fontFamily) { + return fontStyle + ' ' + pixelSize + 'px ' + fontFamily; + }; + helpers$1.longestText = function(ctx, font, arrayOfThings, cache) { + cache = cache || {}; + var data = cache.data = cache.data || {}; + var gc = cache.garbageCollect = cache.garbageCollect || []; + + if (cache.font !== font) { + data = cache.data = {}; + gc = cache.garbageCollect = []; + cache.font = font; + } + + ctx.font = font; + var longest = 0; + helpers$1.each(arrayOfThings, function(thing) { + // Undefined strings and arrays should not be measured + if (thing !== undefined && thing !== null && helpers$1.isArray(thing) !== true) { + longest = helpers$1.measureText(ctx, data, gc, longest, thing); + } else if (helpers$1.isArray(thing)) { + // if it is an array lets measure each element + // to do maybe simplify this function a bit so we can do this more recursively? + helpers$1.each(thing, function(nestedThing) { + // Undefined strings and arrays should not be measured + if (nestedThing !== undefined && nestedThing !== null && !helpers$1.isArray(nestedThing)) { + longest = helpers$1.measureText(ctx, data, gc, longest, nestedThing); + } + }); + } + }); + + var gcLen = gc.length / 2; + if (gcLen > arrayOfThings.length) { + for (var i = 0; i < gcLen; i++) { + delete data[gc[i]]; + } + gc.splice(0, gcLen); + } + return longest; + }; + helpers$1.measureText = function(ctx, data, gc, longest, string) { + var textWidth = data[string]; + if (!textWidth) { + textWidth = data[string] = ctx.measureText(string).width; + gc.push(string); + } + if (textWidth > longest) { + longest = textWidth; + } + return longest; + }; + helpers$1.numberOfLabelLines = function(arrayOfThings) { + var numberOfLines = 1; + helpers$1.each(arrayOfThings, function(thing) { + if (helpers$1.isArray(thing)) { + if (thing.length > numberOfLines) { + numberOfLines = thing.length; + } + } + }); + return numberOfLines; + }; + + helpers$1.color = !chartjsColor ? + function(value) { + console.error('Color.js not found!'); + return value; + } : + function(value) { + /* global CanvasGradient */ + if (value instanceof CanvasGradient) { + value = core_defaults.global.defaultColor; + } + + return chartjsColor(value); + }; + + helpers$1.getHoverColor = function(colorValue) { + /* global CanvasPattern */ + return (colorValue instanceof CanvasPattern || colorValue instanceof CanvasGradient) ? + colorValue : + helpers$1.color(colorValue).saturate(0.5).darken(0.1).rgbString(); + }; +}; + +function abstract() { + throw new Error( + 'This method is not implemented: either no adapter can ' + + 'be found or an incomplete integration was provided.' + ); +} + +/** + * Date adapter (current used by the time scale) + * @namespace Chart._adapters._date + * @memberof Chart._adapters + * @private + */ + +/** + * Currently supported unit string values. + * @typedef {('millisecond'|'second'|'minute'|'hour'|'day'|'week'|'month'|'quarter'|'year')} + * @memberof Chart._adapters._date + * @name Unit + */ + +/** + * @class + */ +function DateAdapter(options) { + this.options = options || {}; +} + +helpers$1.extend(DateAdapter.prototype, /** @lends DateAdapter */ { + /** + * Returns a map of time formats for the supported formatting units defined + * in Unit as well as 'datetime' representing a detailed date/time string. + * @returns {{string: string}} + */ + formats: abstract, + + /** + * Parses the given `value` and return the associated timestamp. + * @param {any} value - the value to parse (usually comes from the data) + * @param {string} [format] - the expected data format + * @returns {(number|null)} + * @function + */ + parse: abstract, + + /** + * Returns the formatted date in the specified `format` for a given `timestamp`. + * @param {number} timestamp - the timestamp to format + * @param {string} format - the date/time token + * @return {string} + * @function + */ + format: abstract, + + /** + * Adds the specified `amount` of `unit` to the given `timestamp`. + * @param {number} timestamp - the input timestamp + * @param {number} amount - the amount to add + * @param {Unit} unit - the unit as string + * @return {number} + * @function + */ + add: abstract, + + /** + * Returns the number of `unit` between the given timestamps. + * @param {number} max - the input timestamp (reference) + * @param {number} min - the timestamp to substract + * @param {Unit} unit - the unit as string + * @return {number} + * @function + */ + diff: abstract, + + /** + * Returns start of `unit` for the given `timestamp`. + * @param {number} timestamp - the input timestamp + * @param {Unit} unit - the unit as string + * @param {number} [weekday] - the ISO day of the week with 1 being Monday + * and 7 being Sunday (only needed if param *unit* is `isoWeek`). + * @function + */ + startOf: abstract, + + /** + * Returns end of `unit` for the given `timestamp`. + * @param {number} timestamp - the input timestamp + * @param {Unit} unit - the unit as string + * @function + */ + endOf: abstract, + + // DEPRECATIONS + + /** + * Provided for backward compatibility for scale.getValueForPixel(), + * this method should be overridden only by the moment adapter. + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ + _create: function(value) { + return value; + } +}); + +DateAdapter.override = function(members) { + helpers$1.extend(DateAdapter.prototype, members); +}; + +var _date = DateAdapter; + +var core_adapters = { + _date: _date +}; + +/** + * Namespace to hold static tick generation functions + * @namespace Chart.Ticks + */ +var core_ticks = { + /** + * Namespace to hold formatters for different types of ticks + * @namespace Chart.Ticks.formatters + */ + formatters: { + /** + * Formatter for value labels + * @method Chart.Ticks.formatters.values + * @param value the value to display + * @return {string|string[]} the label to display + */ + values: function(value) { + return helpers$1.isArray(value) ? value : '' + value; + }, + + /** + * Formatter for linear numeric ticks + * @method Chart.Ticks.formatters.linear + * @param tickValue {number} the value to be formatted + * @param index {number} the position of the tickValue parameter in the ticks array + * @param ticks {number[]} the list of ticks being converted + * @return {string} string representation of the tickValue parameter + */ + linear: function(tickValue, index, ticks) { + // If we have lots of ticks, don't use the ones + var delta = ticks.length > 3 ? ticks[2] - ticks[1] : ticks[1] - ticks[0]; + + // If we have a number like 2.5 as the delta, figure out how many decimal places we need + if (Math.abs(delta) > 1) { + if (tickValue !== Math.floor(tickValue)) { + // not an integer + delta = tickValue - Math.floor(tickValue); + } + } + + var logDelta = helpers$1.log10(Math.abs(delta)); + var tickString = ''; + + if (tickValue !== 0) { + var maxTick = Math.max(Math.abs(ticks[0]), Math.abs(ticks[ticks.length - 1])); + if (maxTick < 1e-4) { // all ticks are small numbers; use scientific notation + var logTick = helpers$1.log10(Math.abs(tickValue)); + tickString = tickValue.toExponential(Math.floor(logTick) - Math.floor(logDelta)); + } else { + var numDecimal = -1 * Math.floor(logDelta); + numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places + tickString = tickValue.toFixed(numDecimal); + } + } else { + tickString = '0'; // never show decimal places for 0 + } + + return tickString; + }, + + logarithmic: function(tickValue, index, ticks) { + var remain = tickValue / (Math.pow(10, Math.floor(helpers$1.log10(tickValue)))); + + if (tickValue === 0) { + return '0'; + } else if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === ticks.length - 1) { + return tickValue.toExponential(); + } + return ''; + } + } +}; + +var valueOrDefault$9 = helpers$1.valueOrDefault; +var valueAtIndexOrDefault = helpers$1.valueAtIndexOrDefault; + +core_defaults._set('scale', { + display: true, + position: 'left', + offset: false, + + // grid line settings + gridLines: { + display: true, + color: 'rgba(0, 0, 0, 0.1)', + lineWidth: 1, + drawBorder: true, + drawOnChartArea: true, + drawTicks: true, + tickMarkLength: 10, + zeroLineWidth: 1, + zeroLineColor: 'rgba(0,0,0,0.25)', + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, + offsetGridLines: false, + borderDash: [], + borderDashOffset: 0.0 + }, + + // scale label + scaleLabel: { + // display property + display: false, + + // actual label + labelString: '', + + // top/bottom padding + padding: { + top: 4, + bottom: 4 + } + }, + + // label settings + ticks: { + beginAtZero: false, + minRotation: 0, + maxRotation: 50, + mirror: false, + padding: 0, + reverse: false, + display: true, + autoSkip: true, + autoSkipPadding: 0, + labelOffset: 0, + // We pass through arrays to be rendered as multiline labels, we convert Others to strings here. + callback: core_ticks.formatters.values, + minor: {}, + major: {} + } +}); + +function labelsFromTicks(ticks) { + var labels = []; + var i, ilen; + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + labels.push(ticks[i].label); + } + + return labels; +} + +function getPixelForGridLine(scale, index, offsetGridLines) { + var lineValue = scale.getPixelForTick(index); + + if (offsetGridLines) { + if (scale.getTicks().length === 1) { + lineValue -= scale.isHorizontal() ? + Math.max(lineValue - scale.left, scale.right - lineValue) : + Math.max(lineValue - scale.top, scale.bottom - lineValue); + } else if (index === 0) { + lineValue -= (scale.getPixelForTick(1) - lineValue) / 2; + } else { + lineValue -= (lineValue - scale.getPixelForTick(index - 1)) / 2; + } + } + return lineValue; +} + +function computeTextSize(context, tick, font) { + return helpers$1.isArray(tick) ? + helpers$1.longestText(context, font, tick) : + context.measureText(tick).width; +} + +var core_scale = core_element.extend({ + /** + * Get the padding needed for the scale + * @method getPadding + * @private + * @returns {Padding} the necessary padding + */ + getPadding: function() { + var me = this; + return { + left: me.paddingLeft || 0, + top: me.paddingTop || 0, + right: me.paddingRight || 0, + bottom: me.paddingBottom || 0 + }; + }, + + /** + * Returns the scale tick objects ({label, major}) + * @since 2.7 + */ + getTicks: function() { + return this._ticks; + }, + + // These methods are ordered by lifecyle. Utilities then follow. + // Any function defined here is inherited by all scale types. + // Any function can be extended by the scale type + + mergeTicksOptions: function() { + var ticks = this.options.ticks; + if (ticks.minor === false) { + ticks.minor = { + display: false + }; + } + if (ticks.major === false) { + ticks.major = { + display: false + }; + } + for (var key in ticks) { + if (key !== 'major' && key !== 'minor') { + if (typeof ticks.minor[key] === 'undefined') { + ticks.minor[key] = ticks[key]; + } + if (typeof ticks.major[key] === 'undefined') { + ticks.major[key] = ticks[key]; + } + } + } + }, + beforeUpdate: function() { + helpers$1.callback(this.options.beforeUpdate, [this]); + }, + + update: function(maxWidth, maxHeight, margins) { + var me = this; + var i, ilen, labels, label, ticks, tick; + + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); + + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = helpers$1.extend({ + left: 0, + right: 0, + top: 0, + bottom: 0 + }, margins); + + me._maxLabelLines = 0; + me.longestLabelWidth = 0; + me.longestTextCache = me.longestTextCache || {}; + + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + + // Data min/max + me.beforeDataLimits(); + me.determineDataLimits(); + me.afterDataLimits(); + + // Ticks - `this.ticks` is now DEPRECATED! + // Internal ticks are now stored as objects in the PRIVATE `this._ticks` member + // and must not be accessed directly from outside this class. `this.ticks` being + // around for long time and not marked as private, we can't change its structure + // without unexpected breaking changes. If you need to access the scale ticks, + // use scale.getTicks() instead. + + me.beforeBuildTicks(); + + // New implementations should return an array of objects but for BACKWARD COMPAT, + // we still support no return (`this.ticks` internally set by calling this method). + ticks = me.buildTicks() || []; + + // Allow modification of ticks in callback. + ticks = me.afterBuildTicks(ticks) || ticks; + + me.beforeTickToLabelConversion(); + + // New implementations should return the formatted tick labels but for BACKWARD + // COMPAT, we still support no return (`this.ticks` internally changed by calling + // this method and supposed to contain only string values). + labels = me.convertTicksToLabels(ticks) || me.ticks; + + me.afterTickToLabelConversion(); + + me.ticks = labels; // BACKWARD COMPATIBILITY + + // IMPORTANT: from this point, we consider that `this.ticks` will NEVER change! + + // BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`) + for (i = 0, ilen = labels.length; i < ilen; ++i) { + label = labels[i]; + tick = ticks[i]; + if (!tick) { + ticks.push(tick = { + label: label, + major: false + }); + } else { + tick.label = label; + } + } + + me._ticks = ticks; + + // Tick Rotation + me.beforeCalculateTickRotation(); + me.calculateTickRotation(); + me.afterCalculateTickRotation(); + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); + // + me.afterUpdate(); + + return me.minSize; + + }, + afterUpdate: function() { + helpers$1.callback(this.options.afterUpdate, [this]); + }, + + // + + beforeSetDimensions: function() { + helpers$1.callback(this.options.beforeSetDimensions, [this]); + }, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } + + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + }, + afterSetDimensions: function() { + helpers$1.callback(this.options.afterSetDimensions, [this]); + }, + + // Data limits + beforeDataLimits: function() { + helpers$1.callback(this.options.beforeDataLimits, [this]); + }, + determineDataLimits: helpers$1.noop, + afterDataLimits: function() { + helpers$1.callback(this.options.afterDataLimits, [this]); + }, + + // + beforeBuildTicks: function() { + helpers$1.callback(this.options.beforeBuildTicks, [this]); + }, + buildTicks: helpers$1.noop, + afterBuildTicks: function(ticks) { + var me = this; + // ticks is empty for old axis implementations here + if (helpers$1.isArray(ticks) && ticks.length) { + return helpers$1.callback(me.options.afterBuildTicks, [me, ticks]); + } + // Support old implementations (that modified `this.ticks` directly in buildTicks) + me.ticks = helpers$1.callback(me.options.afterBuildTicks, [me, me.ticks]) || me.ticks; + return ticks; + }, + + beforeTickToLabelConversion: function() { + helpers$1.callback(this.options.beforeTickToLabelConversion, [this]); + }, + convertTicksToLabels: function() { + var me = this; + // Convert ticks to strings + var tickOpts = me.options.ticks; + me.ticks = me.ticks.map(tickOpts.userCallback || tickOpts.callback, this); + }, + afterTickToLabelConversion: function() { + helpers$1.callback(this.options.afterTickToLabelConversion, [this]); + }, + + // + + beforeCalculateTickRotation: function() { + helpers$1.callback(this.options.beforeCalculateTickRotation, [this]); + }, + calculateTickRotation: function() { + var me = this; + var context = me.ctx; + var tickOpts = me.options.ticks; + var labels = labelsFromTicks(me._ticks); + + // Get the width of each grid by calculating the difference + // between x offsets between 0 and 1. + var tickFont = helpers$1.options._parseFont(tickOpts); + context.font = tickFont.string; + + var labelRotation = tickOpts.minRotation || 0; + + if (labels.length && me.options.display && me.isHorizontal()) { + var originalLabelWidth = helpers$1.longestText(context, tickFont.string, labels, me.longestTextCache); + var labelWidth = originalLabelWidth; + var cosRotation, sinRotation; + + // Allow 3 pixels x2 padding either side for label readability + var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; + + // Max label rotation can be set or default to 90 - also act as a loop counter + while (labelWidth > tickWidth && labelRotation < tickOpts.maxRotation) { + var angleRadians = helpers$1.toRadians(labelRotation); + cosRotation = Math.cos(angleRadians); + sinRotation = Math.sin(angleRadians); + + if (sinRotation * originalLabelWidth > me.maxHeight) { + // go back one step + labelRotation--; + break; + } + + labelRotation++; + labelWidth = cosRotation * originalLabelWidth; + } + } + + me.labelRotation = labelRotation; + }, + afterCalculateTickRotation: function() { + helpers$1.callback(this.options.afterCalculateTickRotation, [this]); + }, + + // + + beforeFit: function() { + helpers$1.callback(this.options.beforeFit, [this]); + }, + fit: function() { + var me = this; + // Reset + var minSize = me.minSize = { + width: 0, + height: 0 + }; + + var labels = labelsFromTicks(me._ticks); + + var opts = me.options; + var tickOpts = opts.ticks; + var scaleLabelOpts = opts.scaleLabel; + var gridLineOpts = opts.gridLines; + var display = me._isVisible(); + var position = opts.position; + var isHorizontal = me.isHorizontal(); + + var parseFont = helpers$1.options._parseFont; + var tickFont = parseFont(tickOpts); + var tickMarkLength = opts.gridLines.tickMarkLength; + + // Width + if (isHorizontal) { + // subtract the margins to line up with the chartArea if we are a full width scale + minSize.width = me.isFullWidth() ? me.maxWidth - me.margins.left - me.margins.right : me.maxWidth; + } else { + minSize.width = display && gridLineOpts.drawTicks ? tickMarkLength : 0; + } + + // height + if (isHorizontal) { + minSize.height = display && gridLineOpts.drawTicks ? tickMarkLength : 0; + } else { + minSize.height = me.maxHeight; // fill all the height + } + + // Are we showing a title for the scale? + if (scaleLabelOpts.display && display) { + var scaleLabelFont = parseFont(scaleLabelOpts); + var scaleLabelPadding = helpers$1.options.toPadding(scaleLabelOpts.padding); + var deltaHeight = scaleLabelFont.lineHeight + scaleLabelPadding.height; + + if (isHorizontal) { + minSize.height += deltaHeight; + } else { + minSize.width += deltaHeight; + } + } + + // Don't bother fitting the ticks if we are not showing the labels + if (tickOpts.display && display) { + var largestTextWidth = helpers$1.longestText(me.ctx, tickFont.string, labels, me.longestTextCache); + var tallestLabelHeightInLines = helpers$1.numberOfLabelLines(labels); + var lineSpace = tickFont.size * 0.5; + var tickPadding = me.options.ticks.padding; + + // Store max number of lines and widest label for _autoSkip + me._maxLabelLines = tallestLabelHeightInLines; + me.longestLabelWidth = largestTextWidth; + + if (isHorizontal) { + var angleRadians = helpers$1.toRadians(me.labelRotation); + var cosRotation = Math.cos(angleRadians); + var sinRotation = Math.sin(angleRadians); + + // TODO - improve this calculation + var labelHeight = (sinRotation * largestTextWidth) + + (tickFont.lineHeight * tallestLabelHeightInLines) + + lineSpace; // padding + + minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding); + + me.ctx.font = tickFont.string; + var firstLabelWidth = computeTextSize(me.ctx, labels[0], tickFont.string); + var lastLabelWidth = computeTextSize(me.ctx, labels[labels.length - 1], tickFont.string); + var offsetLeft = me.getPixelForTick(0) - me.left; + var offsetRight = me.right - me.getPixelForTick(labels.length - 1); + var paddingLeft, paddingRight; + + // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned + // which means that the right padding is dominated by the font height + if (me.labelRotation !== 0) { + paddingLeft = position === 'bottom' ? (cosRotation * firstLabelWidth) : (cosRotation * lineSpace); + paddingRight = position === 'bottom' ? (cosRotation * lineSpace) : (cosRotation * lastLabelWidth); + } else { + paddingLeft = firstLabelWidth / 2; + paddingRight = lastLabelWidth / 2; + } + me.paddingLeft = Math.max(paddingLeft - offsetLeft, 0) + 3; // add 3 px to move away from canvas edges + me.paddingRight = Math.max(paddingRight - offsetRight, 0) + 3; + } else { + // A vertical axis is more constrained by the width. Labels are the + // dominant factor here, so get that length first and account for padding + if (tickOpts.mirror) { + largestTextWidth = 0; + } else { + // use lineSpace for consistency with horizontal axis + // tickPadding is not implemented for horizontal + largestTextWidth += tickPadding + lineSpace; + } + + minSize.width = Math.min(me.maxWidth, minSize.width + largestTextWidth); + + me.paddingTop = tickFont.size / 2; + me.paddingBottom = tickFont.size / 2; + } + } + + me.handleMargins(); + + me.width = minSize.width; + me.height = minSize.height; + }, + + /** + * Handle margins and padding interactions + * @private + */ + handleMargins: function() { + var me = this; + if (me.margins) { + me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); + me.paddingTop = Math.max(me.paddingTop - me.margins.top, 0); + me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); + me.paddingBottom = Math.max(me.paddingBottom - me.margins.bottom, 0); + } + }, + + afterFit: function() { + helpers$1.callback(this.options.afterFit, [this]); + }, + + // Shared Methods + isHorizontal: function() { + return this.options.position === 'top' || this.options.position === 'bottom'; + }, + isFullWidth: function() { + return (this.options.fullWidth); + }, + + // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not + getRightValue: function(rawValue) { + // Null and undefined values first + if (helpers$1.isNullOrUndef(rawValue)) { + return NaN; + } + // isNaN(object) returns true, so make sure NaN is checking for a number; Discard Infinite values + if ((typeof rawValue === 'number' || rawValue instanceof Number) && !isFinite(rawValue)) { + return NaN; + } + // If it is in fact an object, dive in one more level + if (rawValue) { + if (this.isHorizontal()) { + if (rawValue.x !== undefined) { + return this.getRightValue(rawValue.x); + } + } else if (rawValue.y !== undefined) { + return this.getRightValue(rawValue.y); + } + } + + // Value is good, return it + return rawValue; + }, + + /** + * Used to get the value to display in the tooltip for the data at the given index + * @param index + * @param datasetIndex + */ + getLabelForIndex: helpers$1.noop, + + /** + * Returns the location of the given data point. Value can either be an index or a numerical value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param value + * @param index + * @param datasetIndex + */ + getPixelForValue: helpers$1.noop, + + /** + * Used to get the data value from a given pixel. This is the inverse of getPixelForValue + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param pixel + */ + getValueForPixel: helpers$1.noop, + + /** + * Returns the location of the tick at the given index + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ + getPixelForTick: function(index) { + var me = this; + var offset = me.options.offset; + if (me.isHorizontal()) { + var innerWidth = me.width - (me.paddingLeft + me.paddingRight); + var tickWidth = innerWidth / Math.max((me._ticks.length - (offset ? 0 : 1)), 1); + var pixel = (tickWidth * index) + me.paddingLeft; + + if (offset) { + pixel += tickWidth / 2; + } + + var finalVal = me.left + pixel; + finalVal += me.isFullWidth() ? me.margins.left : 0; + return finalVal; + } + var innerHeight = me.height - (me.paddingTop + me.paddingBottom); + return me.top + (index * (innerHeight / (me._ticks.length - 1))); + }, + + /** + * Utility for getting the pixel location of a percentage of scale + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ + getPixelForDecimal: function(decimal) { + var me = this; + if (me.isHorizontal()) { + var innerWidth = me.width - (me.paddingLeft + me.paddingRight); + var valueOffset = (innerWidth * decimal) + me.paddingLeft; + + var finalVal = me.left + valueOffset; + finalVal += me.isFullWidth() ? me.margins.left : 0; + return finalVal; + } + return me.top + (decimal * me.height); + }, + + /** + * Returns the pixel for the minimum chart value + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ + getBasePixel: function() { + return this.getPixelForValue(this.getBaseValue()); + }, + + getBaseValue: function() { + var me = this; + var min = me.min; + var max = me.max; + + return me.beginAtZero ? 0 : + min < 0 && max < 0 ? max : + min > 0 && max > 0 ? min : + 0; + }, + + /** + * Returns a subset of ticks to be plotted to avoid overlapping labels. + * @private + */ + _autoSkip: function(ticks) { + var me = this; + var isHorizontal = me.isHorizontal(); + var optionTicks = me.options.ticks.minor; + var tickCount = ticks.length; + var skipRatio = false; + var maxTicks = optionTicks.maxTicksLimit; + + // Total space needed to display all ticks. First and last ticks are + // drawn as their center at end of axis, so tickCount-1 + var ticksLength = me._tickSize() * (tickCount - 1); + + // Axis length + var axisLength = isHorizontal + ? me.width - (me.paddingLeft + me.paddingRight) + : me.height - (me.paddingTop + me.PaddingBottom); + + var result = []; + var i, tick; + + if (ticksLength > axisLength) { + skipRatio = 1 + Math.floor(ticksLength / axisLength); + } + + // if they defined a max number of optionTicks, + // increase skipRatio until that number is met + if (tickCount > maxTicks) { + skipRatio = Math.max(skipRatio, 1 + Math.floor(tickCount / maxTicks)); + } + + for (i = 0; i < tickCount; i++) { + tick = ticks[i]; + + if (skipRatio > 1 && i % skipRatio > 0) { + // leave tick in place but make sure it's not displayed (#4635) + delete tick.label; + } + result.push(tick); + } + return result; + }, + + /** + * @private + */ + _tickSize: function() { + var me = this; + var isHorizontal = me.isHorizontal(); + var optionTicks = me.options.ticks.minor; + + // Calculate space needed by label in axis direction. + var rot = helpers$1.toRadians(me.labelRotation); + var cos = Math.abs(Math.cos(rot)); + var sin = Math.abs(Math.sin(rot)); + + var padding = optionTicks.autoSkipPadding || 0; + var w = (me.longestLabelWidth + padding) || 0; + + var tickFont = helpers$1.options._parseFont(optionTicks); + var h = (me._maxLabelLines * tickFont.lineHeight + padding) || 0; + + // Calculate space needed for 1 tick in axis direction. + return isHorizontal + ? h * cos > w * sin ? w / cos : h / sin + : h * sin < w * cos ? h / cos : w / sin; + }, + + /** + * @private + */ + _isVisible: function() { + var me = this; + var chart = me.chart; + var display = me.options.display; + var i, ilen, meta; + + if (display !== 'auto') { + return !!display; + } + + // When 'auto', the scale is visible if at least one associated dataset is visible. + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + if (chart.isDatasetVisible(i)) { + meta = chart.getDatasetMeta(i); + if (meta.xAxisID === me.id || meta.yAxisID === me.id) { + return true; + } + } + } + + return false; + }, + + /** + * Actually draw the scale on the canvas + * @param {object} chartArea - the area of the chart to draw full grid lines on + */ + draw: function(chartArea) { + var me = this; + var options = me.options; + + if (!me._isVisible()) { + return; + } + + var chart = me.chart; + var context = me.ctx; + var globalDefaults = core_defaults.global; + var defaultFontColor = globalDefaults.defaultFontColor; + var optionTicks = options.ticks.minor; + var optionMajorTicks = options.ticks.major || optionTicks; + var gridLines = options.gridLines; + var scaleLabel = options.scaleLabel; + var position = options.position; + + var isRotated = me.labelRotation !== 0; + var isMirrored = optionTicks.mirror; + var isHorizontal = me.isHorizontal(); + + var parseFont = helpers$1.options._parseFont; + var ticks = optionTicks.display && optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); + var tickFontColor = valueOrDefault$9(optionTicks.fontColor, defaultFontColor); + var tickFont = parseFont(optionTicks); + var lineHeight = tickFont.lineHeight; + var majorTickFontColor = valueOrDefault$9(optionMajorTicks.fontColor, defaultFontColor); + var majorTickFont = parseFont(optionMajorTicks); + var tickPadding = optionTicks.padding; + var labelOffset = optionTicks.labelOffset; + + var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; + + var scaleLabelFontColor = valueOrDefault$9(scaleLabel.fontColor, defaultFontColor); + var scaleLabelFont = parseFont(scaleLabel); + var scaleLabelPadding = helpers$1.options.toPadding(scaleLabel.padding); + var labelRotationRadians = helpers$1.toRadians(me.labelRotation); + + var itemsToDraw = []; + + var axisWidth = gridLines.drawBorder ? valueAtIndexOrDefault(gridLines.lineWidth, 0, 0) : 0; + var alignPixel = helpers$1._alignPixel; + var borderValue, tickStart, tickEnd; + + if (position === 'top') { + borderValue = alignPixel(chart, me.bottom, axisWidth); + tickStart = me.bottom - tl; + tickEnd = borderValue - axisWidth / 2; + } else if (position === 'bottom') { + borderValue = alignPixel(chart, me.top, axisWidth); + tickStart = borderValue + axisWidth / 2; + tickEnd = me.top + tl; + } else if (position === 'left') { + borderValue = alignPixel(chart, me.right, axisWidth); + tickStart = me.right - tl; + tickEnd = borderValue - axisWidth / 2; + } else { + borderValue = alignPixel(chart, me.left, axisWidth); + tickStart = borderValue + axisWidth / 2; + tickEnd = me.left + tl; + } + + var epsilon = 0.0000001; // 0.0000001 is margin in pixels for Accumulated error. + + helpers$1.each(ticks, function(tick, index) { + // autoskipper skipped this tick (#4635) + if (helpers$1.isNullOrUndef(tick.label)) { + return; + } + + var label = tick.label; + var lineWidth, lineColor, borderDash, borderDashOffset; + if (index === me.zeroLineIndex && options.offset === gridLines.offsetGridLines) { + // Draw the first index specially + lineWidth = gridLines.zeroLineWidth; + lineColor = gridLines.zeroLineColor; + borderDash = gridLines.zeroLineBorderDash || []; + borderDashOffset = gridLines.zeroLineBorderDashOffset || 0.0; + } else { + lineWidth = valueAtIndexOrDefault(gridLines.lineWidth, index); + lineColor = valueAtIndexOrDefault(gridLines.color, index); + borderDash = gridLines.borderDash || []; + borderDashOffset = gridLines.borderDashOffset || 0.0; + } + + // Common properties + var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY, textOffset, textAlign; + var labelCount = helpers$1.isArray(label) ? label.length : 1; + var lineValue = getPixelForGridLine(me, index, gridLines.offsetGridLines); + + if (isHorizontal) { + var labelYOffset = tl + tickPadding; + + if (lineValue < me.left - epsilon) { + lineColor = 'rgba(0,0,0,0)'; + } + + tx1 = tx2 = x1 = x2 = alignPixel(chart, lineValue, lineWidth); + ty1 = tickStart; + ty2 = tickEnd; + labelX = me.getPixelForTick(index) + labelOffset; // x values for optionTicks (need to consider offsetLabel option) + + if (position === 'top') { + y1 = alignPixel(chart, chartArea.top, axisWidth) + axisWidth / 2; + y2 = chartArea.bottom; + textOffset = ((!isRotated ? 0.5 : 1) - labelCount) * lineHeight; + textAlign = !isRotated ? 'center' : 'left'; + labelY = me.bottom - labelYOffset; + } else { + y1 = chartArea.top; + y2 = alignPixel(chart, chartArea.bottom, axisWidth) - axisWidth / 2; + textOffset = (!isRotated ? 0.5 : 0) * lineHeight; + textAlign = !isRotated ? 'center' : 'right'; + labelY = me.top + labelYOffset; + } + } else { + var labelXOffset = (isMirrored ? 0 : tl) + tickPadding; + + if (lineValue < me.top - epsilon) { + lineColor = 'rgba(0,0,0,0)'; + } + + tx1 = tickStart; + tx2 = tickEnd; + ty1 = ty2 = y1 = y2 = alignPixel(chart, lineValue, lineWidth); + labelY = me.getPixelForTick(index) + labelOffset; + textOffset = (1 - labelCount) * lineHeight / 2; + + if (position === 'left') { + x1 = alignPixel(chart, chartArea.left, axisWidth) + axisWidth / 2; + x2 = chartArea.right; + textAlign = isMirrored ? 'left' : 'right'; + labelX = me.right - labelXOffset; + } else { + x1 = chartArea.left; + x2 = alignPixel(chart, chartArea.right, axisWidth) - axisWidth / 2; + textAlign = isMirrored ? 'right' : 'left'; + labelX = me.left + labelXOffset; + } + } + + itemsToDraw.push({ + tx1: tx1, + ty1: ty1, + tx2: tx2, + ty2: ty2, + x1: x1, + y1: y1, + x2: x2, + y2: y2, + labelX: labelX, + labelY: labelY, + glWidth: lineWidth, + glColor: lineColor, + glBorderDash: borderDash, + glBorderDashOffset: borderDashOffset, + rotation: -1 * labelRotationRadians, + label: label, + major: tick.major, + textOffset: textOffset, + textAlign: textAlign + }); + }); + + // Draw all of the tick labels, tick marks, and grid lines at the correct places + helpers$1.each(itemsToDraw, function(itemToDraw) { + var glWidth = itemToDraw.glWidth; + var glColor = itemToDraw.glColor; + + if (gridLines.display && glWidth && glColor) { + context.save(); + context.lineWidth = glWidth; + context.strokeStyle = glColor; + if (context.setLineDash) { + context.setLineDash(itemToDraw.glBorderDash); + context.lineDashOffset = itemToDraw.glBorderDashOffset; + } + + context.beginPath(); + + if (gridLines.drawTicks) { + context.moveTo(itemToDraw.tx1, itemToDraw.ty1); + context.lineTo(itemToDraw.tx2, itemToDraw.ty2); + } + + if (gridLines.drawOnChartArea) { + context.moveTo(itemToDraw.x1, itemToDraw.y1); + context.lineTo(itemToDraw.x2, itemToDraw.y2); + } + + context.stroke(); + context.restore(); + } + + if (optionTicks.display) { + // Make sure we draw text in the correct color and font + context.save(); + context.translate(itemToDraw.labelX, itemToDraw.labelY); + context.rotate(itemToDraw.rotation); + context.font = itemToDraw.major ? majorTickFont.string : tickFont.string; + context.fillStyle = itemToDraw.major ? majorTickFontColor : tickFontColor; + context.textBaseline = 'middle'; + context.textAlign = itemToDraw.textAlign; + + var label = itemToDraw.label; + var y = itemToDraw.textOffset; + if (helpers$1.isArray(label)) { + for (var i = 0; i < label.length; ++i) { + // We just make sure the multiline element is a string here.. + context.fillText('' + label[i], 0, y); + y += lineHeight; + } + } else { + context.fillText(label, 0, y); + } + context.restore(); + } + }); + + if (scaleLabel.display) { + // Draw the scale label + var scaleLabelX; + var scaleLabelY; + var rotation = 0; + var halfLineHeight = scaleLabelFont.lineHeight / 2; + + if (isHorizontal) { + scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width + scaleLabelY = position === 'bottom' + ? me.bottom - halfLineHeight - scaleLabelPadding.bottom + : me.top + halfLineHeight + scaleLabelPadding.top; + } else { + var isLeft = position === 'left'; + scaleLabelX = isLeft + ? me.left + halfLineHeight + scaleLabelPadding.top + : me.right - halfLineHeight - scaleLabelPadding.top; + scaleLabelY = me.top + ((me.bottom - me.top) / 2); + rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; + } + + context.save(); + context.translate(scaleLabelX, scaleLabelY); + context.rotate(rotation); + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillStyle = scaleLabelFontColor; // render in correct colour + context.font = scaleLabelFont.string; + context.fillText(scaleLabel.labelString, 0, 0); + context.restore(); + } + + if (axisWidth) { + // Draw the line at the edge of the axis + var firstLineWidth = axisWidth; + var lastLineWidth = valueAtIndexOrDefault(gridLines.lineWidth, ticks.length - 1, 0); + var x1, x2, y1, y2; + + if (isHorizontal) { + x1 = alignPixel(chart, me.left, firstLineWidth) - firstLineWidth / 2; + x2 = alignPixel(chart, me.right, lastLineWidth) + lastLineWidth / 2; + y1 = y2 = borderValue; + } else { + y1 = alignPixel(chart, me.top, firstLineWidth) - firstLineWidth / 2; + y2 = alignPixel(chart, me.bottom, lastLineWidth) + lastLineWidth / 2; + x1 = x2 = borderValue; + } + + context.lineWidth = axisWidth; + context.strokeStyle = valueAtIndexOrDefault(gridLines.color, 0); + context.beginPath(); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + context.stroke(); + } + } +}); + +var defaultConfig = { + position: 'bottom' +}; + +var scale_category = core_scale.extend({ + /** + * Internal function to get the correct labels. If data.xLabels or data.yLabels are defined, use those + * else fall back to data.labels + * @private + */ + getLabels: function() { + var data = this.chart.data; + return this.options.labels || (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels; + }, + + determineDataLimits: function() { + var me = this; + var labels = me.getLabels(); + me.minIndex = 0; + me.maxIndex = labels.length - 1; + var findIndex; + + if (me.options.ticks.min !== undefined) { + // user specified min value + findIndex = labels.indexOf(me.options.ticks.min); + me.minIndex = findIndex !== -1 ? findIndex : me.minIndex; + } + + if (me.options.ticks.max !== undefined) { + // user specified max value + findIndex = labels.indexOf(me.options.ticks.max); + me.maxIndex = findIndex !== -1 ? findIndex : me.maxIndex; + } + + me.min = labels[me.minIndex]; + me.max = labels[me.maxIndex]; + }, + + buildTicks: function() { + var me = this; + var labels = me.getLabels(); + // If we are viewing some subset of labels, slice the original array + me.ticks = (me.minIndex === 0 && me.maxIndex === labels.length - 1) ? labels : labels.slice(me.minIndex, me.maxIndex + 1); + }, + + getLabelForIndex: function(index, datasetIndex) { + var me = this; + var chart = me.chart; + + if (chart.getDatasetMeta(datasetIndex).controller._getValueScaleId() === me.id) { + return me.getRightValue(chart.data.datasets[datasetIndex].data[index]); + } + + return me.ticks[index - me.minIndex]; + }, + + // Used to get data value locations. Value can either be an index or a numerical value + getPixelForValue: function(value, index) { + var me = this; + var offset = me.options.offset; + // 1 is added because we need the length but we have the indexes + var offsetAmt = Math.max((me.maxIndex + 1 - me.minIndex - (offset ? 0 : 1)), 1); + + // If value is a data object, then index is the index in the data array, + // not the index of the scale. We need to change that. + var valueCategory; + if (value !== undefined && value !== null) { + valueCategory = me.isHorizontal() ? value.x : value.y; + } + if (valueCategory !== undefined || (value !== undefined && isNaN(index))) { + var labels = me.getLabels(); + value = valueCategory || value; + var idx = labels.indexOf(value); + index = idx !== -1 ? idx : index; + } + + if (me.isHorizontal()) { + var valueWidth = me.width / offsetAmt; + var widthOffset = (valueWidth * (index - me.minIndex)); + + if (offset) { + widthOffset += (valueWidth / 2); + } + + return me.left + widthOffset; + } + var valueHeight = me.height / offsetAmt; + var heightOffset = (valueHeight * (index - me.minIndex)); + + if (offset) { + heightOffset += (valueHeight / 2); + } + + return me.top + heightOffset; + }, + + getPixelForTick: function(index) { + return this.getPixelForValue(this.ticks[index], index + this.minIndex, null); + }, + + getValueForPixel: function(pixel) { + var me = this; + var offset = me.options.offset; + var value; + var offsetAmt = Math.max((me._ticks.length - (offset ? 0 : 1)), 1); + var horz = me.isHorizontal(); + var valueDimension = (horz ? me.width : me.height) / offsetAmt; + + pixel -= horz ? me.left : me.top; + + if (offset) { + pixel -= (valueDimension / 2); + } + + if (pixel <= 0) { + value = 0; + } else { + value = Math.round(pixel / valueDimension); + } + + return value + me.minIndex; + }, + + getBasePixel: function() { + return this.bottom; + } +}); + +// INTERNAL: static default options, registered in src/index.js +var _defaults = defaultConfig; +scale_category._defaults = _defaults; + +var noop = helpers$1.noop; +var isNullOrUndef = helpers$1.isNullOrUndef; + +/** + * Generate a set of linear ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {number[]} array of tick values + */ +function generateTicks(generationOptions, dataRange) { + var ticks = []; + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See https://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + var MIN_SPACING = 1e-14; + var stepSize = generationOptions.stepSize; + var unit = stepSize || 1; + var maxNumSpaces = generationOptions.maxTicks - 1; + var min = generationOptions.min; + var max = generationOptions.max; + var precision = generationOptions.precision; + var rmin = dataRange.min; + var rmax = dataRange.max; + var spacing = helpers$1.niceNum((rmax - rmin) / maxNumSpaces / unit) * unit; + var factor, niceMin, niceMax, numSpaces; + + // Beyond MIN_SPACING floating point numbers being to lose precision + // such that we can't do the math necessary to generate ticks + if (spacing < MIN_SPACING && isNullOrUndef(min) && isNullOrUndef(max)) { + return [rmin, rmax]; + } + + numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing); + if (numSpaces > maxNumSpaces) { + // If the calculated num of spaces exceeds maxNumSpaces, recalculate it + spacing = helpers$1.niceNum(numSpaces * spacing / maxNumSpaces / unit) * unit; + } + + if (stepSize || isNullOrUndef(precision)) { + // If a precision is not specified, calculate factor based on spacing + factor = Math.pow(10, helpers$1._decimalPlaces(spacing)); + } else { + // If the user specified a precision, round to that number of decimal places + factor = Math.pow(10, precision); + spacing = Math.ceil(spacing * factor) / factor; + } + + niceMin = Math.floor(rmin / spacing) * spacing; + niceMax = Math.ceil(rmax / spacing) * spacing; + + // If min, max and stepSize is set and they make an evenly spaced scale use it. + if (stepSize) { + // If very close to our whole number, use it. + if (!isNullOrUndef(min) && helpers$1.almostWhole(min / spacing, spacing / 1000)) { + niceMin = min; + } + if (!isNullOrUndef(max) && helpers$1.almostWhole(max / spacing, spacing / 1000)) { + niceMax = max; + } + } + + numSpaces = (niceMax - niceMin) / spacing; + // If very close to our rounded value, use it. + if (helpers$1.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { + numSpaces = Math.round(numSpaces); + } else { + numSpaces = Math.ceil(numSpaces); + } + + niceMin = Math.round(niceMin * factor) / factor; + niceMax = Math.round(niceMax * factor) / factor; + ticks.push(isNullOrUndef(min) ? niceMin : min); + for (var j = 1; j < numSpaces; ++j) { + ticks.push(Math.round((niceMin + j * spacing) * factor) / factor); + } + ticks.push(isNullOrUndef(max) ? niceMax : max); + + return ticks; +} + +var scale_linearbase = core_scale.extend({ + getRightValue: function(value) { + if (typeof value === 'string') { + return +value; + } + return core_scale.prototype.getRightValue.call(this, value); + }, + + handleTickRangeOptions: function() { + var me = this; + var opts = me.options; + var tickOpts = opts.ticks; + + // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, + // do nothing since that would make the chart weird. If the user really wants a weird chart + // axis, they can manually override it + if (tickOpts.beginAtZero) { + var minSign = helpers$1.sign(me.min); + var maxSign = helpers$1.sign(me.max); + + if (minSign < 0 && maxSign < 0) { + // move the top up to 0 + me.max = 0; + } else if (minSign > 0 && maxSign > 0) { + // move the bottom down to 0 + me.min = 0; + } + } + + var setMin = tickOpts.min !== undefined || tickOpts.suggestedMin !== undefined; + var setMax = tickOpts.max !== undefined || tickOpts.suggestedMax !== undefined; + + if (tickOpts.min !== undefined) { + me.min = tickOpts.min; + } else if (tickOpts.suggestedMin !== undefined) { + if (me.min === null) { + me.min = tickOpts.suggestedMin; + } else { + me.min = Math.min(me.min, tickOpts.suggestedMin); + } + } + + if (tickOpts.max !== undefined) { + me.max = tickOpts.max; + } else if (tickOpts.suggestedMax !== undefined) { + if (me.max === null) { + me.max = tickOpts.suggestedMax; + } else { + me.max = Math.max(me.max, tickOpts.suggestedMax); + } + } + + if (setMin !== setMax) { + // We set the min or the max but not both. + // So ensure that our range is good + // Inverted or 0 length range can happen when + // ticks.min is set, and no datasets are visible + if (me.min >= me.max) { + if (setMin) { + me.max = me.min + 1; + } else { + me.min = me.max - 1; + } + } + } + + if (me.min === me.max) { + me.max++; + + if (!tickOpts.beginAtZero) { + me.min--; + } + } + }, + + getTickLimit: function() { + var me = this; + var tickOpts = me.options.ticks; + var stepSize = tickOpts.stepSize; + var maxTicksLimit = tickOpts.maxTicksLimit; + var maxTicks; + + if (stepSize) { + maxTicks = Math.ceil(me.max / stepSize) - Math.floor(me.min / stepSize) + 1; + } else { + maxTicks = me._computeTickLimit(); + maxTicksLimit = maxTicksLimit || 11; + } + + if (maxTicksLimit) { + maxTicks = Math.min(maxTicksLimit, maxTicks); + } + + return maxTicks; + }, + + _computeTickLimit: function() { + return Number.POSITIVE_INFINITY; + }, + + handleDirectionalChanges: noop, + + buildTicks: function() { + var me = this; + var opts = me.options; + var tickOpts = opts.ticks; + + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 40 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph. Make sure we always have at least 2 ticks + var maxTicks = me.getTickLimit(); + maxTicks = Math.max(2, maxTicks); + + var numericGeneratorOptions = { + maxTicks: maxTicks, + min: tickOpts.min, + max: tickOpts.max, + precision: tickOpts.precision, + stepSize: helpers$1.valueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize) + }; + var ticks = me.ticks = generateTicks(numericGeneratorOptions, me); + + me.handleDirectionalChanges(); + + // At this point, we need to update our max and min given the tick values since we have expanded the + // range of the scale + me.max = helpers$1.max(ticks); + me.min = helpers$1.min(ticks); + + if (tickOpts.reverse) { + ticks.reverse(); + + me.start = me.max; + me.end = me.min; + } else { + me.start = me.min; + me.end = me.max; + } + }, + + convertTicksToLabels: function() { + var me = this; + me.ticksAsNumbers = me.ticks.slice(); + me.zeroLineIndex = me.ticks.indexOf(0); + + core_scale.prototype.convertTicksToLabels.call(me); + } +}); + +var defaultConfig$1 = { + position: 'left', + ticks: { + callback: core_ticks.formatters.linear + } +}; + +var scale_linear = scale_linearbase.extend({ + determineDataLimits: function() { + var me = this; + var opts = me.options; + var chart = me.chart; + var data = chart.data; + var datasets = data.datasets; + var isHorizontal = me.isHorizontal(); + var DEFAULT_MIN = 0; + var DEFAULT_MAX = 1; + + function IDMatches(meta) { + return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; + } + + // First Calculate the range + me.min = null; + me.max = null; + + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers$1.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; + + helpers$1.each(datasets, function(dataset, datasetIndex) { + var meta = chart.getDatasetMeta(datasetIndex); + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = { + positiveValues: [], + negativeValues: [] + }; + } + + // Store these per type + var positiveValues = valuesPerStack[key].positiveValues; + var negativeValues = valuesPerStack[key].negativeValues; + + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { + helpers$1.each(dataset.data, function(rawValue, index) { + var value = +me.getRightValue(rawValue); + if (isNaN(value) || meta.data[index].hidden) { + return; + } + + positiveValues[index] = positiveValues[index] || 0; + negativeValues[index] = negativeValues[index] || 0; + + if (opts.relativePoints) { + positiveValues[index] = 100; + } else if (value < 0) { + negativeValues[index] += value; + } else { + positiveValues[index] += value; + } + }); + } + }); + + helpers$1.each(valuesPerStack, function(valuesForType) { + var values = valuesForType.positiveValues.concat(valuesForType.negativeValues); + var minVal = helpers$1.min(values); + var maxVal = helpers$1.max(values); + me.min = me.min === null ? minVal : Math.min(me.min, minVal); + me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + }); + + } else { + helpers$1.each(datasets, function(dataset, datasetIndex) { + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { + helpers$1.each(dataset.data, function(rawValue, index) { + var value = +me.getRightValue(rawValue); + if (isNaN(value) || meta.data[index].hidden) { + return; + } + + if (me.min === null) { + me.min = value; + } else if (value < me.min) { + me.min = value; + } + + if (me.max === null) { + me.max = value; + } else if (value > me.max) { + me.max = value; + } + }); + } + }); + } + + me.min = isFinite(me.min) && !isNaN(me.min) ? me.min : DEFAULT_MIN; + me.max = isFinite(me.max) && !isNaN(me.max) ? me.max : DEFAULT_MAX; + + // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero + this.handleTickRangeOptions(); + }, + + // Returns the maximum number of ticks based on the scale dimension + _computeTickLimit: function() { + var me = this; + var tickFont; + + if (me.isHorizontal()) { + return Math.ceil(me.width / 40); + } + tickFont = helpers$1.options._parseFont(me.options.ticks); + return Math.ceil(me.height / tickFont.lineHeight); + }, + + // Called after the ticks are built. We need + handleDirectionalChanges: function() { + if (!this.isHorizontal()) { + // We are in a vertical orientation. The top value is the highest. So reverse the array + this.ticks.reverse(); + } + }, + + getLabelForIndex: function(index, datasetIndex) { + return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); + }, + + // Utils + getPixelForValue: function(value) { + // This must be called after fit has been run so that + // this.left, this.top, this.right, and this.bottom have been defined + var me = this; + var start = me.start; + + var rightValue = +me.getRightValue(value); + var pixel; + var range = me.end - start; + + if (me.isHorizontal()) { + pixel = me.left + (me.width / range * (rightValue - start)); + } else { + pixel = me.bottom - (me.height / range * (rightValue - start)); + } + return pixel; + }, + + getValueForPixel: function(pixel) { + var me = this; + var isHorizontal = me.isHorizontal(); + var innerDimension = isHorizontal ? me.width : me.height; + var offset = (isHorizontal ? pixel - me.left : me.bottom - pixel) / innerDimension; + return me.start + ((me.end - me.start) * offset); + }, + + getPixelForTick: function(index) { + return this.getPixelForValue(this.ticksAsNumbers[index]); + } +}); + +// INTERNAL: static default options, registered in src/index.js +var _defaults$1 = defaultConfig$1; +scale_linear._defaults = _defaults$1; + +var valueOrDefault$a = helpers$1.valueOrDefault; + +/** + * Generate a set of logarithmic ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {number[]} array of tick values + */ +function generateTicks$1(generationOptions, dataRange) { + var ticks = []; + + var tickVal = valueOrDefault$a(generationOptions.min, Math.pow(10, Math.floor(helpers$1.log10(dataRange.min)))); + + var endExp = Math.floor(helpers$1.log10(dataRange.max)); + var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + var exp, significand; + + if (tickVal === 0) { + exp = Math.floor(helpers$1.log10(dataRange.minNotZero)); + significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); + + ticks.push(tickVal); + tickVal = significand * Math.pow(10, exp); + } else { + exp = Math.floor(helpers$1.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)); + } + var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; + + do { + ticks.push(tickVal); + + ++significand; + if (significand === 10) { + significand = 1; + ++exp; + precision = exp >= 0 ? 1 : precision; + } + + tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; + } while (exp < endExp || (exp === endExp && significand < endSignificand)); + + var lastTick = valueOrDefault$a(generationOptions.max, tickVal); + ticks.push(lastTick); + + return ticks; +} + +var defaultConfig$2 = { + position: 'left', + + // label settings + ticks: { + callback: core_ticks.formatters.logarithmic + } +}; + +// TODO(v3): change this to positiveOrDefault +function nonNegativeOrDefault(value, defaultValue) { + return helpers$1.isFinite(value) && value >= 0 ? value : defaultValue; +} + +var scale_logarithmic = core_scale.extend({ + determineDataLimits: function() { + var me = this; + var opts = me.options; + var chart = me.chart; + var data = chart.data; + var datasets = data.datasets; + var isHorizontal = me.isHorizontal(); + function IDMatches(meta) { + return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; + } + + // Calculate Range + me.min = null; + me.max = null; + me.minNotZero = null; + + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers$1.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; + + helpers$1.each(datasets, function(dataset, datasetIndex) { + var meta = chart.getDatasetMeta(datasetIndex); + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = []; + } + + helpers$1.each(dataset.data, function(rawValue, index) { + var values = valuesPerStack[key]; + var value = +me.getRightValue(rawValue); + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { + return; + } + values[index] = values[index] || 0; + values[index] += value; + }); + } + }); + + helpers$1.each(valuesPerStack, function(valuesForType) { + if (valuesForType.length > 0) { + var minVal = helpers$1.min(valuesForType); + var maxVal = helpers$1.max(valuesForType); + me.min = me.min === null ? minVal : Math.min(me.min, minVal); + me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + } + }); + + } else { + helpers$1.each(datasets, function(dataset, datasetIndex) { + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { + helpers$1.each(dataset.data, function(rawValue, index) { + var value = +me.getRightValue(rawValue); + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { + return; + } + + if (me.min === null) { + me.min = value; + } else if (value < me.min) { + me.min = value; + } + + if (me.max === null) { + me.max = value; + } else if (value > me.max) { + me.max = value; + } + + if (value !== 0 && (me.minNotZero === null || value < me.minNotZero)) { + me.minNotZero = value; + } + }); + } + }); + } + + // Common base implementation to handle ticks.min, ticks.max + this.handleTickRangeOptions(); + }, + + handleTickRangeOptions: function() { + var me = this; + var tickOpts = me.options.ticks; + var DEFAULT_MIN = 1; + var DEFAULT_MAX = 10; + + me.min = nonNegativeOrDefault(tickOpts.min, me.min); + me.max = nonNegativeOrDefault(tickOpts.max, me.max); + + if (me.min === me.max) { + if (me.min !== 0 && me.min !== null) { + me.min = Math.pow(10, Math.floor(helpers$1.log10(me.min)) - 1); + me.max = Math.pow(10, Math.floor(helpers$1.log10(me.max)) + 1); + } else { + me.min = DEFAULT_MIN; + me.max = DEFAULT_MAX; + } + } + if (me.min === null) { + me.min = Math.pow(10, Math.floor(helpers$1.log10(me.max)) - 1); + } + if (me.max === null) { + me.max = me.min !== 0 + ? Math.pow(10, Math.floor(helpers$1.log10(me.min)) + 1) + : DEFAULT_MAX; + } + if (me.minNotZero === null) { + if (me.min > 0) { + me.minNotZero = me.min; + } else if (me.max < 1) { + me.minNotZero = Math.pow(10, Math.floor(helpers$1.log10(me.max))); + } else { + me.minNotZero = DEFAULT_MIN; + } + } + }, + + buildTicks: function() { + var me = this; + var tickOpts = me.options.ticks; + var reverse = !me.isHorizontal(); + + var generationOptions = { + min: nonNegativeOrDefault(tickOpts.min), + max: nonNegativeOrDefault(tickOpts.max) + }; + var ticks = me.ticks = generateTicks$1(generationOptions, me); + + // At this point, we need to update our max and min given the tick values since we have expanded the + // range of the scale + me.max = helpers$1.max(ticks); + me.min = helpers$1.min(ticks); + + if (tickOpts.reverse) { + reverse = !reverse; + me.start = me.max; + me.end = me.min; + } else { + me.start = me.min; + me.end = me.max; + } + if (reverse) { + ticks.reverse(); + } + }, + + convertTicksToLabels: function() { + this.tickValues = this.ticks.slice(); + + core_scale.prototype.convertTicksToLabels.call(this); + }, + + // Get the correct tooltip label + getLabelForIndex: function(index, datasetIndex) { + return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); + }, + + getPixelForTick: function(index) { + return this.getPixelForValue(this.tickValues[index]); + }, + + /** + * Returns the value of the first tick. + * @param {number} value - The minimum not zero value. + * @return {number} The first tick value. + * @private + */ + _getFirstTickValue: function(value) { + var exp = Math.floor(helpers$1.log10(value)); + var significand = Math.floor(value / Math.pow(10, exp)); + + return significand * Math.pow(10, exp); + }, + + getPixelForValue: function(value) { + var me = this; + var tickOpts = me.options.ticks; + var reverse = tickOpts.reverse; + var log10 = helpers$1.log10; + var firstTickValue = me._getFirstTickValue(me.minNotZero); + var offset = 0; + var innerDimension, pixel, start, end, sign; + + value = +me.getRightValue(value); + if (reverse) { + start = me.end; + end = me.start; + sign = -1; + } else { + start = me.start; + end = me.end; + sign = 1; + } + if (me.isHorizontal()) { + innerDimension = me.width; + pixel = reverse ? me.right : me.left; + } else { + innerDimension = me.height; + sign *= -1; // invert, since the upper-left corner of the canvas is at pixel (0, 0) + pixel = reverse ? me.top : me.bottom; + } + if (value !== start) { + if (start === 0) { // include zero tick + offset = valueOrDefault$a(tickOpts.fontSize, core_defaults.global.defaultFontSize); + innerDimension -= offset; + start = firstTickValue; + } + if (value !== 0) { + offset += innerDimension / (log10(end) - log10(start)) * (log10(value) - log10(start)); + } + pixel += sign * offset; + } + return pixel; + }, + + getValueForPixel: function(pixel) { + var me = this; + var tickOpts = me.options.ticks; + var reverse = tickOpts.reverse; + var log10 = helpers$1.log10; + var firstTickValue = me._getFirstTickValue(me.minNotZero); + var innerDimension, start, end, value; + + if (reverse) { + start = me.end; + end = me.start; + } else { + start = me.start; + end = me.end; + } + if (me.isHorizontal()) { + innerDimension = me.width; + value = reverse ? me.right - pixel : pixel - me.left; + } else { + innerDimension = me.height; + value = reverse ? pixel - me.top : me.bottom - pixel; + } + if (value !== start) { + if (start === 0) { // include zero tick + var offset = valueOrDefault$a(tickOpts.fontSize, core_defaults.global.defaultFontSize); + value -= offset; + innerDimension -= offset; + start = firstTickValue; + } + value *= log10(end) - log10(start); + value /= innerDimension; + value = Math.pow(10, log10(start) + value); + } + return value; + } +}); + +// INTERNAL: static default options, registered in src/index.js +var _defaults$2 = defaultConfig$2; +scale_logarithmic._defaults = _defaults$2; + +var valueOrDefault$b = helpers$1.valueOrDefault; +var valueAtIndexOrDefault$1 = helpers$1.valueAtIndexOrDefault; +var resolve$7 = helpers$1.options.resolve; + +var defaultConfig$3 = { + display: true, + + // Boolean - Whether to animate scaling the chart from the centre + animate: true, + position: 'chartArea', + + angleLines: { + display: true, + color: 'rgba(0, 0, 0, 0.1)', + lineWidth: 1, + borderDash: [], + borderDashOffset: 0.0 + }, + + gridLines: { + circular: false + }, + + // label settings + ticks: { + // Boolean - Show a backdrop to the scale label + showLabelBackdrop: true, + + // String - The colour of the label backdrop + backdropColor: 'rgba(255,255,255,0.75)', + + // Number - The backdrop padding above & below the label in pixels + backdropPaddingY: 2, + + // Number - The backdrop padding to the side of the label in pixels + backdropPaddingX: 2, + + callback: core_ticks.formatters.linear + }, + + pointLabels: { + // Boolean - if true, show point labels + display: true, + + // Number - Point label font size in pixels + fontSize: 10, + + // Function - Used to convert point labels + callback: function(label) { + return label; + } + } +}; + +function getValueCount(scale) { + var opts = scale.options; + return opts.angleLines.display || opts.pointLabels.display ? scale.chart.data.labels.length : 0; +} + +function getTickBackdropHeight(opts) { + var tickOpts = opts.ticks; + + if (tickOpts.display && opts.display) { + return valueOrDefault$b(tickOpts.fontSize, core_defaults.global.defaultFontSize) + tickOpts.backdropPaddingY * 2; + } + return 0; +} + +function measureLabelSize(ctx, lineHeight, label) { + if (helpers$1.isArray(label)) { + return { + w: helpers$1.longestText(ctx, ctx.font, label), + h: label.length * lineHeight + }; + } + + return { + w: ctx.measureText(label).width, + h: lineHeight + }; +} + +function determineLimits(angle, pos, size, min, max) { + if (angle === min || angle === max) { + return { + start: pos - (size / 2), + end: pos + (size / 2) + }; + } else if (angle < min || angle > max) { + return { + start: pos - size, + end: pos + }; + } + + return { + start: pos, + end: pos + size + }; +} + +/** + * Helper function to fit a radial linear scale with point labels + */ +function fitWithPointLabels(scale) { + + // Right, this is really confusing and there is a lot of maths going on here + // The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + // + // Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + // + // Solution: + // + // We assume the radius of the polygon is half the size of the canvas at first + // at each index we check if the text overlaps. + // + // Where it does, we store that angle and that index. + // + // After finding the largest index and angle we calculate how much we need to remove + // from the shape radius to move the point inwards by that x. + // + // We average the left and right distances to get the maximum shape radius that can fit in the box + // along with labels. + // + // Once we have that, we can find the centre point for the chart, by taking the x text protrusion + // on each side, removing that from the size, halving it and adding the left x protrusion width. + // + // This will mean we have a shape fitted to the canvas, as large as it can be with the labels + // and position it in the most space efficient manner + // + // https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + + var plFont = helpers$1.options._parseFont(scale.options.pointLabels); + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var furthestLimits = { + l: 0, + r: scale.width, + t: 0, + b: scale.height - scale.paddingTop + }; + var furthestAngles = {}; + var i, textSize, pointPosition; + + scale.ctx.font = plFont.string; + scale._pointLabelSizes = []; + + var valueCount = getValueCount(scale); + for (i = 0; i < valueCount; i++) { + pointPosition = scale.getPointPosition(i, scale.drawingArea + 5); + textSize = measureLabelSize(scale.ctx, plFont.lineHeight, scale.pointLabels[i] || ''); + scale._pointLabelSizes[i] = textSize; + + // Add quarter circle to make degree 0 mean top of circle + var angleRadians = scale.getIndexAngle(i); + var angle = helpers$1.toDegrees(angleRadians) % 360; + var hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180); + var vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270); + + if (hLimits.start < furthestLimits.l) { + furthestLimits.l = hLimits.start; + furthestAngles.l = angleRadians; + } + + if (hLimits.end > furthestLimits.r) { + furthestLimits.r = hLimits.end; + furthestAngles.r = angleRadians; + } + + if (vLimits.start < furthestLimits.t) { + furthestLimits.t = vLimits.start; + furthestAngles.t = angleRadians; + } + + if (vLimits.end > furthestLimits.b) { + furthestLimits.b = vLimits.end; + furthestAngles.b = angleRadians; + } + } + + scale.setReductions(scale.drawingArea, furthestLimits, furthestAngles); +} + +function getTextAlignForAngle(angle) { + if (angle === 0 || angle === 180) { + return 'center'; + } else if (angle < 180) { + return 'left'; + } + + return 'right'; +} + +function fillText(ctx, text, position, lineHeight) { + var y = position.y + lineHeight / 2; + var i, ilen; + + if (helpers$1.isArray(text)) { + for (i = 0, ilen = text.length; i < ilen; ++i) { + ctx.fillText(text[i], position.x, y); + y += lineHeight; + } + } else { + ctx.fillText(text, position.x, y); + } +} + +function adjustPointPositionForLabelHeight(angle, textSize, position) { + if (angle === 90 || angle === 270) { + position.y -= (textSize.h / 2); + } else if (angle > 270 || angle < 90) { + position.y -= textSize.h; + } +} + +function drawPointLabels(scale) { + var ctx = scale.ctx; + var opts = scale.options; + var angleLineOpts = opts.angleLines; + var gridLineOpts = opts.gridLines; + var pointLabelOpts = opts.pointLabels; + var lineWidth = valueOrDefault$b(angleLineOpts.lineWidth, gridLineOpts.lineWidth); + var lineColor = valueOrDefault$b(angleLineOpts.color, gridLineOpts.color); + var tickBackdropHeight = getTickBackdropHeight(opts); + + ctx.save(); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = lineColor; + if (ctx.setLineDash) { + ctx.setLineDash(resolve$7([angleLineOpts.borderDash, gridLineOpts.borderDash, []])); + ctx.lineDashOffset = resolve$7([angleLineOpts.borderDashOffset, gridLineOpts.borderDashOffset, 0.0]); + } + + var outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max); + + // Point Label Font + var plFont = helpers$1.options._parseFont(pointLabelOpts); + + ctx.font = plFont.string; + ctx.textBaseline = 'middle'; + + for (var i = getValueCount(scale) - 1; i >= 0; i--) { + if (angleLineOpts.display && lineWidth && lineColor) { + var outerPosition = scale.getPointPosition(i, outerDistance); + ctx.beginPath(); + ctx.moveTo(scale.xCenter, scale.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + } + + if (pointLabelOpts.display) { + // Extra pixels out for some label spacing + var extra = (i === 0 ? tickBackdropHeight / 2 : 0); + var pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + 5); + + // Keep this in loop since we may support array properties here + var pointLabelFontColor = valueAtIndexOrDefault$1(pointLabelOpts.fontColor, i, core_defaults.global.defaultFontColor); + ctx.fillStyle = pointLabelFontColor; + + var angleRadians = scale.getIndexAngle(i); + var angle = helpers$1.toDegrees(angleRadians); + ctx.textAlign = getTextAlignForAngle(angle); + adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition); + fillText(ctx, scale.pointLabels[i] || '', pointLabelPosition, plFont.lineHeight); + } + } + ctx.restore(); +} + +function drawRadiusLine(scale, gridLineOpts, radius, index) { + var ctx = scale.ctx; + var circular = gridLineOpts.circular; + var valueCount = getValueCount(scale); + var lineColor = valueAtIndexOrDefault$1(gridLineOpts.color, index - 1); + var lineWidth = valueAtIndexOrDefault$1(gridLineOpts.lineWidth, index - 1); + var pointPosition; + + if ((!circular && !valueCount) || !lineColor || !lineWidth) { + return; + } + + ctx.save(); + ctx.strokeStyle = lineColor; + ctx.lineWidth = lineWidth; + if (ctx.setLineDash) { + ctx.setLineDash(gridLineOpts.borderDash || []); + ctx.lineDashOffset = gridLineOpts.borderDashOffset || 0.0; + } + + ctx.beginPath(); + if (circular) { + // Draw circular arcs between the points + ctx.arc(scale.xCenter, scale.yCenter, radius, 0, Math.PI * 2); + } else { + // Draw straight lines connecting each index + pointPosition = scale.getPointPosition(0, radius); + ctx.moveTo(pointPosition.x, pointPosition.y); + + for (var i = 1; i < valueCount; i++) { + pointPosition = scale.getPointPosition(i, radius); + ctx.lineTo(pointPosition.x, pointPosition.y); + } + } + ctx.closePath(); + ctx.stroke(); + ctx.restore(); +} + +function numberOrZero(param) { + return helpers$1.isNumber(param) ? param : 0; +} + +var scale_radialLinear = scale_linearbase.extend({ + setDimensions: function() { + var me = this; + + // Set the unconstrained dimension before label rotation + me.width = me.maxWidth; + me.height = me.maxHeight; + me.paddingTop = getTickBackdropHeight(me.options) / 2; + me.xCenter = Math.floor(me.width / 2); + me.yCenter = Math.floor((me.height - me.paddingTop) / 2); + me.drawingArea = Math.min(me.height - me.paddingTop, me.width) / 2; + }, + + determineDataLimits: function() { + var me = this; + var chart = me.chart; + var min = Number.POSITIVE_INFINITY; + var max = Number.NEGATIVE_INFINITY; + + helpers$1.each(chart.data.datasets, function(dataset, datasetIndex) { + if (chart.isDatasetVisible(datasetIndex)) { + var meta = chart.getDatasetMeta(datasetIndex); + + helpers$1.each(dataset.data, function(rawValue, index) { + var value = +me.getRightValue(rawValue); + if (isNaN(value) || meta.data[index].hidden) { + return; + } + + min = Math.min(value, min); + max = Math.max(value, max); + }); + } + }); + + me.min = (min === Number.POSITIVE_INFINITY ? 0 : min); + me.max = (max === Number.NEGATIVE_INFINITY ? 0 : max); + + // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero + me.handleTickRangeOptions(); + }, + + // Returns the maximum number of ticks based on the scale dimension + _computeTickLimit: function() { + return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options)); + }, + + convertTicksToLabels: function() { + var me = this; + + scale_linearbase.prototype.convertTicksToLabels.call(me); + + // Point labels + me.pointLabels = me.chart.data.labels.map(me.options.pointLabels.callback, me); + }, + + getLabelForIndex: function(index, datasetIndex) { + return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); + }, + + fit: function() { + var me = this; + var opts = me.options; + + if (opts.display && opts.pointLabels.display) { + fitWithPointLabels(me); + } else { + me.setCenterPoint(0, 0, 0, 0); + } + }, + + /** + * Set radius reductions and determine new radius and center point + * @private + */ + setReductions: function(largestPossibleRadius, furthestLimits, furthestAngles) { + var me = this; + var radiusReductionLeft = furthestLimits.l / Math.sin(furthestAngles.l); + var radiusReductionRight = Math.max(furthestLimits.r - me.width, 0) / Math.sin(furthestAngles.r); + var radiusReductionTop = -furthestLimits.t / Math.cos(furthestAngles.t); + var radiusReductionBottom = -Math.max(furthestLimits.b - (me.height - me.paddingTop), 0) / Math.cos(furthestAngles.b); + + radiusReductionLeft = numberOrZero(radiusReductionLeft); + radiusReductionRight = numberOrZero(radiusReductionRight); + radiusReductionTop = numberOrZero(radiusReductionTop); + radiusReductionBottom = numberOrZero(radiusReductionBottom); + + me.drawingArea = Math.min( + Math.floor(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2), + Math.floor(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2)); + me.setCenterPoint(radiusReductionLeft, radiusReductionRight, radiusReductionTop, radiusReductionBottom); + }, + + setCenterPoint: function(leftMovement, rightMovement, topMovement, bottomMovement) { + var me = this; + var maxRight = me.width - rightMovement - me.drawingArea; + var maxLeft = leftMovement + me.drawingArea; + var maxTop = topMovement + me.drawingArea; + var maxBottom = (me.height - me.paddingTop) - bottomMovement - me.drawingArea; + + me.xCenter = Math.floor(((maxLeft + maxRight) / 2) + me.left); + me.yCenter = Math.floor(((maxTop + maxBottom) / 2) + me.top + me.paddingTop); + }, + + getIndexAngle: function(index) { + var angleMultiplier = (Math.PI * 2) / getValueCount(this); + var startAngle = this.chart.options && this.chart.options.startAngle ? + this.chart.options.startAngle : + 0; + + var startAngleRadians = startAngle * Math.PI * 2 / 360; + + // Start from the top instead of right, so remove a quarter of the circle + return index * angleMultiplier + startAngleRadians; + }, + + getDistanceFromCenterForValue: function(value) { + var me = this; + + if (value === null) { + return 0; // null always in center + } + + // Take into account half font size + the yPadding of the top value + var scalingFactor = me.drawingArea / (me.max - me.min); + if (me.options.ticks.reverse) { + return (me.max - value) * scalingFactor; + } + return (value - me.min) * scalingFactor; + }, + + getPointPosition: function(index, distanceFromCenter) { + var me = this; + var thisAngle = me.getIndexAngle(index) - (Math.PI / 2); + return { + x: Math.cos(thisAngle) * distanceFromCenter + me.xCenter, + y: Math.sin(thisAngle) * distanceFromCenter + me.yCenter + }; + }, + + getPointPositionForValue: function(index, value) { + return this.getPointPosition(index, this.getDistanceFromCenterForValue(value)); + }, + + getBasePosition: function() { + var me = this; + var min = me.min; + var max = me.max; + + return me.getPointPositionForValue(0, + me.beginAtZero ? 0 : + min < 0 && max < 0 ? max : + min > 0 && max > 0 ? min : + 0); + }, + + draw: function() { + var me = this; + var opts = me.options; + var gridLineOpts = opts.gridLines; + var tickOpts = opts.ticks; + + if (opts.display) { + var ctx = me.ctx; + var startAngle = this.getIndexAngle(0); + var tickFont = helpers$1.options._parseFont(tickOpts); + + if (opts.angleLines.display || opts.pointLabels.display) { + drawPointLabels(me); + } + + helpers$1.each(me.ticks, function(label, index) { + // Don't draw a centre value (if it is minimum) + if (index > 0 || tickOpts.reverse) { + var yCenterOffset = me.getDistanceFromCenterForValue(me.ticksAsNumbers[index]); + + // Draw circular lines around the scale + if (gridLineOpts.display && index !== 0) { + drawRadiusLine(me, gridLineOpts, yCenterOffset, index); + } + + if (tickOpts.display) { + var tickFontColor = valueOrDefault$b(tickOpts.fontColor, core_defaults.global.defaultFontColor); + ctx.font = tickFont.string; + + ctx.save(); + ctx.translate(me.xCenter, me.yCenter); + ctx.rotate(startAngle); + + if (tickOpts.showLabelBackdrop) { + var labelWidth = ctx.measureText(label).width; + ctx.fillStyle = tickOpts.backdropColor; + ctx.fillRect( + -labelWidth / 2 - tickOpts.backdropPaddingX, + -yCenterOffset - tickFont.size / 2 - tickOpts.backdropPaddingY, + labelWidth + tickOpts.backdropPaddingX * 2, + tickFont.size + tickOpts.backdropPaddingY * 2 + ); + } + + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = tickFontColor; + ctx.fillText(label, 0, -yCenterOffset); + ctx.restore(); + } + } + }); + } + } +}); + +// INTERNAL: static default options, registered in src/index.js +var _defaults$3 = defaultConfig$3; +scale_radialLinear._defaults = _defaults$3; + +var valueOrDefault$c = helpers$1.valueOrDefault; + +// Integer constants are from the ES6 spec. +var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; +var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; + +var INTERVALS = { + millisecond: { + common: true, + size: 1, + steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] + }, + second: { + common: true, + size: 1000, + steps: [1, 2, 5, 10, 15, 30] + }, + minute: { + common: true, + size: 60000, + steps: [1, 2, 5, 10, 15, 30] + }, + hour: { + common: true, + size: 3600000, + steps: [1, 2, 3, 6, 12] + }, + day: { + common: true, + size: 86400000, + steps: [1, 2, 5] + }, + week: { + common: false, + size: 604800000, + steps: [1, 2, 3, 4] + }, + month: { + common: true, + size: 2.628e9, + steps: [1, 2, 3] + }, + quarter: { + common: false, + size: 7.884e9, + steps: [1, 2, 3, 4] + }, + year: { + common: true, + size: 3.154e10 + } +}; + +var UNITS = Object.keys(INTERVALS); + +function sorter(a, b) { + return a - b; +} + +function arrayUnique(items) { + var hash = {}; + var out = []; + var i, ilen, item; + + for (i = 0, ilen = items.length; i < ilen; ++i) { + item = items[i]; + if (!hash[item]) { + hash[item] = true; + out.push(item); + } + } + + return out; +} + +/** + * Returns an array of {time, pos} objects used to interpolate a specific `time` or position + * (`pos`) on the scale, by searching entries before and after the requested value. `pos` is + * a decimal between 0 and 1: 0 being the start of the scale (left or top) and 1 the other + * extremity (left + width or top + height). Note that it would be more optimized to directly + * store pre-computed pixels, but the scale dimensions are not guaranteed at the time we need + * to create the lookup table. The table ALWAYS contains at least two items: min and max. + * + * @param {number[]} timestamps - timestamps sorted from lowest to highest. + * @param {string} distribution - If 'linear', timestamps will be spread linearly along the min + * and max range, so basically, the table will contains only two items: {min, 0} and {max, 1}. + * If 'series', timestamps will be positioned at the same distance from each other. In this + * case, only timestamps that break the time linearity are registered, meaning that in the + * best case, all timestamps are linear, the table contains only min and max. + */ +function buildLookupTable(timestamps, min, max, distribution) { + if (distribution === 'linear' || !timestamps.length) { + return [ + {time: min, pos: 0}, + {time: max, pos: 1} + ]; + } + + var table = []; + var items = [min]; + var i, ilen, prev, curr, next; + + for (i = 0, ilen = timestamps.length; i < ilen; ++i) { + curr = timestamps[i]; + if (curr > min && curr < max) { + items.push(curr); + } + } + + items.push(max); + + for (i = 0, ilen = items.length; i < ilen; ++i) { + next = items[i + 1]; + prev = items[i - 1]; + curr = items[i]; + + // only add points that breaks the scale linearity + if (prev === undefined || next === undefined || Math.round((next + prev) / 2) !== curr) { + table.push({time: curr, pos: i / (ilen - 1)}); + } + } + + return table; +} + +// @see adapted from https://www.anujgakhar.com/2014/03/01/binary-search-in-javascript/ +function lookup(table, key, value) { + var lo = 0; + var hi = table.length - 1; + var mid, i0, i1; + + while (lo >= 0 && lo <= hi) { + mid = (lo + hi) >> 1; + i0 = table[mid - 1] || null; + i1 = table[mid]; + + if (!i0) { + // given value is outside table (before first item) + return {lo: null, hi: i1}; + } else if (i1[key] < value) { + lo = mid + 1; + } else if (i0[key] > value) { + hi = mid - 1; + } else { + return {lo: i0, hi: i1}; + } + } + + // given value is outside table (after last item) + return {lo: i1, hi: null}; +} + +/** + * Linearly interpolates the given source `value` using the table items `skey` values and + * returns the associated `tkey` value. For example, interpolate(table, 'time', 42, 'pos') + * returns the position for a timestamp equal to 42. If value is out of bounds, values at + * index [0, 1] or [n - 1, n] are used for the interpolation. + */ +function interpolate$1(table, skey, sval, tkey) { + var range = lookup(table, skey, sval); + + // Note: the lookup table ALWAYS contains at least 2 items (min and max) + var prev = !range.lo ? table[0] : !range.hi ? table[table.length - 2] : range.lo; + var next = !range.lo ? table[1] : !range.hi ? table[table.length - 1] : range.hi; + + var span = next[skey] - prev[skey]; + var ratio = span ? (sval - prev[skey]) / span : 0; + var offset = (next[tkey] - prev[tkey]) * ratio; + + return prev[tkey] + offset; +} + +function toTimestamp(scale, input) { + var adapter = scale._adapter; + var options = scale.options.time; + var parser = options.parser; + var format = parser || options.format; + var value = input; + + if (typeof parser === 'function') { + value = parser(value); + } + + // Only parse if its not a timestamp already + if (!helpers$1.isFinite(value)) { + value = typeof format === 'string' + ? adapter.parse(value, format) + : adapter.parse(value); + } + + if (value !== null) { + return +value; + } + + // Labels are in an incompatible format and no `parser` has been provided. + // The user might still use the deprecated `format` option for parsing. + if (!parser && typeof format === 'function') { + value = format(input); + + // `format` could return something else than a timestamp, if so, parse it + if (!helpers$1.isFinite(value)) { + value = adapter.parse(value); + } + } + + return value; +} + +function parse(scale, input) { + if (helpers$1.isNullOrUndef(input)) { + return null; + } + + var options = scale.options.time; + var value = toTimestamp(scale, scale.getRightValue(input)); + if (value === null) { + return value; + } + + if (options.round) { + value = +scale._adapter.startOf(value, options.round); + } + + return value; +} + +/** + * Returns the number of unit to skip to be able to display up to `capacity` number of ticks + * in `unit` for the given `min` / `max` range and respecting the interval steps constraints. + */ +function determineStepSize(min, max, unit, capacity) { + var range = max - min; + var interval = INTERVALS[unit]; + var milliseconds = interval.size; + var steps = interval.steps; + var i, ilen, factor; + + if (!steps) { + return Math.ceil(range / (capacity * milliseconds)); + } + + for (i = 0, ilen = steps.length; i < ilen; ++i) { + factor = steps[i]; + if (Math.ceil(range / (milliseconds * factor)) <= capacity) { + break; + } + } + + return factor; +} + +/** + * Figures out what unit results in an appropriate number of auto-generated ticks + */ +function determineUnitForAutoTicks(minUnit, min, max, capacity) { + var ilen = UNITS.length; + var i, interval, factor; + + for (i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) { + interval = INTERVALS[UNITS[i]]; + factor = interval.steps ? interval.steps[interval.steps.length - 1] : MAX_INTEGER; + + if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) { + return UNITS[i]; + } + } + + return UNITS[ilen - 1]; +} + +/** + * Figures out what unit to format a set of ticks with + */ +function determineUnitForFormatting(scale, ticks, minUnit, min, max) { + var ilen = UNITS.length; + var i, unit; + + for (i = ilen - 1; i >= UNITS.indexOf(minUnit); i--) { + unit = UNITS[i]; + if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= ticks.length) { + return unit; + } + } + + return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0]; +} + +function determineMajorUnit(unit) { + for (var i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { + if (INTERVALS[UNITS[i]].common) { + return UNITS[i]; + } + } +} + +/** + * Generates a maximum of `capacity` timestamps between min and max, rounded to the + * `minor` unit, aligned on the `major` unit and using the given scale time `options`. + * Important: this method can return ticks outside the min and max range, it's the + * responsibility of the calling code to clamp values if needed. + */ +function generate(scale, min, max, capacity) { + var adapter = scale._adapter; + var options = scale.options; + var timeOpts = options.time; + var minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity); + var major = determineMajorUnit(minor); + var stepSize = valueOrDefault$c(timeOpts.stepSize, timeOpts.unitStepSize); + var weekday = minor === 'week' ? timeOpts.isoWeekday : false; + var majorTicksEnabled = options.ticks.major.enabled; + var interval = INTERVALS[minor]; + var first = min; + var last = max; + var ticks = []; + var time; + + if (!stepSize) { + stepSize = determineStepSize(min, max, minor, capacity); + } + + // For 'week' unit, handle the first day of week option + if (weekday) { + first = +adapter.startOf(first, 'isoWeek', weekday); + last = +adapter.startOf(last, 'isoWeek', weekday); + } + + // Align first/last ticks on unit + first = +adapter.startOf(first, weekday ? 'day' : minor); + last = +adapter.startOf(last, weekday ? 'day' : minor); + + // Make sure that the last tick include max + if (last < max) { + last = +adapter.add(last, 1, minor); + } + + time = first; + + if (majorTicksEnabled && major && !weekday && !timeOpts.round) { + // Align the first tick on the previous `minor` unit aligned on the `major` unit: + // we first aligned time on the previous `major` unit then add the number of full + // stepSize there is between first and the previous major time. + time = +adapter.startOf(time, major); + time = +adapter.add(time, ~~((first - time) / (interval.size * stepSize)) * stepSize, minor); + } + + for (; time < last; time = +adapter.add(time, stepSize, minor)) { + ticks.push(+time); + } + + ticks.push(+time); + + return ticks; +} + +/** + * Returns the start and end offsets from edges in the form of {start, end} + * where each value is a relative width to the scale and ranges between 0 and 1. + * They add extra margins on the both sides by scaling down the original scale. + * Offsets are added when the `offset` option is true. + */ +function computeOffsets(table, ticks, min, max, options) { + var start = 0; + var end = 0; + var first, last; + + if (options.offset && ticks.length) { + if (!options.time.min) { + first = interpolate$1(table, 'time', ticks[0], 'pos'); + if (ticks.length === 1) { + start = 1 - first; + } else { + start = (interpolate$1(table, 'time', ticks[1], 'pos') - first) / 2; + } + } + if (!options.time.max) { + last = interpolate$1(table, 'time', ticks[ticks.length - 1], 'pos'); + if (ticks.length === 1) { + end = last; + } else { + end = (last - interpolate$1(table, 'time', ticks[ticks.length - 2], 'pos')) / 2; + } + } + } + + return {start: start, end: end}; +} + +function ticksFromTimestamps(scale, values, majorUnit) { + var ticks = []; + var i, ilen, value, major; + + for (i = 0, ilen = values.length; i < ilen; ++i) { + value = values[i]; + major = majorUnit ? value === +scale._adapter.startOf(value, majorUnit) : false; + + ticks.push({ + value: value, + major: major + }); + } + + return ticks; +} + +var defaultConfig$4 = { + position: 'bottom', + + /** + * Data distribution along the scale: + * - 'linear': data are spread according to their time (distances can vary), + * - 'series': data are spread at the same distance from each other. + * @see https://github.com/chartjs/Chart.js/pull/4507 + * @since 2.7.0 + */ + distribution: 'linear', + + /** + * Scale boundary strategy (bypassed by min/max time options) + * - `data`: make sure data are fully visible, ticks outside are removed + * - `ticks`: make sure ticks are fully visible, data outside are truncated + * @see https://github.com/chartjs/Chart.js/pull/4556 + * @since 2.7.0 + */ + bounds: 'data', + + adapters: {}, + time: { + parser: false, // false == a pattern string from https://momentjs.com/docs/#/parsing/string-format/ or a custom callback that converts its argument to a moment + format: false, // DEPRECATED false == date objects, moment object, callback or a pattern string from https://momentjs.com/docs/#/parsing/string-format/ + unit: false, // false == automatic or override with week, month, year, etc. + round: false, // none, or override with week, month, year, etc. + displayFormat: false, // DEPRECATED + isoWeekday: false, // override week start day - see https://momentjs.com/docs/#/get-set/iso-weekday/ + minUnit: 'millisecond', + displayFormats: {} + }, + ticks: { + autoSkip: false, + + /** + * Ticks generation input values: + * - 'auto': generates "optimal" ticks based on scale size and time options. + * - 'data': generates ticks from data (including labels from data {t|x|y} objects). + * - 'labels': generates ticks from user given `data.labels` values ONLY. + * @see https://github.com/chartjs/Chart.js/pull/4507 + * @since 2.7.0 + */ + source: 'auto', + + major: { + enabled: false + } + } +}; + +var scale_time = core_scale.extend({ + initialize: function() { + this.mergeTicksOptions(); + core_scale.prototype.initialize.call(this); + }, + + update: function() { + var me = this; + var options = me.options; + var time = options.time || (options.time = {}); + var adapter = me._adapter = new core_adapters._date(options.adapters.date); + + // DEPRECATIONS: output a message only one time per update + if (time.format) { + console.warn('options.time.format is deprecated and replaced by options.time.parser.'); + } + + // Backward compatibility: before introducing adapter, `displayFormats` was + // supposed to contain *all* unit/string pairs but this can't be resolved + // when loading the scale (adapters are loaded afterward), so let's populate + // missing formats on update + helpers$1.mergeIf(time.displayFormats, adapter.formats()); + + return core_scale.prototype.update.apply(me, arguments); + }, + + /** + * Allows data to be referenced via 't' attribute + */ + getRightValue: function(rawValue) { + if (rawValue && rawValue.t !== undefined) { + rawValue = rawValue.t; + } + return core_scale.prototype.getRightValue.call(this, rawValue); + }, + + determineDataLimits: function() { + var me = this; + var chart = me.chart; + var adapter = me._adapter; + var timeOpts = me.options.time; + var unit = timeOpts.unit || 'day'; + var min = MAX_INTEGER; + var max = MIN_INTEGER; + var timestamps = []; + var datasets = []; + var labels = []; + var i, j, ilen, jlen, data, timestamp; + var dataLabels = chart.data.labels || []; + + // Convert labels to timestamps + for (i = 0, ilen = dataLabels.length; i < ilen; ++i) { + labels.push(parse(me, dataLabels[i])); + } + + // Convert data to timestamps + for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) { + if (chart.isDatasetVisible(i)) { + data = chart.data.datasets[i].data; + + // Let's consider that all data have the same format. + if (helpers$1.isObject(data[0])) { + datasets[i] = []; + + for (j = 0, jlen = data.length; j < jlen; ++j) { + timestamp = parse(me, data[j]); + timestamps.push(timestamp); + datasets[i][j] = timestamp; + } + } else { + for (j = 0, jlen = labels.length; j < jlen; ++j) { + timestamps.push(labels[j]); + } + datasets[i] = labels.slice(0); + } + } else { + datasets[i] = []; + } + } + + if (labels.length) { + // Sort labels **after** data have been converted + labels = arrayUnique(labels).sort(sorter); + min = Math.min(min, labels[0]); + max = Math.max(max, labels[labels.length - 1]); + } + + if (timestamps.length) { + timestamps = arrayUnique(timestamps).sort(sorter); + min = Math.min(min, timestamps[0]); + max = Math.max(max, timestamps[timestamps.length - 1]); + } + + min = parse(me, timeOpts.min) || min; + max = parse(me, timeOpts.max) || max; + + // In case there is no valid min/max, set limits based on unit time option + min = min === MAX_INTEGER ? +adapter.startOf(Date.now(), unit) : min; + max = max === MIN_INTEGER ? +adapter.endOf(Date.now(), unit) + 1 : max; + + // Make sure that max is strictly higher than min (required by the lookup table) + me.min = Math.min(min, max); + me.max = Math.max(min + 1, max); + + // PRIVATE + me._horizontal = me.isHorizontal(); + me._table = []; + me._timestamps = { + data: timestamps, + datasets: datasets, + labels: labels + }; + }, + + buildTicks: function() { + var me = this; + var min = me.min; + var max = me.max; + var options = me.options; + var timeOpts = options.time; + var timestamps = []; + var ticks = []; + var i, ilen, timestamp; + + switch (options.ticks.source) { + case 'data': + timestamps = me._timestamps.data; + break; + case 'labels': + timestamps = me._timestamps.labels; + break; + case 'auto': + default: + timestamps = generate(me, min, max, me.getLabelCapacity(min), options); + } + + if (options.bounds === 'ticks' && timestamps.length) { + min = timestamps[0]; + max = timestamps[timestamps.length - 1]; + } + + // Enforce limits with user min/max options + min = parse(me, timeOpts.min) || min; + max = parse(me, timeOpts.max) || max; + + // Remove ticks outside the min/max range + for (i = 0, ilen = timestamps.length; i < ilen; ++i) { + timestamp = timestamps[i]; + if (timestamp >= min && timestamp <= max) { + ticks.push(timestamp); + } + } + + me.min = min; + me.max = max; + + // PRIVATE + me._unit = timeOpts.unit || determineUnitForFormatting(me, ticks, timeOpts.minUnit, me.min, me.max); + me._majorUnit = determineMajorUnit(me._unit); + me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); + me._offsets = computeOffsets(me._table, ticks, min, max, options); + + if (options.ticks.reverse) { + ticks.reverse(); + } + + return ticksFromTimestamps(me, ticks, me._majorUnit); + }, + + getLabelForIndex: function(index, datasetIndex) { + var me = this; + var adapter = me._adapter; + var data = me.chart.data; + var timeOpts = me.options.time; + var label = data.labels && index < data.labels.length ? data.labels[index] : ''; + var value = data.datasets[datasetIndex].data[index]; + + if (helpers$1.isObject(value)) { + label = me.getRightValue(value); + } + if (timeOpts.tooltipFormat) { + return adapter.format(toTimestamp(me, label), timeOpts.tooltipFormat); + } + if (typeof label === 'string') { + return label; + } + return adapter.format(toTimestamp(me, label), timeOpts.displayFormats.datetime); + }, + + /** + * Function to format an individual tick mark + * @private + */ + tickFormatFunction: function(time, index, ticks, format) { + var me = this; + var adapter = me._adapter; + var options = me.options; + var formats = options.time.displayFormats; + var minorFormat = formats[me._unit]; + var majorUnit = me._majorUnit; + var majorFormat = formats[majorUnit]; + var majorTime = +adapter.startOf(time, majorUnit); + var majorTickOpts = options.ticks.major; + var major = majorTickOpts.enabled && majorUnit && majorFormat && time === majorTime; + var label = adapter.format(time, format ? format : major ? majorFormat : minorFormat); + var tickOpts = major ? majorTickOpts : options.ticks.minor; + var formatter = valueOrDefault$c(tickOpts.callback, tickOpts.userCallback); + + return formatter ? formatter(label, index, ticks) : label; + }, + + convertTicksToLabels: function(ticks) { + var labels = []; + var i, ilen; + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + labels.push(this.tickFormatFunction(ticks[i].value, i, ticks)); + } + + return labels; + }, + + /** + * @private + */ + getPixelForOffset: function(time) { + var me = this; + var isReverse = me.options.ticks.reverse; + var size = me._horizontal ? me.width : me.height; + var start = me._horizontal ? isReverse ? me.right : me.left : isReverse ? me.bottom : me.top; + var pos = interpolate$1(me._table, 'time', time, 'pos'); + var offset = size * (me._offsets.start + pos) / (me._offsets.start + 1 + me._offsets.end); + + return isReverse ? start - offset : start + offset; + }, + + getPixelForValue: function(value, index, datasetIndex) { + var me = this; + var time = null; + + if (index !== undefined && datasetIndex !== undefined) { + time = me._timestamps.datasets[datasetIndex][index]; + } + + if (time === null) { + time = parse(me, value); + } + + if (time !== null) { + return me.getPixelForOffset(time); + } + }, + + getPixelForTick: function(index) { + var ticks = this.getTicks(); + return index >= 0 && index < ticks.length ? + this.getPixelForOffset(ticks[index].value) : + null; + }, + + getValueForPixel: function(pixel) { + var me = this; + var size = me._horizontal ? me.width : me.height; + var start = me._horizontal ? me.left : me.top; + var pos = (size ? (pixel - start) / size : 0) * (me._offsets.start + 1 + me._offsets.start) - me._offsets.end; + var time = interpolate$1(me._table, 'pos', pos, 'time'); + + // DEPRECATION, we should return time directly + return me._adapter._create(time); + }, + + /** + * Crude approximation of what the label width might be + * @private + */ + getLabelWidth: function(label) { + var me = this; + var ticksOpts = me.options.ticks; + var tickLabelWidth = me.ctx.measureText(label).width; + var angle = helpers$1.toRadians(ticksOpts.maxRotation); + var cosRotation = Math.cos(angle); + var sinRotation = Math.sin(angle); + var tickFontSize = valueOrDefault$c(ticksOpts.fontSize, core_defaults.global.defaultFontSize); + + return (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation); + }, + + /** + * @private + */ + getLabelCapacity: function(exampleTime) { + var me = this; + + // pick the longest format (milliseconds) for guestimation + var format = me.options.time.displayFormats.millisecond; + var exampleLabel = me.tickFormatFunction(exampleTime, 0, [], format); + var tickLabelWidth = me.getLabelWidth(exampleLabel); + var innerWidth = me.isHorizontal() ? me.width : me.height; + var capacity = Math.floor(innerWidth / tickLabelWidth); + + return capacity > 0 ? capacity : 1; + } +}); + +// INTERNAL: static default options, registered in src/index.js +var _defaults$4 = defaultConfig$4; +scale_time._defaults = _defaults$4; + +var scales = { + category: scale_category, + linear: scale_linear, + logarithmic: scale_logarithmic, + radialLinear: scale_radialLinear, + time: scale_time +}; + +var FORMATS = { + datetime: 'MMM D, YYYY, h:mm:ss a', + millisecond: 'h:mm:ss.SSS a', + second: 'h:mm:ss a', + minute: 'h:mm a', + hour: 'hA', + day: 'MMM D', + week: 'll', + month: 'MMM YYYY', + quarter: '[Q]Q - YYYY', + year: 'YYYY' +}; + +core_adapters._date.override(typeof moment === 'function' ? { + _id: 'moment', // DEBUG ONLY + + formats: function() { + return FORMATS; + }, + + parse: function(value, format) { + if (typeof value === 'string' && typeof format === 'string') { + value = moment(value, format); + } else if (!(value instanceof moment)) { + value = moment(value); + } + return value.isValid() ? value.valueOf() : null; + }, + + format: function(time, format) { + return moment(time).format(format); + }, + + add: function(time, amount, unit) { + return moment(time).add(amount, unit).valueOf(); + }, + + diff: function(max, min, unit) { + return moment.duration(moment(max).diff(moment(min))).as(unit); + }, + + startOf: function(time, unit, weekday) { + time = moment(time); + if (unit === 'isoWeek') { + return time.isoWeekday(weekday).valueOf(); + } + return time.startOf(unit).valueOf(); + }, + + endOf: function(time, unit) { + return moment(time).endOf(unit).valueOf(); + }, + + // DEPRECATIONS + + /** + * Provided for backward compatibility with scale.getValueForPixel(). + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ + _create: function(time) { + return moment(time); + }, +} : {}); + +core_defaults._set('global', { + plugins: { + filler: { + propagate: true + } + } +}); + +var mappers = { + dataset: function(source) { + var index = source.fill; + var chart = source.chart; + var meta = chart.getDatasetMeta(index); + var visible = meta && chart.isDatasetVisible(index); + var points = (visible && meta.dataset._children) || []; + var length = points.length || 0; + + return !length ? null : function(point, i) { + return (i < length && points[i]._view) || null; + }; + }, + + boundary: function(source) { + var boundary = source.boundary; + var x = boundary ? boundary.x : null; + var y = boundary ? boundary.y : null; + + return function(point) { + return { + x: x === null ? point.x : x, + y: y === null ? point.y : y, + }; + }; + } +}; + +// @todo if (fill[0] === '#') +function decodeFill(el, index, count) { + var model = el._model || {}; + var fill = model.fill; + var target; + + if (fill === undefined) { + fill = !!model.backgroundColor; + } + + if (fill === false || fill === null) { + return false; + } + + if (fill === true) { + return 'origin'; + } + + target = parseFloat(fill, 10); + if (isFinite(target) && Math.floor(target) === target) { + if (fill[0] === '-' || fill[0] === '+') { + target = index + target; + } + + if (target === index || target < 0 || target >= count) { + return false; + } + + return target; + } + + switch (fill) { + // compatibility + case 'bottom': + return 'start'; + case 'top': + return 'end'; + case 'zero': + return 'origin'; + // supported boundaries + case 'origin': + case 'start': + case 'end': + return fill; + // invalid fill values + default: + return false; + } +} + +function computeBoundary(source) { + var model = source.el._model || {}; + var scale = source.el._scale || {}; + var fill = source.fill; + var target = null; + var horizontal; + + if (isFinite(fill)) { + return null; + } + + // Backward compatibility: until v3, we still need to support boundary values set on + // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and + // controllers might still use it (e.g. the Smith chart). + + if (fill === 'start') { + target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; + } else if (fill === 'end') { + target = model.scaleTop === undefined ? scale.top : model.scaleTop; + } else if (model.scaleZero !== undefined) { + target = model.scaleZero; + } else if (scale.getBasePosition) { + target = scale.getBasePosition(); + } else if (scale.getBasePixel) { + target = scale.getBasePixel(); + } + + if (target !== undefined && target !== null) { + if (target.x !== undefined && target.y !== undefined) { + return target; + } + + if (helpers$1.isFinite(target)) { + horizontal = scale.isHorizontal(); + return { + x: horizontal ? target : null, + y: horizontal ? null : target + }; + } + } + + return null; +} + +function resolveTarget(sources, index, propagate) { + var source = sources[index]; + var fill = source.fill; + var visited = [index]; + var target; + + if (!propagate) { + return fill; + } + + while (fill !== false && visited.indexOf(fill) === -1) { + if (!isFinite(fill)) { + return fill; + } + + target = sources[fill]; + if (!target) { + return false; + } + + if (target.visible) { + return fill; + } + + visited.push(fill); + fill = target.fill; + } + + return false; +} + +function createMapper(source) { + var fill = source.fill; + var type = 'dataset'; + + if (fill === false) { + return null; + } + + if (!isFinite(fill)) { + type = 'boundary'; + } + + return mappers[type](source); +} + +function isDrawable(point) { + return point && !point.skip; +} + +function drawArea(ctx, curve0, curve1, len0, len1) { + var i; + + if (!len0 || !len1) { + return; + } + + // building first area curve (normal) + ctx.moveTo(curve0[0].x, curve0[0].y); + for (i = 1; i < len0; ++i) { + helpers$1.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); + } + + // joining the two area curves + ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); + + // building opposite area curve (reverse) + for (i = len1 - 1; i > 0; --i) { + helpers$1.canvas.lineTo(ctx, curve1[i], curve1[i - 1], true); + } +} + +function doFill(ctx, points, mapper, view, color, loop) { + var count = points.length; + var span = view.spanGaps; + var curve0 = []; + var curve1 = []; + var len0 = 0; + var len1 = 0; + var i, ilen, index, p0, p1, d0, d1; + + ctx.beginPath(); + + for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { + index = i % count; + p0 = points[index]._view; + p1 = mapper(p0, index, view); + d0 = isDrawable(p0); + d1 = isDrawable(p1); + + if (d0 && d1) { + len0 = curve0.push(p0); + len1 = curve1.push(p1); + } else if (len0 && len1) { + if (!span) { + drawArea(ctx, curve0, curve1, len0, len1); + len0 = len1 = 0; + curve0 = []; + curve1 = []; + } else { + if (d0) { + curve0.push(p0); + } + if (d1) { + curve1.push(p1); + } + } + } + } + + drawArea(ctx, curve0, curve1, len0, len1); + + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); +} + +var plugin_filler = { + id: 'filler', + + afterDatasetsUpdate: function(chart, options) { + var count = (chart.data.datasets || []).length; + var propagate = options.propagate; + var sources = []; + var meta, i, el, source; + + for (i = 0; i < count; ++i) { + meta = chart.getDatasetMeta(i); + el = meta.dataset; + source = null; + + if (el && el._model && el instanceof elements.Line) { + source = { + visible: chart.isDatasetVisible(i), + fill: decodeFill(el, i, count), + chart: chart, + el: el + }; + } + + meta.$filler = source; + sources.push(source); + } + + for (i = 0; i < count; ++i) { + source = sources[i]; + if (!source) { + continue; + } + + source.fill = resolveTarget(sources, i, propagate); + source.boundary = computeBoundary(source); + source.mapper = createMapper(source); + } + }, + + beforeDatasetDraw: function(chart, args) { + var meta = args.meta.$filler; + if (!meta) { + return; + } + + var ctx = chart.ctx; + var el = meta.el; + var view = el._view; + var points = el._children || []; + var mapper = meta.mapper; + var color = view.backgroundColor || core_defaults.global.defaultColor; + + if (mapper && color && points.length) { + helpers$1.canvas.clipArea(ctx, chart.chartArea); + doFill(ctx, points, mapper, view, color, el._loop); + helpers$1.canvas.unclipArea(ctx); + } + } +}; + +var noop$1 = helpers$1.noop; +var valueOrDefault$d = helpers$1.valueOrDefault; + +core_defaults._set('global', { + legend: { + display: true, + position: 'top', + fullWidth: true, + reverse: false, + weight: 1000, + + // a callback that will handle + onClick: function(e, legendItem) { + var index = legendItem.datasetIndex; + var ci = this.chart; + var meta = ci.getDatasetMeta(index); + + // See controller.isDatasetVisible comment + meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null; + + // We hid a dataset ... rerender the chart + ci.update(); + }, + + onHover: null, + onLeave: null, + + labels: { + boxWidth: 40, + padding: 10, + // Generates labels shown in the legend + // Valid properties to return: + // text : text to display + // fillStyle : fill of coloured box + // strokeStyle: stroke of coloured box + // hidden : if this legend item refers to a hidden item + // lineCap : cap style for line + // lineDash + // lineDashOffset : + // lineJoin : + // lineWidth : + generateLabels: function(chart) { + var data = chart.data; + return helpers$1.isArray(data.datasets) ? data.datasets.map(function(dataset, i) { + return { + text: dataset.label, + fillStyle: (!helpers$1.isArray(dataset.backgroundColor) ? dataset.backgroundColor : dataset.backgroundColor[0]), + hidden: !chart.isDatasetVisible(i), + lineCap: dataset.borderCapStyle, + lineDash: dataset.borderDash, + lineDashOffset: dataset.borderDashOffset, + lineJoin: dataset.borderJoinStyle, + lineWidth: dataset.borderWidth, + strokeStyle: dataset.borderColor, + pointStyle: dataset.pointStyle, + + // Below is extra data used for toggling the datasets + datasetIndex: i + }; + }, this) : []; + } + } + }, + + legendCallback: function(chart) { + var text = []; + text.push('
    '); + for (var i = 0; i < chart.data.datasets.length; i++) { + text.push('
  • '); + if (chart.data.datasets[i].label) { + text.push(chart.data.datasets[i].label); + } + text.push('
  • '); + } + text.push('
'); + return text.join(''); + } +}); + +/** + * Helper function to get the box width based on the usePointStyle option + * @param {object} labelopts - the label options on the legend + * @param {number} fontSize - the label font size + * @return {number} width of the color box area + */ +function getBoxWidth(labelOpts, fontSize) { + return labelOpts.usePointStyle && labelOpts.boxWidth > fontSize ? + fontSize : + labelOpts.boxWidth; +} + +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Legend = core_element.extend({ + + initialize: function(config) { + helpers$1.extend(this, config); + + // Contains hit boxes for each dataset (in dataset order) + this.legendHitBoxes = []; + + /** + * @private + */ + this._hoveredItem = null; + + // Are we in doughnut mode which has a different data type + this.doughnutMode = false; + }, + + // These methods are ordered by lifecycle. Utilities then follow. + // Any function defined here is inherited by all legend types. + // Any function can be extended by the legend type + + beforeUpdate: noop$1, + update: function(maxWidth, maxHeight, margins) { + var me = this; + + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); + + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; + + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); + // + me.afterUpdate(); + + return me.minSize; + }, + afterUpdate: noop$1, + + // + + beforeSetDimensions: noop$1, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } + + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop$1, + + // + + beforeBuildLabels: noop$1, + buildLabels: function() { + var me = this; + var labelOpts = me.options.labels || {}; + var legendItems = helpers$1.callback(labelOpts.generateLabels, [me.chart], me) || []; + + if (labelOpts.filter) { + legendItems = legendItems.filter(function(item) { + return labelOpts.filter(item, me.chart.data); + }); + } + + if (me.options.reverse) { + legendItems.reverse(); + } + + me.legendItems = legendItems; + }, + afterBuildLabels: noop$1, + + // + + beforeFit: noop$1, + fit: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var display = opts.display; + + var ctx = me.ctx; + + var labelFont = helpers$1.options._parseFont(labelOpts); + var fontSize = labelFont.size; + + // Reset hit boxes + var hitboxes = me.legendHitBoxes = []; + + var minSize = me.minSize; + var isHorizontal = me.isHorizontal(); + + if (isHorizontal) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = display ? 10 : 0; + } else { + minSize.width = display ? 10 : 0; + minSize.height = me.maxHeight; // fill all the height + } + + // Increase sizes here + if (display) { + ctx.font = labelFont.string; + + if (isHorizontal) { + // Labels + + // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one + var lineWidths = me.lineWidths = [0]; + var totalHeight = 0; + + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + helpers$1.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + + if (i === 0 || lineWidths[lineWidths.length - 1] + width + labelOpts.padding > minSize.width) { + totalHeight += fontSize + labelOpts.padding; + lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = labelOpts.padding; + } + + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: width, + height: fontSize + }; + + lineWidths[lineWidths.length - 1] += width + labelOpts.padding; + }); + + minSize.height += totalHeight; + + } else { + var vPadding = labelOpts.padding; + var columnWidths = me.columnWidths = []; + var totalWidth = labelOpts.padding; + var currentColWidth = 0; + var currentColHeight = 0; + var itemHeight = fontSize + vPadding; + + helpers$1.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + + // If too tall, go to new column + if (i > 0 && currentColHeight + itemHeight > minSize.height - vPadding) { + totalWidth += currentColWidth + labelOpts.padding; + columnWidths.push(currentColWidth); // previous column width + + currentColWidth = 0; + currentColHeight = 0; + } + + // Get max width + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += itemHeight; + + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: itemWidth, + height: fontSize + }; + }); + + totalWidth += currentColWidth; + columnWidths.push(currentColWidth); + minSize.width += totalWidth; + } + } + + me.width = minSize.width; + me.height = minSize.height; + }, + afterFit: noop$1, + + // Shared Methods + isHorizontal: function() { + return this.options.position === 'top' || this.options.position === 'bottom'; + }, + + // Actually draw the legend on the canvas + draw: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var globalDefaults = core_defaults.global; + var defaultColor = globalDefaults.defaultColor; + var lineDefault = globalDefaults.elements.line; + var legendWidth = me.width; + var lineWidths = me.lineWidths; + + if (opts.display) { + var ctx = me.ctx; + var fontColor = valueOrDefault$d(labelOpts.fontColor, globalDefaults.defaultFontColor); + var labelFont = helpers$1.options._parseFont(labelOpts); + var fontSize = labelFont.size; + var cursor; + + // Canvas setup + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.lineWidth = 0.5; + ctx.strokeStyle = fontColor; // for strikethrough effect + ctx.fillStyle = fontColor; // render in correct colour + ctx.font = labelFont.string; + + var boxWidth = getBoxWidth(labelOpts, fontSize); + var hitboxes = me.legendHitBoxes; + + // current position + var drawLegendBox = function(x, y, legendItem) { + if (isNaN(boxWidth) || boxWidth <= 0) { + return; + } + + // Set the ctx for the box + ctx.save(); + + var lineWidth = valueOrDefault$d(legendItem.lineWidth, lineDefault.borderWidth); + ctx.fillStyle = valueOrDefault$d(legendItem.fillStyle, defaultColor); + ctx.lineCap = valueOrDefault$d(legendItem.lineCap, lineDefault.borderCapStyle); + ctx.lineDashOffset = valueOrDefault$d(legendItem.lineDashOffset, lineDefault.borderDashOffset); + ctx.lineJoin = valueOrDefault$d(legendItem.lineJoin, lineDefault.borderJoinStyle); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = valueOrDefault$d(legendItem.strokeStyle, defaultColor); + + if (ctx.setLineDash) { + // IE 9 and 10 do not support line dash + ctx.setLineDash(valueOrDefault$d(legendItem.lineDash, lineDefault.borderDash)); + } + + if (opts.labels && opts.labels.usePointStyle) { + // Recalculate x and y for drawPoint() because its expecting + // x and y to be center of figure (instead of top left) + var radius = boxWidth * Math.SQRT2 / 2; + var centerX = x + boxWidth / 2; + var centerY = y + fontSize / 2; + + // Draw pointStyle as legend symbol + helpers$1.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); + } else { + // Draw box as legend symbol + if (lineWidth !== 0) { + ctx.strokeRect(x, y, boxWidth, fontSize); + } + ctx.fillRect(x, y, boxWidth, fontSize); + } + + ctx.restore(); + }; + var fillText = function(x, y, legendItem, textWidth) { + var halfFontSize = fontSize / 2; + var xLeft = boxWidth + halfFontSize + x; + var yMiddle = y + halfFontSize; + + ctx.fillText(legendItem.text, xLeft, yMiddle); + + if (legendItem.hidden) { + // Strikethrough the text if hidden + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.moveTo(xLeft, yMiddle); + ctx.lineTo(xLeft + textWidth, yMiddle); + ctx.stroke(); + } + }; + + // Horizontal + var isHorizontal = me.isHorizontal(); + if (isHorizontal) { + cursor = { + x: me.left + ((legendWidth - lineWidths[0]) / 2) + labelOpts.padding, + y: me.top + labelOpts.padding, + line: 0 + }; + } else { + cursor = { + x: me.left + labelOpts.padding, + y: me.top + labelOpts.padding, + line: 0 + }; + } + + var itemHeight = fontSize + labelOpts.padding; + helpers$1.each(me.legendItems, function(legendItem, i) { + var textWidth = ctx.measureText(legendItem.text).width; + var width = boxWidth + (fontSize / 2) + textWidth; + var x = cursor.x; + var y = cursor.y; + + // Use (me.left + me.minSize.width) and (me.top + me.minSize.height) + // instead of me.right and me.bottom because me.width and me.height + // may have been changed since me.minSize was calculated + if (isHorizontal) { + if (i > 0 && x + width + labelOpts.padding > me.left + me.minSize.width) { + y = cursor.y += itemHeight; + cursor.line++; + x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2) + labelOpts.padding; + } + } else if (i > 0 && y + itemHeight > me.top + me.minSize.height) { + x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; + y = cursor.y = me.top + labelOpts.padding; + cursor.line++; + } + + drawLegendBox(x, y, legendItem); + + hitboxes[i].left = x; + hitboxes[i].top = y; + + // Fill the actual label + fillText(x, y, legendItem, textWidth); + + if (isHorizontal) { + cursor.x += width + labelOpts.padding; + } else { + cursor.y += itemHeight; + } + + }); + } + }, + + /** + * @private + */ + _getLegendItemAt: function(x, y) { + var me = this; + var i, hitBox, lh; + + if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { + // See if we are touching one of the dataset boxes + lh = me.legendHitBoxes; + for (i = 0; i < lh.length; ++i) { + hitBox = lh[i]; + + if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { + // Touching an element + return me.legendItems[i]; + } + } + } + + return null; + }, + + /** + * Handle an event + * @private + * @param {IEvent} event - The event to handle + */ + handleEvent: function(e) { + var me = this; + var opts = me.options; + var type = e.type === 'mouseup' ? 'click' : e.type; + var hoveredItem; + + if (type === 'mousemove') { + if (!opts.onHover && !opts.onLeave) { + return; + } + } else if (type === 'click') { + if (!opts.onClick) { + return; + } + } else { + return; + } + + // Chart event already has relative position in it + hoveredItem = me._getLegendItemAt(e.x, e.y); + + if (type === 'click') { + if (hoveredItem && opts.onClick) { + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, hoveredItem); + } + } else { + if (opts.onLeave && hoveredItem !== me._hoveredItem) { + if (me._hoveredItem) { + opts.onLeave.call(me, e.native, me._hoveredItem); + } + me._hoveredItem = hoveredItem; + } + + if (opts.onHover && hoveredItem) { + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, hoveredItem); + } + } + } +}); + +function createNewLegendAndAttach(chart, legendOpts) { + var legend = new Legend({ + ctx: chart.ctx, + options: legendOpts, + chart: chart + }); + + core_layouts.configure(chart, legend, legendOpts); + core_layouts.addBox(chart, legend); + chart.legend = legend; +} + +var plugin_legend = { + id: 'legend', + + /** + * Backward compatibility: since 2.1.5, the legend is registered as a plugin, making + * Chart.Legend obsolete. To avoid a breaking change, we export the Legend as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Legend, + + beforeInit: function(chart) { + var legendOpts = chart.options.legend; + + if (legendOpts) { + createNewLegendAndAttach(chart, legendOpts); + } + }, + + beforeUpdate: function(chart) { + var legendOpts = chart.options.legend; + var legend = chart.legend; + + if (legendOpts) { + helpers$1.mergeIf(legendOpts, core_defaults.global.legend); + + if (legend) { + core_layouts.configure(chart, legend, legendOpts); + legend.options = legendOpts; + } else { + createNewLegendAndAttach(chart, legendOpts); + } + } else if (legend) { + core_layouts.removeBox(chart, legend); + delete chart.legend; + } + }, + + afterEvent: function(chart, e) { + var legend = chart.legend; + if (legend) { + legend.handleEvent(e); + } + } +}; + +var noop$2 = helpers$1.noop; + +core_defaults._set('global', { + title: { + display: false, + fontStyle: 'bold', + fullWidth: true, + padding: 10, + position: 'top', + text: '', + weight: 2000 // by default greater than legend (1000) to be above + } +}); + +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Title = core_element.extend({ + initialize: function(config) { + var me = this; + helpers$1.extend(me, config); + + // Contains hit boxes for each dataset (in dataset order) + me.legendHitBoxes = []; + }, + + // These methods are ordered by lifecycle. Utilities then follow. + + beforeUpdate: noop$2, + update: function(maxWidth, maxHeight, margins) { + var me = this; + + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); + + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; + + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); + // + me.afterUpdate(); + + return me.minSize; + + }, + afterUpdate: noop$2, + + // + + beforeSetDimensions: noop$2, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } + + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop$2, + + // + + beforeBuildLabels: noop$2, + buildLabels: noop$2, + afterBuildLabels: noop$2, + + // + + beforeFit: noop$2, + fit: function() { + var me = this; + var opts = me.options; + var display = opts.display; + var minSize = me.minSize; + var lineCount = helpers$1.isArray(opts.text) ? opts.text.length : 1; + var fontOpts = helpers$1.options._parseFont(opts); + var textSize = display ? (lineCount * fontOpts.lineHeight) + (opts.padding * 2) : 0; + + if (me.isHorizontal()) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = textSize; + } else { + minSize.width = textSize; + minSize.height = me.maxHeight; // fill all the height + } + + me.width = minSize.width; + me.height = minSize.height; + + }, + afterFit: noop$2, + + // Shared Methods + isHorizontal: function() { + var pos = this.options.position; + return pos === 'top' || pos === 'bottom'; + }, + + // Actually draw the title block on the canvas + draw: function() { + var me = this; + var ctx = me.ctx; + var opts = me.options; + + if (opts.display) { + var fontOpts = helpers$1.options._parseFont(opts); + var lineHeight = fontOpts.lineHeight; + var offset = lineHeight / 2 + opts.padding; + var rotation = 0; + var top = me.top; + var left = me.left; + var bottom = me.bottom; + var right = me.right; + var maxWidth, titleX, titleY; + + ctx.fillStyle = helpers$1.valueOrDefault(opts.fontColor, core_defaults.global.defaultFontColor); // render in correct colour + ctx.font = fontOpts.string; + + // Horizontal + if (me.isHorizontal()) { + titleX = left + ((right - left) / 2); // midpoint of the width + titleY = top + offset; + maxWidth = right - left; + } else { + titleX = opts.position === 'left' ? left + offset : right - offset; + titleY = top + ((bottom - top) / 2); + maxWidth = bottom - top; + rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); + } + + ctx.save(); + ctx.translate(titleX, titleY); + ctx.rotate(rotation); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + var text = opts.text; + if (helpers$1.isArray(text)) { + var y = 0; + for (var i = 0; i < text.length; ++i) { + ctx.fillText(text[i], 0, y, maxWidth); + y += lineHeight; + } + } else { + ctx.fillText(text, 0, 0, maxWidth); + } + + ctx.restore(); + } + } +}); + +function createNewTitleBlockAndAttach(chart, titleOpts) { + var title = new Title({ + ctx: chart.ctx, + options: titleOpts, + chart: chart + }); + + core_layouts.configure(chart, title, titleOpts); + core_layouts.addBox(chart, title); + chart.titleBlock = title; +} + +var plugin_title = { + id: 'title', + + /** + * Backward compatibility: since 2.1.5, the title is registered as a plugin, making + * Chart.Title obsolete. To avoid a breaking change, we export the Title as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Title, + + beforeInit: function(chart) { + var titleOpts = chart.options.title; + + if (titleOpts) { + createNewTitleBlockAndAttach(chart, titleOpts); + } + }, + + beforeUpdate: function(chart) { + var titleOpts = chart.options.title; + var titleBlock = chart.titleBlock; + + if (titleOpts) { + helpers$1.mergeIf(titleOpts, core_defaults.global.title); + + if (titleBlock) { + core_layouts.configure(chart, titleBlock, titleOpts); + titleBlock.options = titleOpts; + } else { + createNewTitleBlockAndAttach(chart, titleOpts); + } + } else if (titleBlock) { + core_layouts.removeBox(chart, titleBlock); + delete chart.titleBlock; + } + } +}; + +var plugins = {}; +var filler = plugin_filler; +var legend = plugin_legend; +var title = plugin_title; +plugins.filler = filler; +plugins.legend = legend; +plugins.title = title; + +/** + * @namespace Chart + */ + + +core_controller.helpers = helpers$1; + +// @todo dispatch these helpers into appropriated helpers/helpers.* file and write unit tests! +core_helpers(core_controller); + +core_controller._adapters = core_adapters; +core_controller.Animation = core_animation; +core_controller.animationService = core_animations; +core_controller.controllers = controllers; +core_controller.DatasetController = core_datasetController; +core_controller.defaults = core_defaults; +core_controller.Element = core_element; +core_controller.elements = elements; +core_controller.Interaction = core_interaction; +core_controller.layouts = core_layouts; +core_controller.platform = platform; +core_controller.plugins = core_plugins; +core_controller.Scale = core_scale; +core_controller.scaleService = core_scaleService; +core_controller.Ticks = core_ticks; +core_controller.Tooltip = core_tooltip; + +// Register built-in scales + +core_controller.helpers.each(scales, function(scale, type) { + core_controller.scaleService.registerScaleType(type, scale, scale._defaults); +}); + +// Load to register built-in adapters (as side effects) + + +// Loading built-in plugins + +for (var k in plugins) { + if (plugins.hasOwnProperty(k)) { + core_controller.plugins.register(plugins[k]); + } +} + +core_controller.platform.initialize(); + +var src = core_controller; +if (typeof window !== 'undefined') { + window.Chart = core_controller; +} + +// DEPRECATIONS + +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Chart + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ +core_controller.Chart = core_controller; + +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Legend + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +core_controller.Legend = plugins.legend._element; + +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Title + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +core_controller.Title = plugins.title._element; + +/** + * Provided for backward compatibility, use Chart.plugins instead + * @namespace Chart.pluginService + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +core_controller.pluginService = core_controller.plugins; + +/** + * Provided for backward compatibility, inheriting from Chart.PlugingBase has no + * effect, instead simply create/register plugins via plain JavaScript objects. + * @interface Chart.PluginBase + * @deprecated since version 2.5.0 + * @todo remove at version 3 + * @private + */ +core_controller.PluginBase = core_controller.Element.extend({}); + +/** + * Provided for backward compatibility, use Chart.helpers.canvas instead. + * @namespace Chart.canvasHelpers + * @deprecated since version 2.6.0 + * @todo remove at version 3 + * @private + */ +core_controller.canvasHelpers = core_controller.helpers.canvas; + +/** + * Provided for backward compatibility, use Chart.layouts instead. + * @namespace Chart.layoutService + * @deprecated since version 2.7.3 + * @todo remove at version 3 + * @private + */ +core_controller.layoutService = core_controller.layouts; + +/** + * Provided for backward compatibility, not available anymore. + * @namespace Chart.LinearScaleBase + * @deprecated since version 2.8 + * @todo remove at version 3 + * @private + */ +core_controller.LinearScaleBase = scale_linearbase; + +/** + * Provided for backward compatibility, instead we should create a new Chart + * by setting the type in the config (`new Chart(id, {type: '{chart-type}'}`). + * @deprecated since version 2.8.0 + * @todo remove at version 3 + */ +core_controller.helpers.each( + [ + 'Bar', + 'Bubble', + 'Doughnut', + 'Line', + 'PolarArea', + 'Radar', + 'Scatter' + ], + function(klass) { + core_controller[klass] = function(ctx, cfg) { + return new core_controller(ctx, core_controller.helpers.merge(cfg || {}, { + type: klass.charAt(0).toLowerCase() + klass.slice(1) + })); + }; + } +); + +return src; + +}))); diff --git a/resources/libs/clipboard/clipboard.js b/resources/libs/clipboard/clipboard.js new file mode 100644 index 0000000..85e8e2d --- /dev/null +++ b/resources/libs/clipboard/clipboard.js @@ -0,0 +1,742 @@ +/*! + * clipboard.js v1.5.12 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0})},e=function(a,b){var c={},d=new RegExp("^"+b+"([A-Z])(.*)");for(var e in a){var f=e.match(d);if(f){var g=(f[1]+f[2].replace(/([A-Z])/g,"-$1")).toLowerCase();c[g]=a[e]}}return c},f={keyup:"onKeyUp",resize:"onResize"},g=function(c){a.each(b.opened().reverse(),function(){return c.isDefaultPrevented()||!1!==this[f[c.type]](c)?void 0:(c.preventDefault(),c.stopPropagation(),!1)})},h=function(c){if(c!==b._globalHandlerInstalled){b._globalHandlerInstalled=c;var d=a.map(f,function(a,c){return c+"."+b.prototype.namespace}).join(" ");a(window)[c?"on":"off"](d,g)}};b.prototype={constructor:b,namespace:"featherlight",targetAttr:"data-featherlight",variant:null,resetCss:!1,background:null,openTrigger:"click",closeTrigger:"click",filter:null,root:"body",openSpeed:250,closeSpeed:250,closeOnClick:"background",closeOnEsc:!0,closeIcon:"✕",loading:"",otherClose:null,beforeOpen:a.noop,beforeContent:a.noop,beforeClose:a.noop,afterOpen:a.noop,afterContent:a.noop,afterClose:a.noop,onKeyUp:a.noop,onResize:a.noop,type:null,contentFilters:["jquery","image","html","ajax","iframe","text"],setup:function(b,c){"object"!=typeof b||b instanceof a!=!1||c||(c=b,b=void 0);var d=a.extend(this,c,{target:b}),e=d.resetCss?d.namespace+"-reset":d.namespace,f=a(d.background||['
','
','',d.closeIcon,"",'
'+d.loading+"
","
","
"].join("")),g="."+d.namespace+"-close"+(d.otherClose?","+d.otherClose:"");return d.$instance=f.clone().addClass(d.variant),d.$instance.on(d.closeTrigger+"."+d.namespace,function(b){var c=a(b.target);("background"===d.closeOnClick&&c.is("."+d.namespace)||"anywhere"===d.closeOnClick||c.closest(g).length)&&(b.preventDefault(),d.close())}),this},getContent:function(){var b=this,c=this.constructor.contentFilters,d=function(a){return b.$currentTarget&&b.$currentTarget.attr(a)},e=d(b.targetAttr),f=b.target||e||"",g=c[b.type];if(!g&&f in c&&(g=c[f],f=b.target&&e),f=f||d("href")||"",!g)for(var h in c)b[h]&&(g=c[h],f=b[h]);if(!g){var i=f;if(f=null,a.each(b.contentFilters,function(){return g=c[this],g.test&&(f=g.test(i)),!f&&g.regex&&i.match&&i.match(g.regex)&&(f=i),!f}),!f)return"console"in window&&window.console.error("Featherlight: no content filter found "+(i?' for "'+i+'"':" (no target specified)")),!1}return g.process.call(b,f)},setContent:function(b){var c=this;return(b.is("iframe")||a("iframe",b).length>0)&&c.$instance.addClass(c.namespace+"-iframe"),c.$instance.removeClass(c.namespace+"-loading"),c.$instance.find("."+c.namespace+"-inner").slice(1).remove().end().replaceWith(a.contains(c.$instance[0],b[0])?"":b),c.$content=b.addClass(c.namespace+"-inner"),c},open:function(b){var d=this;if(!(b&&b.isDefaultPrevented()||d.beforeOpen(b)===!1)){b&&b.preventDefault();var e=d.getContent();if(e)return c.push(d),h(!0),d.$instance.appendTo(d.root).fadeIn(d.openSpeed),d.beforeContent(b),a.when(e).always(function(c){d.setContent(c),d.afterContent(b),a.when(d.$instance.promise()).done(function(){d.afterOpen(b)})}),d}return!1},close:function(a){var b=this;return b.beforeClose(a)===!1?!1:(0===d(b).length&&h(!1),void b.$instance.fadeOut(b.closeSpeed,function(){b.$instance.detach(),b.afterClose(a)}))},chainCallbacks:function(b){for(var c in b)this[c]=a.proxy(b[c],this,a.proxy(this[c],this))}},a.extend(b,{id:0,autoBind:"[data-featherlight]",defaults:b.prototype,contentFilters:{jquery:{regex:/^[#.]\w/,test:function(b){return b instanceof a&&b},process:function(b){return a(b).clone(!0)}},image:{regex:/\.(png|jpg|jpeg|gif|tiff|bmp)(\?\S*)?$/i,process:function(b){var c=this,d=a.Deferred(),e=new Image,f=a('');return e.onload=function(){f.naturalWidth=e.width,f.naturalHeight=e.height,d.resolve(f)},e.onerror=function(){d.reject(f)},e.src=b,d.promise()}},html:{regex:/^\s*<[\w!][^<]*>/,process:function(b){return a(b)}},ajax:{regex:/./,process:function(b){var c=a.Deferred(),d=a("
").load(b,function(a,b){"error"!==b&&c.resolve(d.contents()),c.fail()});return c.promise()}},iframe:{process:function(b){var c=new a.Deferred,d=a(" + {% if contest_problem and contest_problem.contest.use_clarifications %} +
+

{{ _('Clarifications') }}

+ {% if has_clarifications %} + {% for clarification in clarifications %} +
+
{{ relative_time(clarification.date) }}
+ + {{ clarification.description|markdown|reference }} + +
+ {% endfor %} + {% else %} +

{{ _('No clarifications have been made at this time.') }}

+ {% endif %} +
+ {% else %} +
+ {% include "comments/list.html" %} +
+ {% endif %} + {% endblock %} {% block bodyend %} - {{ super() }} - {% include "comments/math.html" %} + {{ super() }} + {% include "comments/math.html" %} {% endblock %} diff --git a/templates/problem/raw.html b/templates/problem/raw.html index a620d22..e227009 100644 --- a/templates/problem/raw.html +++ b/templates/problem/raw.html @@ -1,107 +1,107 @@ - + - + - + - -

{{ problem_name }}

-
-
-
+ +

{{ problem_name }}

+
+
+
{{ _('Input:') }} {{ fileio_input or _('stdin') }} -
-
+
+
{{ _('Output:') }} {{ fileio_output or _('stdout') }} -
-
+
+
{{ _('Time Limit:') }} {{ problem.time_limit }}s {% for name, limit in problem.language_time_limit %} -
- {{ name }} - {{ limit }}s -
+
+ {{ name }} + {{ limit }}s +
{% endfor %} -
-
+
+
{{ _('Memory Limit:') }} {{ problem.memory_limit|kbsimpleformat}} {% for name, limit in problem.language_memory_limit %} -
- {{ name }} - {{ limit|kbsimpleformat }} -
+
+ {{ name }} + {{ limit|kbsimpleformat }} +
{% endfor %} +
+
+
+
+ {{ description|markdown|reference|absolutify(url)|str|safe }}
-
-
-
- {{ description|markdown|reference|absolutify(url)|str|safe }} -
- - - - + + + - + }); + + diff --git a/templates/problem/recent-attempt.html b/templates/problem/recent-attempt.html index d94d17c..86aa7d3 100644 --- a/templates/problem/recent-attempt.html +++ b/templates/problem/recent-attempt.html @@ -1,22 +1,22 @@ -{% if last_attempted_problems%} - +{% if last_attempted_problems%} + {% endif %} \ No newline at end of file diff --git a/templates/problem/search-form.html b/templates/problem/search-form.html index d0116fe..073d3c7 100644 --- a/templates/problem/search-form.html +++ b/templates/problem/search-form.html @@ -1,106 +1,106 @@ \ No newline at end of file diff --git a/templates/problem/submission-diff.html b/templates/problem/submission-diff.html index 55dbd35..f541762 100644 --- a/templates/problem/submission-diff.html +++ b/templates/problem/submission-diff.html @@ -1,100 +1,100 @@ {% extends "common-content.html" %} {% block js_media %} - - - + + + {% endblock %} {% block media %} - + .sub-case-status { + text-decoration: underline; + } + {% endblock %} {% block body %} - - - - - - - - - {% for case in range(num_cases) %} - - {% endfor %} - - - - {% for sub in submissions %} - - - - - - - {% for case in sub.test_cases.all() %} - - {% endfor %} - +
IDUsernameResultLanguageDate{{ loop.index }}
{{ sub.id }}{{ link_user(sub.user) }}{{ sub.result }}{{ sub.language.name }}{{ relative_time(sub.date) }} - {% if case.status == 'SC' %} - --- - {% else %} - - {% if case.status == 'AC' %} - {{ case.time|floatformat(2) }} - {% else %} - {{ case.status }} - {% endif %} - - {% endif %} -
+ + + + + + + + {% for case in range(num_cases) %} + {% endfor %} - -
IDUsernameResultLanguageDate{{ loop.index }}
- + + + + {% for sub in submissions %} + + {{ sub.id }} + {{ link_user(sub.user) }} + {{ sub.result }} + {{ sub.language.name }} + {{ relative_time(sub.date) }} + {% for case in sub.test_cases.all() %} + + {% if case.status == 'SC' %} + --- + {% else %} + + {% if case.status == 'AC' %} + {{ case.time|floatformat(2) }} + {% else %} + {{ case.status }} + {% endif %} + + {% endif %} + + {% endfor %} + + {% endfor %} + + + {% endblock %} \ No newline at end of file diff --git a/templates/problem/submit.html b/templates/problem/submit.html index 4d7821e..282ee5c 100644 --- a/templates/problem/submit.html +++ b/templates/problem/submit.html @@ -1,236 +1,236 @@ {% extends "base.html" %} {% block js_media %} - - - {{ form.media.js }} - {% compress js %} - + + {{ form.media.js }} + {% compress js %} + - {% endcompress %} + RefPresenter.__super__.bind.call(this, container, $container); + }; + return RefPresenter; + }); + + var customAdapter = $.fn.select2.amd.require('select2/data/customAdapter'); + + $("#id_language").select2({ + templateResult: format, + templateSelection: formatSelection, + resultsAdapter: customAdapter + }); + + $('#id_language').on('select2:open', function (evt) { + var dropdown = $('.select2-dropdown'); + if (!$('#result-version-info').length) + dropdown.append($("")); + dropdown.attr('id', 'language-select2'); + }); + + $('#id_judge').on('select2:open', function (evt) { + var dropdown = $('.select2-dropdown'); + $('#result-version-info').remove(); + dropdown.attr('id', 'judge-select2'); + }); + + $('#id_language').change(function () { + var lang = $("#id_language").find("option:selected").attr('data-ace'); + window.ace_source.getSession().setMode("ace/mode/" + lang); + update_language_template(); + }); + + $('#ace_source').on('ace_load', function (e, editor) { + update_language_template(); + editor.commands.addCommand({ + name: 'save', + bindKey: {win: 'Ctrl-S', mac: 'Command-S'}, + exec: function () { + localStorage.setItem('submit:' + $('#id_language').val(), editor.getSession().getValue()); + } + }); + editor.getSession().setUseWrapMode(true); + editor.setFontSize(14); + editor.setShowPrintMargin(false); + // editor.setPrintMarginColumn(100); + editor.focus(); + }); + + // $(window).resize(function () { + // $('#ace_source').height(Math.max($(window).height() - 353, 100)); + // }).resize(); + + $('#problem_submit').submit(function (event) { + if ($('#id_source').val().length > 65536) { + alert("{{ _('Your source code must contain at most 65536 characters.') }}"); + event.preventDefault(); + $('#problem_submit').find(':submit').attr('disabled', false); + } + }); + + function get_source_default(file) { + const reader = new FileReader(); + reader.readAsText(file); + reader.onload = function (evt) { + var src = evt.target.result; + update_submit_area(src); + } + reader.onerror = function (evt) { + alert("Fail to upload file"); + } + } + + function get_source_scratch(file) { + JSZip.loadAsync(file).then(function($content) { + return $content.files["project.json"].async('text'); + }).then(function (src) { + update_submit_area(src); + }); + } + + $('#file-upload').on('click change', function(e) { + var file = $(this)[0].files[0]; + if (file) { + if (file.name.endsWith('sb3')) { + get_source_scratch(file); + } + else { + get_source_default(file); + } + } + }) + }); + + {% endcompress %} {% endblock %} {% block media %} - {{ form.media.css }} + {{ form.media.css }} {% endblock %} {% block body %} -
- {% if not no_judges %} - {% if default_lang not in form.fields.language.queryset %} -
- x - {% trans trimmed default_language=default_lang.name %} - Warning! Your default language, {{ default_language }}, - is unavailable for this problem and has been deselected. - {% endtrans %} -
- {% endif %} - - {% if request.in_contest and submission_limit %} - {% if submissions_left > 0 %} -
- x - {% trans left=submissions_left %} - You have {{ left }} submission left - {% pluralize %} - You have {{ left }} submissions left - {% endtrans %} -
- {% else %} -
- x - {{ _('You have 0 submissions left') }} -
- {% endif %} - {% endif %} +
+ {% if not no_judges %} + {% if default_lang not in form.fields.language.queryset %} +
+ x + {% trans trimmed default_language=default_lang.name %} + Warning! Your default language, {{ default_language }}, + is unavailable for this problem and has been deselected. + {% endtrans %} +
{% endif %} -
- {% csrf_token %} - {{ form.non_field_errors() }} -
-
- {{ form.source.errors }} - {{ form.source }} -
- {% if not no_judges %} -
- {{ form.language.errors }} -
- -
-
- {% endif %} + {% if request.in_contest and submission_limit %} + {% if submissions_left > 0 %} +
+ x + {% trans left=submissions_left %} + You have {{ left }} submission left + {% pluralize %} + You have {{ left }} submissions left + {% endtrans %}
+ {% else %} +
+ x + {{ _('You have 0 submissions left') }} +
+ {% endif %} + {% endif %} + {% endif %} -
+ + {% csrf_token %} + {{ form.non_field_errors() }} +
+
+ {{ form.source.errors }} + {{ form.source }} +
+ {% if not no_judges %} +
+ {{ form.language.errors }} +
+ +
+
+ {% endif %} +
- {% if no_judges %} - {{ _('No judge is available for this problem.') }} - {% else %} - -
- {{ form.judge }} - -
- {% endif %} - +
+ + {% if no_judges %} + {{ _('No judge is available for this problem.') }} + {% else %} + +
+ {{ form.judge }} + +
+ {% endif %} + {% endblock %} \ No newline at end of file diff --git a/templates/problem/yaml.html b/templates/problem/yaml.html index 8dee21c..1a7b821 100644 --- a/templates/problem/yaml.html +++ b/templates/problem/yaml.html @@ -1,20 +1,20 @@ {% extends "base.html" %} {% block body %} -
- - - - - -
-
- {% for line in raw_source.split('\n') %} - -
{{ loop.index }}
-
- {% endfor %} -
-
{{ highlighted_source }}
-
+
+ + + + + +
+
+ {% for line in raw_source.split('\n') %} + +
{{ loop.index }}
+
+ {% endfor %} +
+
{{ highlighted_source }}
+
{% endblock %} \ No newline at end of file diff --git a/templates/recent-organization.html b/templates/recent-organization.html index 2af87b7..41e9e57 100644 --- a/templates/recent-organization.html +++ b/templates/recent-organization.html @@ -1,28 +1,28 @@ {% block two_col_media %} - + {% endblock %} {% if recent_organizations %} -
+
+ + {% endblock %} {% block bodyend %} - {{ super() }} - {% if REQUIRE_JAX %} - {% include "mathjax-load.html" %} - {% endif %} - {% include "comments/math.html" %} + {{ super() }} + {% if REQUIRE_JAX %} + {% include "mathjax-load.html" %} + {% endif %} + {% include "comments/math.html" %} {% endblock %} diff --git a/templates/time-remaining-fragment.html b/templates/time-remaining-fragment.html index 19cc7f5..ffee0cf 100644 --- a/templates/time-remaining-fragment.html +++ b/templates/time-remaining-fragment.html @@ -1,3 +1,3 @@ - {{ initial|timedelta("localized") }} + {{ initial|timedelta("localized") }} \ No newline at end of file diff --git a/templates/timezone/media-css.html b/templates/timezone/media-css.html index d17de5f..cb29687 100644 --- a/templates/timezone/media-css.html +++ b/templates/timezone/media-css.html @@ -1,27 +1,27 @@ {% compress css %} - {% if TIMEZONE_BG %} - - {% endif %} - + {% if TIMEZONE_BG %} + {% endif %} + + {% endcompress %} \ No newline at end of file diff --git a/templates/timezone/media-js.html b/templates/timezone/media-js.html index 9d11c75..e8f6cf0 100644 --- a/templates/timezone/media-js.html +++ b/templates/timezone/media-js.html @@ -1,45 +1,45 @@ {% compress js %} - - - + + + }); + }); + + if ('geolocation' in navigator) { + $(function () { + $(', detect').insertAfter('#open-map'); + $('#detect-tz').click(function (event) { + var button = $(this).html('detecting...'); + navigator.geolocation.getCurrentPosition(function (position) { + $.ajax({ + url: 'https://maps.googleapis.com/maps/api/timezone/json', + data: { + location: position.coords.latitude + ',' + position.coords.longitude, + timestamp: Math.floor(Date.now() / 1000) + } + }).done(function (data) { + if (data['timeZoneId']) + $('#id_timezone').val(data.timeZoneId).change(); + else + alert('Invalid response from Google: ' + data); + }).fail(function (jqXHR, status) { + alert(status); + }).always(function () { + button.html('detect'); + }); + }, function () { + button.html('detect'); + alert('Unable to detect'); + }); + event.preventDefault(); + }); + }); + } + {% endcompress %} \ No newline at end of file diff --git a/templates/top-users.html b/templates/top-users.html index e7abb32..f1cdcc9 100644 --- a/templates/top-users.html +++ b/templates/top-users.html @@ -1,37 +1,37 @@ -{% if top_rated %} - -{% endif %} - -{% if top_scorer %} - +{% if top_rated %} + +{% endif %} + +{% if top_scorer %} + {% endif %} \ No newline at end of file diff --git a/templates/two-column-content.html b/templates/two-column-content.html index 053dfd3..cebf64b 100644 --- a/templates/two-column-content.html +++ b/templates/two-column-content.html @@ -1,17 +1,17 @@ -{% extends "three-column-content.html" %} - -{% block three_col_js %} - {% block two_col_js %}{% endblock %} -{% endblock %} - -{% block three_col_media %} - - {% block two_col_media %}{% endblock %} -{% endblock %} +{% extends "three-column-content.html" %} + +{% block three_col_js %} + {% block two_col_js %}{% endblock %} +{% endblock %} + +{% block three_col_media %} + + {% block two_col_media %}{% endblock %} +{% endblock %} diff --git a/templates/user/base-users-js.html b/templates/user/base-users-js.html index 8a5a96c..d162f24 100644 --- a/templates/user/base-users-js.html +++ b/templates/user/base-users-js.html @@ -1,60 +1,60 @@ - \ No newline at end of file diff --git a/templates/user/base-users-table.html b/templates/user/base-users-table.html index 9d7baba..26ee830 100644 --- a/templates/user/base-users-table.html +++ b/templates/user/base-users-table.html @@ -1,32 +1,32 @@ - + {{ rank_header or _("Rank") }} {% block after_rank_head %}{% endblock %} {{ _('Username') }}{% block username_head_extra %}{% endblock %} {% block before_point_head %}{% endblock %} - {% if sort_links %}{% endif %} - {{ _('Points') }} - {%- if sort_links %}{{ sort_order.performance_points }}{% endif %} + {% if sort_links %}{% endif %} + {{ _('Points') }} + {%- if sort_links %}{{ sort_order.performance_points }}{% endif %} {% block after_point_head %}{% endblock %} - + -{% for rank, user in users %} + {% for rank, user in users %} - {{ rank }} - {% block after_rank scoped %}{% endblock %} -
{{ link_user(user) }}{% block user_footer scoped %}{% endblock %}
{% block user_data scoped %}{% endblock %} - {% block before_point scoped %}{% endblock %} - {% block point scoped %} - - {{ user.performance_points|floatformat(0) }} - - {% endblock %} - {% block after_point scoped %}{% endblock %} + {{ rank }} + {% block after_rank scoped %}{% endblock %} +
{{ link_user(user) }}{% block user_footer scoped %}{% endblock %}
{% block user_data scoped %}{% endblock %} + {% block before_point scoped %}{% endblock %} + {% block point scoped %} + + {{ user.performance_points|floatformat(0) }} + + {% endblock %} + {% block after_point scoped %}{% endblock %} -{% endfor %} + {% endfor %} diff --git a/templates/user/base-users-two-col.html b/templates/user/base-users-two-col.html index 37e17be..9cad8db 100644 --- a/templates/user/base-users-two-col.html +++ b/templates/user/base-users-two-col.html @@ -1,36 +1,36 @@ -{% extends "two-column-content.html" %} - -{% block two_col_js %} - {% block users_js_media %}{% endblock %} - {% include "user/base-users-js.html" %} -{% endblock %} - -{% block two_col_media %} - - {% block users_media %}{% endblock %} -{% endblock %} - -{% block middle_content %} - {% if page_obj and page_obj.num_pages > 1 %} -
- {% include "list-pages.html" %} -
- -
-
-
- {% endif %} - - {% block before_table %}{% endblock %} - -
- - {% block users_table %}{% endblock %} -
-
- - {% if page_obj and page_obj.num_pages > 1 %} -
{% include "list-pages.html" %}
- {% endif %} -{% endblock %} +{% extends "two-column-content.html" %} + +{% block two_col_js %} + {% block users_js_media %}{% endblock %} + {% include "user/base-users-js.html" %} +{% endblock %} + +{% block two_col_media %} + + {% block users_media %}{% endblock %} +{% endblock %} + +{% block middle_content %} + {% if page_obj and page_obj.num_pages > 1 %} +
+ {% include "list-pages.html" %} +
+ +
+
+
+ {% endif %} + + {% block before_table %}{% endblock %} + +
+ + {% block users_table %}{% endblock %} +
+
+ + {% if page_obj and page_obj.num_pages > 1 %} +
{% include "list-pages.html" %}
+ {% endif %} +{% endblock %} diff --git a/templates/user/edit-profile.html b/templates/user/edit-profile.html index 47539c4..05f3b2c 100644 --- a/templates/user/edit-profile.html +++ b/templates/user/edit-profile.html @@ -1,175 +1,175 @@ {% extends "user/user-base.html" %} {% block user_media %} - {% include "timezone/media-css.html" %} - {{ form.media.css }} - + #center-float { + position: relative; + margin: 0 auto auto -28.5em; + left: 50%; + width: 700px; + } + {% endblock %} {% block js_media %} - {% include "timezone/media-js.html" %} - {{ form.media.js }} - - + {% include "timezone/media-js.html" %} + {{ form.media.js }} + + {% endblock %} {% block title_ruler %}{% endblock %} {% block title_row %} - {% set tab = 'edit' %} - {% set user = request.profile %} - {% include "user/user-tabs.html" %} + {% set tab = 'edit' %} + {% set user = request.profile %} + {% include "user/user-tabs.html" %} {% endblock %} {% block body %} -
-
- {% if form.errors %} -
- x - {{ form.non_field_errors() }} -
- {% endif %} +
+ + {% if form.errors %} +
+ x + {{ form.non_field_errors() }} +
+ {% endif %} - {% csrf_token %} + {% csrf_token %} - +
+ + + + + + + + +
{{ _('Fullname') }}: {{ form_user.first_name }}
{{ _('School') }}: {{ form_user.last_name }}
+
+ +
{{ _('Self-description') }}:
+ {{ form.about }} +
+ + + + + + +
+ + + + + + + + + + + + + + {% if has_math_config %} - - - - - - - -
{{ form.timezone }}
{{ form.language }}
{{ form.ace_theme }}
{{ _('Fullname') }}: {{ form_user.first_name }}
{{ _('School') }}: {{ form_user.last_name }}
-
- -
{{ _('Self-description') }}:
- {{ form.about }} -
- - - - - + + + {% endif %} + + + + + +
- - - - - - - - - - - - - - {% if has_math_config %} - - - - - {% endif %} - - - - - - -
{{ form.timezone }}
{{ form.language }}
{{ form.ace_theme }}
{{ form.math_engine }}
- {{ _('Change your avatar') }} -
- - {{ _('Change your password') }} - -
-
-
- {{ _('Affiliated organizations') }}: -
- {{ form.organizations }} -
{{ form.math_engine }}
+ {{ _('Change your avatar') }} +
+ + {{ _('Change your password') }} + +
- -
- {% if profile.is_totp_enabled %} - {{ _('Two Factor Authentication is enabled.') }} - {% if require_staff_2fa and request.user.is_staff %} - Disable - {% else %} - Disable - {% endif %} - {% else %} - {{ _('Two Factor Authentication is disabled.') }} - Enable - {% endif %} +
+
+ {{ _('Affiliated organizations') }}:
-

-
{{ _('User-script') }}:
- {{ form.user_script }} -
+ {{ form.organizations }} +
- - -
+
+ {% if profile.is_totp_enabled %} + {{ _('Two Factor Authentication is enabled.') }} + {% if require_staff_2fa and request.user.is_staff %} + Disable + {% else %} + Disable + {% endif %} + {% else %} + {{ _('Two Factor Authentication is disabled.') }} + Enable + {% endif %} +
+

+
{{ _('User-script') }}:
+ {{ form.user_script }} +
+ + + +
{% endblock %} diff --git a/templates/user/import/index.html b/templates/user/import/index.html index 5f71b8a..ca146df 100644 --- a/templates/user/import/index.html +++ b/templates/user/import/index.html @@ -1,111 +1,111 @@ {% extends "user/user-base.html" %} {% block js_media %} - + {% endblock %} {% block body %} -{% csrf_token %} -
+ {% csrf_token %} +
{{_('Sample')}}
- - + +
-
-
-
-

+
+
+
+

{% endblock %} diff --git a/templates/user/import/table_csv.html b/templates/user/import/table_csv.html index 5ab87ef..958974e 100644 --- a/templates/user/import/table_csv.html +++ b/templates/user/import/table_csv.html @@ -1,24 +1,24 @@ - - {{_('ID')}} - {{_('Username')}} - {{_('Password')}} - {{_('Name')}} - {{_('School')}} - {{_('Email')}} - {{_('Organizations')}} - + + {{_('ID')}} + {{_('Username')}} + {{_('Password')}} + {{_('Name')}} + {{_('School')}} + {{_('Email')}} + {{_('Organizations')}} + - {% for i in data %} + {% for i in data %} - {{loop.index}} - {{i.username}} - {{i.password}} - {{i.name}} - {{i.school}} - {{i.email}} - {{i.organizations}} + {{loop.index}} + {{i.username}} + {{i.password}} + {{i.name}} + {{i.school}} + {{i.email}} + {{i.organizations}} - {% endfor %} + {% endfor %} diff --git a/templates/user/link-list.html b/templates/user/link-list.html index 0b6f9e4..e2a7e74 100644 --- a/templates/user/link-list.html +++ b/templates/user/link-list.html @@ -1,3 +1,3 @@ {% for user in users -%} - {{ link_user(user) }}{% if not loop.last %}, {% endif %} + {{ link_user(user) }}{% if not loop.last %}, {% endif %} {%- endfor %} diff --git a/templates/user/list.html b/templates/user/list.html index 27352fa..2fb1622 100644 --- a/templates/user/list.html +++ b/templates/user/list.html @@ -1,21 +1,21 @@ {% extends "user/base-users-two-col.html" %} {% block users_media %} - + {% endblock %} {% block title_ruler %}{% endblock %} @@ -23,9 +23,9 @@ {% block title_row %}{% endblock %} {% block left_sidebar %} - {% include "user/user-left-sidebar.html" %} + {% include "user/user-left-sidebar.html" %} {% endblock %} {% block users_table %} - {% include "user/users-table.html" %} + {% include "user/users-table.html" %} {% endblock %} \ No newline at end of file diff --git a/templates/user/pp-row.html b/templates/user/pp-row.html index 9730f5a..9e0758d 100644 --- a/templates/user/pp-row.html +++ b/templates/user/pp-row.html @@ -1,32 +1,32 @@
-
{{ breakdown.sub_points|floatformat(0) }} / {{ breakdown.sub_total|floatformat(0) }}
-
- {{ breakdown.sub_short_status }} - | - {{ breakdown.sub_lang }} -
+
{{ breakdown.sub_points|floatformat(0) }} / {{ breakdown.sub_total|floatformat(0) }}
+
+ {{ breakdown.sub_short_status }} + | + {{ breakdown.sub_lang }} +
- -
{{ relative_time(breakdown.sub_date) }}
+ +
{{ relative_time(breakdown.sub_date) }}
- -
- {% trans weight=breakdown.weight|floatformat(0) %} - weighted {{ weight }}% - {% endtrans %} + +
+ {% trans weight=breakdown.weight|floatformat(0) %} + weighted {{ weight }}% + {% endtrans %} - {% if breakdown.scaled_points < 10 %} - ({{ _('%(pp).1fpp', pp=breakdown.scaled_points) }}) - {% else %} - ({{ _('%(pp).0fpp', pp=breakdown.scaled_points) }}) - {% endif %} -
+ {% if breakdown.scaled_points < 10 %} + ({{ _('%(pp).1fpp', pp=breakdown.scaled_points) }}) + {% else %} + ({{ _('%(pp).0fpp', pp=breakdown.scaled_points) }}) + {% endif %} +
diff --git a/templates/user/pp-table-body.html b/templates/user/pp-table-body.html index 3f47e56..b9a416a 100644 --- a/templates/user/pp-table-body.html +++ b/templates/user/pp-table-body.html @@ -1,3 +1,3 @@ {% for breakdown in pp_breakdown %} -
{% include "user/pp-row.html" %}
+
{% include "user/pp-row.html" %}
{% endfor %} \ No newline at end of file diff --git a/templates/user/preview.html b/templates/user/preview.html index 82e7cb1..1c5028c 100644 --- a/templates/user/preview.html +++ b/templates/user/preview.html @@ -1,4 +1,4 @@ {{ preview_data|markdown|reference|str|safe }} {% if REQUIRE_JAX %} -
+
{% endif %} \ No newline at end of file diff --git a/templates/user/rating.html b/templates/user/rating.html index 8394b26..016833a 100644 --- a/templates/user/rating.html +++ b/templates/user/rating.html @@ -1,17 +1,17 @@ {% spaceless %} - - {% if rating_class(rating) == 'rate-target' %} - - - - - {% else %} - - - - - {% endif %} - {{ rating.rating|default(rating) }} - + + {% if rating_class(rating) == 'rate-target' %} + + + + + {% else %} + + + + + {% endif %} + {{ rating.rating|default(rating) }} + {% endspaceless %} \ No newline at end of file diff --git a/templates/user/user-about.html b/templates/user/user-about.html index a0ba824..834f6cd 100644 --- a/templates/user/user-about.html +++ b/templates/user/user-about.html @@ -3,524 +3,524 @@ {% block title_ruler %}{% endblock %} {% block title_row %} - {% set tab = 'about' %} - {% include "user/user-tabs.html" %} + {% set tab = 'about' %} + {% include "user/user-tabs.html" %} {% endblock %} {% block user_content %} -
-