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

Allow python tutor for every judge #5050

Merged
merged 11 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions app/assets/javascripts/file_viewer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { showInfoModal } from "./modal";
import { fetch } from "utilities";
import { html } from "lit";

Check warning on line 3 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L1-L3

Added lines #L1 - L3 were not covered by tests

function showInlineFile(name: string, content: string): void {
showInfoModal(name, html`<div class='code'>${content}</div>`);

Check warning on line 6 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L5-L6

Added lines #L5 - L6 were not covered by tests
}

function showRealFile(name: string, activityPath: string, filePath: string): void {
const path = activityPath + "/" + filePath;
const random = Math.floor(Math.random() * 10000 + 1);
showInfoModal(

Check warning on line 12 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L9-L12

Added lines #L9 - L12 were not covered by tests
html`${name} <a href='${path}' title='Download' download><i class='mdi mdi-download'></i></a>`,
html`<div class='code' id='file-${random}'>Loading...</div>`
);

fetch(path, {

Check warning on line 17 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L17

Added line #L17 was not covered by tests
method: "GET"
}).then(response => {

Check warning on line 19 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L19

Added line #L19 was not covered by tests
if (response.ok) {
response.text().then(data => {
let lines = data.split("\n");
const maxLines = 99;

Check warning on line 23 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L21-L23

Added lines #L21 - L23 were not covered by tests
if (lines.length > maxLines) {
lines = lines.slice(0, maxLines);
lines.push("...");

Check warning on line 26 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L25-L26

Added lines #L25 - L26 were not covered by tests
}

const table = document.createElement("table");
table.className = "external-file";
for (let i = 0; i < lines.length; i++) {
const tr = document.createElement("tr");

Check warning on line 32 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L29-L32

Added lines #L29 - L32 were not covered by tests

const number = document.createElement("td");
number.className = "line-nr";

Check warning on line 35 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L34-L35

Added lines #L34 - L35 were not covered by tests
number.textContent = (i === maxLines) ? "" : (i + 1).toString();
tr.appendChild(number);

Check warning on line 37 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L37

Added line #L37 was not covered by tests

const line = document.createElement("td");
line.className = "line";

Check warning on line 40 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L39-L40

Added lines #L39 - L40 were not covered by tests
// textContent is safe, html is not executed
line.textContent = lines[i];
tr.appendChild(line);
table.appendChild(tr);

Check warning on line 44 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L42-L44

Added lines #L42 - L44 were not covered by tests
}
const fileView = document.getElementById(`file-${random}`);
fileView.innerHTML = "";
fileView.appendChild(table);

Check warning on line 48 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L46-L48

Added lines #L46 - L48 were not covered by tests
});
}
});
}
export function initFileViewers(activityPath: string): void {
document.querySelectorAll("a.file-link").forEach(l => l.addEventListener("click", e => {
const link = e.currentTarget as HTMLLinkElement;
const fileName = link.innerText;
const tc = link.closest(".testcase.contains-file") as HTMLDivElement;

Check warning on line 57 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L55-L57

Added lines #L55 - L57 were not covered by tests
if (tc === null) {
return;

Check warning on line 59 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L59

Added line #L59 was not covered by tests
}
const files = JSON.parse(tc.dataset.files);
const file = files[fileName];

Check warning on line 62 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L61-L62

Added lines #L61 - L62 were not covered by tests
if (file.location === "inline") {
showInlineFile(fileName, file.content);

Check warning on line 64 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L64

Added line #L64 was not covered by tests
} else if (file.location === "href") {
showRealFile(fileName, activityPath, file.content);

Check warning on line 66 in app/assets/javascripts/file_viewer.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/file_viewer.ts#L66

Added line #L66 was not covered by tests
}
}));
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import fscreen from "fscreen";
import { showInfoModal } from "./modal";
import { fetch } from "utilities";
import { showInfoModal } from "modal";

Check warning on line 3 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L3

Added line #L3 was not covered by tests
import { html } from "lit";

function initPythiaSubmissionShow(submissionCode: string, activityPath: string): void {
export function initTutor(submissionCode: string): void {
function init(): void {
initTutorLinks();
initFileViewers(activityPath);
if (document.querySelectorAll(".tutormodal").length == 1) {
initFullScreen();
} else {
Expand All @@ -19,21 +18,22 @@

function initTutorLinks(): void {
document.querySelectorAll(".tutorlink").forEach(l => {
const group = l.closest(".group") as HTMLElement;
if (!(group.dataset.statements || group.dataset.stdin)) {
const tutorLink = l as HTMLLinkElement;
if (!(tutorLink.dataset.statements || tutorLink.dataset.stdin)) {
l.remove();
}
});

document.querySelectorAll(".tutorlink").forEach(l => l.addEventListener("click", e => {
const exerciseId = (document.querySelector(".feedback-table") as HTMLElement).dataset.exercise_id;
const group = e.currentTarget.closest(".group");
const stdin = group.dataset.stdin.slice(0, -1);
const statements = group.dataset.statements;
const tutorLink = e.currentTarget as HTMLLinkElement;
const group = tutorLink.closest(".group");
const stdin = tutorLink.dataset.stdin.slice(0, -1);
const statements = tutorLink.dataset.statements;

Check warning on line 32 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L29-L32

Added lines #L29 - L32 were not covered by tests
const files = { inline: {}, href: {} };

group.querySelectorAll(".contains-file").forEach(g => {
const content = JSON.parse(g.dataset.files);
const content = JSON.parse((g as HTMLElement).dataset.files);

Check warning on line 36 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L36

Added line #L36 was not covered by tests

Object.values(content).forEach(value => {
files[value["location"]][value["name"]] = value["content"];
Expand All @@ -51,73 +51,6 @@
}));
}

function initFileViewers(activityPath: string): void {
document.querySelectorAll("a.file-link").forEach(l => l.addEventListener("click", e => {
const link = e.currentTarget as HTMLLinkElement;
const fileName = link.innerText;
const tc = link.closest(".testcase.contains-file") as HTMLDivElement;
if (tc === null) {
return;
}
const files = JSON.parse(tc.dataset.files);
const file = files[fileName];
if (file.location === "inline") {
showInlineFile(fileName, file.content);
} else if (file.location === "href") {
showRealFile(fileName, activityPath, file.content);
}
}));
}

function showInlineFile(name: string, content: string): void {
showInfoModal(name, html`<div class='code'>${content}</div>`);
}

function showRealFile(name: string, activityPath: string, filePath: string): void {
const path = activityPath + "/" + filePath;
const random = Math.floor(Math.random() * 10000 + 1);
showInfoModal(
html`${name} <a href='${path}' title='Download' download><i class='mdi mdi-download'></i></a>`,
html`<div class='code' id='file-${random}'>Loading...</div>`
);

fetch(path, {
method: "GET"
}).then(response => {
if (response.ok) {
response.text().then(data => {
let lines = data.split("\n");
const maxLines = 99;
if (lines.length > maxLines) {
lines = lines.slice(0, maxLines);
lines.push("...");
}

const table = document.createElement("table");
table.className = "external-file";
for (let i = 0; i < lines.length; i++) {
const tr = document.createElement("tr");

const number = document.createElement("td");
number.className = "line-nr";
number.textContent = (i === maxLines) ? "" : (i + 1).toString();
tr.appendChild(number);

const line = document.createElement("td");
line.className = "line";
// textContent is safe, html is not executed
line.textContent = lines[i];
tr.appendChild(line);
table.appendChild(tr);
}
const fileView = document.getElementById(`file-${random}`);
fileView.innerHTML = "";
fileView.appendChild(table);
});
}
});
}

function initFullScreen(): void {
fscreen.addEventListener("fullscreenchange", resizeFullScreen);

Expand Down Expand Up @@ -219,5 +152,3 @@

init();
}

export { initPythiaSubmissionShow };
46 changes: 46 additions & 0 deletions app/helpers/renderers/feedback_table_renderer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class FeedbackTableRenderer
include ActionView::Helpers::JavaScriptHelper
include Rails.application.routes.url_helpers
include ApplicationHelper

Expand Down Expand Up @@ -29,6 +30,7 @@ def initialize(submission, user)

def parse
if @result.present?
tutor_init
@builder.div(class: 'feedback-table', 'data-exercise_id': @exercise.id) do
if @result[:messages].present?
@builder.div(class: 'feedback-table-messages') do
Expand Down Expand Up @@ -186,6 +188,18 @@ def tab_content(t)

def group(g)
@builder.div(class: "group #{g[:accepted] ? 'correct' : 'wrong'}") do
# Add a link to the debugger if there is data
if g[:data] && (g[:data][:statements] || g[:data][:stdin])
@builder.div(class: 'tutor-strip tutorlink',
title: 'Start debugger',
'data-statements': (g[:data][:statements]).to_s,
'data-stdin': (g[:data][:stdin]).to_s) do
@builder.div(class: 'tutor-strip-icon') do
@builder.i('', class: 'mdi mdi-launch mdi-18')
end
end
end

if g[:description]
@builder.div(class: 'row') do
@builder.div(class: 'col-12 description') do
Expand Down Expand Up @@ -395,4 +409,36 @@ def safe(html)
sanitize html
end
end

def tutor_init
# Initialize tutor javascript
@builder.script do
escaped = escape_javascript(@code.strip)
@builder << 'dodona.ready.then(function() {'
@builder << "document.body.append(document.getElementById('tutor'));"
@builder << "dodona.initTutor(\"#{escaped}\");});"
end

# Tutor HTML
@builder.div(id: 'tutor', class: 'tutormodal') do
@builder.div(id: 'info-modal', class: 'modal fade', 'data-backdrop': true, tabindex: -1) do
@builder.div(class: 'modal-dialog modal-xl modal-fullscreen-lg-down tutor') do
@builder.div(class: 'modal-content') do
@builder.div(class: 'modal-header') do
@builder.h4(class: 'modal-title') {}
@builder.div(class: 'icons') do
@builder.button(id: 'fullscreen-button', type: 'button', class: 'btn btn-icon') do
@builder.i('', class: 'mdi mdi-fullscreen')
end
@builder.button(type: 'button', class: 'btn btn-icon', 'data-bs-dismiss': 'modal') do
@builder.i('', class: 'mdi mdi-close')
end
end
end
@builder.div(class: 'modal-body') {}
end
end
end
end
end
end
58 changes: 4 additions & 54 deletions app/helpers/renderers/pythia_renderer.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
class PythiaRenderer < FeedbackTableRenderer
include ActionView::Helpers::JavaScriptHelper

def parse
tutor_init
file_viewer_init
super
end

Expand Down Expand Up @@ -59,29 +57,6 @@ def output_message(m)
end
end

def group(g)
if g.key?(:data)
@builder.div(class: "group #{g[:accepted] ? 'correct' : 'wrong'}",
'data-statements': (g[:data][:statements]).to_s,
'data-stdin': (g[:data][:stdin]).to_s) do
@builder.div(class: 'tutor-strip tutorlink', title: 'Start debugger') do
@builder.div(class: 'tutor-strip-icon') do
@builder.i('', class: 'mdi mdi-launch mdi-18')
end
end
if g[:description]
@builder.div(class: 'col-12 description') do
message(g[:description])
end
end
messages(g[:messages])
g[:groups]&.each { |tc| testcase(tc) }
end
else
super(g)
end
end

def testcase(tc)
return super(tc) unless tc[:data] && tc[:data][:files]

Expand All @@ -97,36 +72,11 @@ def testcase(tc)

## custom methods

def tutor_init
# Initialize tutor javascript
def file_viewer_init
# Initialize file viewers
@builder.script do
escaped = escape_javascript(@code.strip)
@builder << 'dodona.ready.then(function() {'
@builder << "document.body.append(document.getElementById('tutor'));"
@builder << "var code = \"#{escaped}\";"
@builder << "dodona.initPythiaSubmissionShow(code, '#{activity_path(nil, @exercise)}');});"
end

# Tutor HTML
@builder.div(id: 'tutor', class: 'tutormodal') do
@builder.div(id: 'info-modal', class: 'modal fade', 'data-backdrop': true, tabindex: -1) do
@builder.div(class: 'modal-dialog modal-xl modal-fullscreen-lg-down tutor') do
@builder.div(class: 'modal-content') do
@builder.div(class: 'modal-header') do
@builder.h4(class: 'modal-title') {}
@builder.div(class: 'icons') do
@builder.button(id: 'fullscreen-button', type: 'button', class: 'btn btn-icon') do
@builder.i('', class: 'mdi mdi-fullscreen')
end
@builder.button(type: 'button', class: 'btn btn-icon', 'data-bs-dismiss': 'modal') do
@builder.i('', class: 'mdi mdi-close')
end
end
end
@builder.div(class: 'modal-body') {}
end
end
end
@builder << "dodona.initFileViewers('#{activity_path(nil, @exercise)}');});"
end
end

Expand Down
6 changes: 0 additions & 6 deletions app/javascript/packs/pythia_submission.js

This file was deleted.

7 changes: 7 additions & 0 deletions app/javascript/packs/submission.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { attachClipboard } from "copy";
import { evaluationState } from "state/Evaluations";
import codeListing from "code_listing";
import { annotationState } from "state/Annotations";
import { initTutor } from "tutor";
import { initFileViewers } from "file_viewer";

window.dodona.initSubmissionShow = initSubmissionShow;
window.dodona.codeListing = codeListing;
Expand All @@ -14,3 +16,8 @@ window.dodona.initSubmissionHistory = initSubmissionHistory;
window.dodona.setEvaluationId = id => evaluationState.id = id;
window.dodona.setAnnotationVisibility = visibility => annotationState.visibility = visibility;
window.dodona.showLastTab = showLastTab;
window.dodona.initTutor = initTutor;
window.dodona.initFileViewers = initFileViewers;

// will automatically bind to window.iFrameResize()
require("iframe-resizer"); // eslint-disable-line no-undef
3 changes: 2 additions & 1 deletion app/runners/result_constructor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,10 @@ def close_testcase(accepted: nil)
@level = :context
end

def close_context(accepted: nil)
def close_context(accepted: nil, data: nil)
check_level(:context, 'context closed')
@context[:accepted] = accepted unless accepted.nil?
@context[:data] = data unless data.nil?
@judgement[:accepted] &&= @context[:accepted]
@hiddentab &&= @context[:accepted]
(@tab[:groups] ||= []) << @context
Expand Down
1 change: 0 additions & 1 deletion app/views/activities/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
window.dodona.initMathJax();
</script>
<script id="MathJax-script" src='https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'></script>
<%= javascript_include_tag 'pythia_submission' if @activity.judge.renderer == PythiaRenderer %>
<% end %>
<% end %>
<%= render 'navbar_links' %>
Expand Down
Loading