Skip to content

Commit

Permalink
Merge pull request #5050 from dodona-edu/feat/all-judges-debug
Browse files Browse the repository at this point in the history
Allow python tutor for every judge
  • Loading branch information
jorg-vr authored Oct 23, 2023
2 parents 927894a + 36188c8 commit 6e36633
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 187 deletions.
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";

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);
});
}
});
}
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;
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);
}
}));
}
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";
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 initPythiaSubmissionShow(submissionCode: string, activityPath: string):

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;
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);

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

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 @@ function initPythiaSubmissionShow(submissionCode: string, activityPath: string):

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

0 comments on commit 6e36633

Please sign in to comment.