Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add links to problems in results and problem results sidebar #40

Merged
merged 10 commits into from
Feb 15, 2023
23 changes: 23 additions & 0 deletions ext/js/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
*/
let contestResultsRedirects = new Set();

const contestProblemList = {};

function displayStatusNotification(submitID, problemCode, status) {
browser.notifications.create({
type: 'basic',
Expand Down Expand Up @@ -281,6 +283,23 @@
});
}

function saveContestProblemList(contestID, problems) {
contestProblemList[contestID] = problems;
}

async function getProblemList(contestID) {
if (!contestProblemList[contestID]) {
try {
const response = await fetch(`${SATORI_URL_HTTPS}contest/${contestID}/problems`);
if (!response.ok) throw new Error(`HTTP Status ${response.status}`);
contestProblemList[contestID] = parseProblemList($.parseHTML(await response.text()));
} catch (error) {
console.error(error);
}
}
return contestProblemList[contestID] ?? {};
}

retrieveLastContestID();
setUpLastContestRedirect();
setUpSubmitRedirect();
Expand All @@ -307,6 +326,10 @@
});
} else if (request.action === 'injectHighlightJsCss') {
injectHighlightJsCss(sender.tab);
} else if (request.action === 'saveContestProblemList') {
saveContestProblemList(request.contestID, request.problems);
} else if (request.action === 'getContestProblemList') {
return getProblemList(request.contestID);
}
return new Promise(resolve => resolve(null));
});
Expand Down
60 changes: 58 additions & 2 deletions ext/js/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,67 @@
* @returns {string} contest ID
*/
function getContestID(url) {
return PROBLEM_URL_REGEX.exec(url)[1];
return CONTEST_URL_REGEX.exec(url)[1];
}

/**
* Parse given problem URL and return the contest and problem ID.
* @param {string} url URL to parse
* @returns {object} contest and problem ID
*/
function getContestAndProblemID(url) {
const match = PROBLEM_URL_REGEX.exec(url);
return {
contestID: match[1],
problemID: match[2],
}
}

if (typeof module !== 'undefined') {
module.exports = {
getContestID
getContestID,
getContestAndProblemID,
};
}

function parseProblemList(jqueryHandles) {
return Object.fromEntries(jqueryHandles.flatMap(
(el) => [...$(el).find('#content table.results tr:not(:first-of-type)')].map(
(tr) => [
$(tr).find('td:nth-child(1)').text(),
{
title: $(tr).find('td:nth-child(2)').text(),
href: $(tr).find('td:nth-child(2) a').attr('href'),
pdfHref: $(tr).find('td:nth-child(3) a').attr('href'),
submitHref: $(tr).find('td:nth-child(5) a').attr('href'),
}
]
))
);
}

async function insertProblemLinks(isResultList) {
const column = isResultList ? 2 : 3;
const problems = await browser.runtime.sendMessage({
action: 'getContestProblemList',
contestID: getContestID(document.location.href),
});

let submitHref;
for (const el of $(`table.results > tbody > tr:not(:first-of-type) > td:nth-child(${column})`)) {
const code = $(el).text();
const problem = problems[code];
if (!problem) continue;
if (problem.submitHref) submitHref = problem.submitHref;
const statementHref = problem.href || problem.pdfHref;
if (!statementHref) {
$(el).text(`${code} - ${problem.title}`);
continue;
}
const link = $('<a class="stdlink"></a>');
link.attr('href', statementHref);
link.text(`${code} - ${problem.title}`);
$(el).empty().append(link);
}
return submitHref;
}
5 changes: 4 additions & 1 deletion ext/js/config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const SATORI_URL = 'satori.tcs.uj.edu.pl/';
const SATORI_URL_HTTP = 'http://' + SATORI_URL;
const SATORI_URL_HTTPS = 'https://' + SATORI_URL;
const PROBLEM_URL_REGEX =
const CONTEST_URL_REGEX =
/https:\/\/satori\.tcs\.uj\.edu\.pl\/contest\/(\d+)\//;
const PROBLEM_URL_REGEX =
/https:\/\/satori\.tcs\.uj\.edu\.pl\/contest\/(\d+)\/problems\/(\d+)/;

const CHOSEN_LOGO_PRIMARY_KEY = 'chosenLogo_primary';
const CHOSEN_LOGO_SECONDARY_KEY = 'chosenLogo_secondary';
Expand Down Expand Up @@ -42,6 +44,7 @@ if (typeof module !== 'undefined') {
SATORI_URL,
SATORI_URL_HTTP,
SATORI_URL_HTTPS,
CONTEST_URL_REGEX,
PROBLEM_URL_REGEX,

CHOSEN_LOGO_PRIMARY_KEY,
Expand Down
49 changes: 49 additions & 0 deletions ext/js/problem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
(function () {
'use strict';

const { contestID, problemID } = getContestAndProblemID(document.location.href);
const resultsUrl = `${SATORI_URL_HTTPS}contest/${contestID}/results?results_filter_problem=${problemID}`;
$('<a class="button">Results</a>')
.attr('href', resultsUrl)
.appendTo('#content > .buttton_bar')

function parseResultsPage(html) {
for (const x of $.parseHTML(html)) {
const content = $(x).find('#content');
if (content.length > 0) return content;
}
}

async function loadResults() {
const response = await fetch(`${resultsUrl}&results_limit=20`);
if (!response.ok) throw Error(`Results request failed with HTTP status code ${response.status}`);
const content = parseResultsPage(await response.text());
const table = content.find('> table.results');
if (!table) return;
table.find('td:nth-child(2), th:nth-child(2)').remove();

if (table.find('tr:not(:first-child)').length === 0) {
$('<tr><td colspan="3" class="centered">No submissions yet</td></tr>')
.appendTo(table);
}

// Page selector has more than one page - show link to see all submissions
if (content.find('> .wheel > a.wheelitem').length > 0) {
const row = $('<tr><td colspan="3" class="centered"><a class="stdlink">See more submissions</a></td></tr>');
row.find('a').attr('href', resultsUrl);
table.append(row);
}

$(
'<div id="results-sidebar">' +
' <h3>Recent submission results</h3>' +
'</div>'
)
.append(table)
.insertAfter('#content');
}

loadResults().catch((error) => {
console.error('Failed to load recent submissions', error);
});
})();
16 changes: 10 additions & 6 deletions ext/js/problems.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,20 @@
});
}

hideProblemGroups();
connectGroupHideLinks();
const table = $('table.results');
table.each((index, table) => processProblemGroup($(table)));


// "Results" constants
const contestID = getContestID(document.location.href);
const resultsURL = `${SATORI_URL_HTTPS}contest/${contestID}/results`;

browser.runtime.sendMessage({
action: 'saveContestProblemList',
contestID,
problems: parseProblemList([$]),
});

hideProblemGroups();
connectGroupHideLinks();
const table = $('table.results');
table.each((index, table) => processProblemGroup($(table)));

// "Results" button
const submitUrlRegex = /submit\?select=(\d+)/;
Expand Down
13 changes: 13 additions & 0 deletions ext/js/results-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,17 @@
'tr:nth-child(2) > td:first > a').attr('href');
}
});

const contestID = getContestID(document.location.href);

function addCompareSubmitsButton() {
const problemID = new URLSearchParams(document.location.search).get('results_filter_problem');
if (!problemID || !/^\d+$/.test(problemID)) return;
$(`<a class="button">Submit another</a>`)
.attr('href', `${SATORI_URL_HTTPS}contest/${contestID}/submit?select=${problemID}`)
.appendTo('#content .button_bar')
}

addCompareSubmitsButton();
insertProblemLinks(true);
})();
15 changes: 15 additions & 0 deletions ext/js/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
let problemStatus = tr.find('td:last').text();

let url = document.location.href;
const contestID = getContestID(url);

/**
* Parse given HTML and return problem status code if it's found.
Expand Down Expand Up @@ -84,5 +85,19 @@
});
}

const submitUrlRegex = /submit\?select=(\d+)/;

initializeSyntaxHighlighter();
insertProblemLinks(false).then((submitUrl) => {
if (!submitUrl) return;
const submitID = submitUrlRegex.exec(submitUrl)[1];
const resultsButton = $('<a class="button">All submissions</a>')
.attr('href', `${SATORI_URL_HTTPS}contest/${contestID}/results?results_filter_problem=${submitID}`);
const submitButton = $('<a class="button">Submit another</a>')
.attr('href', submitUrl);
$('<div class="button_bar"></div>')
.append(resultsButton)
.append(submitButton)
.insertAfter('#content .results')
});
})();
26 changes: 24 additions & 2 deletions ext/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"background": {
"scripts": [
"vendor/browser-polyfill.js",
"vendor/bower/jquery.min.js",
"js/config.js",
"js/common.js",
"js/background.js"
Expand Down Expand Up @@ -93,7 +94,10 @@
]
},
{
"matches": ["*://satori.tcs.uj.edu.pl/contest/*/problems*"],
"matches": [
"*://satori.tcs.uj.edu.pl/contest/*/problems",
"*://satori.tcs.uj.edu.pl/contest/*/problems?*"
],
"js": [
"vendor/browser-polyfill.js",
"vendor/bower/jquery.min.js",
Expand All @@ -104,10 +108,27 @@
"css": ["css/problems.css"]
},
{
"matches": ["*://satori.tcs.uj.edu.pl/contest/*/results"],
"matches": ["*://satori.tcs.uj.edu.pl/contest/*/problems/*"],
"js": [
"vendor/browser-polyfill.js",
"vendor/bower/jquery.min.js",
"js/common.js",
"js/problem.js"
],
"run_at": "document_end",
"css": [
"css/problem.css"
]
},
{
"matches": [
"*://satori.tcs.uj.edu.pl/contest/*/results",
"*://satori.tcs.uj.edu.pl/contest/*/results?*"
],
"js": [
"vendor/browser-polyfill.js",
"vendor/bower/jquery.min.js",
"js/common.js",
"js/results-list.js"
],
"run_at": "document_end"
Expand All @@ -119,6 +140,7 @@
"vendor/bower/jquery.min.js",
"vendor/bower/highlight.pack.min.js",
"vendor/bower/highlightjs-line-numbers.min.js",
"js/common.js",
"js/results.js"
],
"run_at": "document_end",
Expand Down
33 changes: 33 additions & 0 deletions ext/scss/problem.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#container > .colmask > .colmid > .colleft > .col1 {
display: grid;
grid-template-columns: 1fr 300px;

#content {
grid-column: 1;

.buttton_bar {
margin-top: 1em;
}
}

#results-sidebar {
grid-column: 2;
margin-top: -1em;
}

@media screen and (max-width: 1200px) {
grid-template-columns: 1fr;
grid-template-rows: auto auto;

#content {
grid-row: 1;
}

#results-sidebar {
border-top: 1px dotted #A1A39D;
grid-column: 1;
grid-row: 2;
margin: 0 1em 1em;
}
}
}