Skip to content
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
6 changes: 4 additions & 2 deletions cms/server/admin/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
SubmissionHandler, \
SubmissionCommentHandler, \
SubmissionOfficialStatusHandler, \
SubmissionFileHandler
SubmissionFileHandler, \
SubmissionDiffHandler
from .task import \
AddTaskHandler, \
TaskHandler, \
Expand Down Expand Up @@ -215,12 +216,13 @@
(r"/submission/([0-9]+)(?:/([0-9]+))?/comment", SubmissionCommentHandler),
(r"/submission/([0-9]+)(?:/([0-9]+))?/official", SubmissionOfficialStatusHandler),
(r"/submission_file/([0-9]+)", SubmissionFileHandler),
(r"/submission_diff/([0-9]+)/([0-9]+)", SubmissionDiffHandler),

# User tests

(r"/user_test/([0-9]+)(?:/([0-9]+))?", UserTestHandler),
(r"/user_test_file/([0-9]+)", UserTestFileHandler),

# The following prefixes are handled by WSGI middlewares:
# * /rpc, defined in cms/io/web_service.py
# * /static, defined in cms/io/web_service.py
Expand Down
85 changes: 85 additions & 0 deletions cms/server/admin/handlers/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@

"""

import json
import logging
import difflib

from cms.db import Dataset, File, Submission
from cms.grading.languagemanager import get_language
Expand Down Expand Up @@ -88,6 +90,89 @@ def get(self, file_id):
self.fetch(digest, "text/plain", real_filename)


class SubmissionDiffHandler(BaseHandler):
"""Shows a diff between two submissions.
"""
@require_permission(BaseHandler.AUTHENTICATED)
def get(self, old_id, new_id):
sub_old = Submission.get_from_id(old_id, self.sql_session)
sub_new = Submission.get_from_id(new_id, self.sql_session)

self.set_header("Content-type", "application/json; charset=utf-8")
resp = {
'message': None,
'files': []
}

if sub_old is None or sub_new is None:
missing_id = old_id if sub_old is None else new_id
resp['message'] = f"Submission ID {missing_id} not found."
self.write(json.dumps(resp))
return

if sub_old.task_id == sub_new.task_id:
files_to_compare = sub_old.task.submission_format
old_files = sub_old.files
new_files = sub_new.files
elif len(sub_old.files) == 1 and len(sub_new.files) == 1:
old_file = list(sub_old.files.values())[0]
old_files = {"submission.%l": old_file}
new_file = list(sub_new.files.values())[0]
new_files = {"submission.%l": new_file}
files_to_compare = ["submission.%l"]
else:
resp['message'] = "Cannot compare submissions: they are for " \
"different tasks and have more than 1 file."
self.write(json.dumps(resp))
return

result_files = []
for fname in files_to_compare:
if ".%l" in fname:
if sub_old.language == sub_new.language and sub_old.language is not None:
ext = get_language(sub_old.language).source_extension
else:
ext = ".txt"
real_fname = fname.replace(".%l", ext)
else:
real_fname = fname

def get_file(x, which):
if fname not in x:
return None, f"File not present in {which} submission"
digest = x[fname].digest
file_bin = self.service.file_cacher.get_file_content(digest)
if len(file_bin) > 1000000:
return None, f"{which} file is too big to diff".capitalize()
file_lines = file_bin.decode(errors='replace').splitlines()
if len(file_lines) > 5000:
return None, f"{which} file has too many lines to diff".capitalize()
return file_lines, None

old_content, old_status = get_file(old_files, "old")
if old_status:
result_files.append({"fname": real_fname, "status": old_status})
continue
new_content, new_status = get_file(new_files, "new")
if new_status:
result_files.append({"fname": real_fname, "status": new_status})
continue

if old_content == new_content:
result_files.append({"fname": real_fname, "status": "No changes"})
else:
diff_iter = difflib.unified_diff(old_content, new_content, lineterm='')
# skip the "---" and "+++" lines.
next(diff_iter)
next(diff_iter)
diff = '\n'.join(diff_iter)

result_files.append({"fname": real_fname, "diff": diff})

resp['files'] = result_files
self.write(json.dumps(resp))


class SubmissionCommentHandler(BaseHandler):
"""Called when the admin comments on a submission.

Expand Down
7 changes: 7 additions & 0 deletions cms/server/admin/static/aws_style.css
Original file line number Diff line number Diff line change
Expand Up @@ -742,3 +742,10 @@ a.button-link {
a.button-link:hover {
background-color: #EEF3EA;
}

th.diff-only, td.diff-only {
display: none;
}
table.diff-open th.diff-only, table.diff-open td.diff-only {
display: table-cell;
}
104 changes: 87 additions & 17 deletions cms/server/admin/static/aws_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,25 @@ document.addEventListener('keydown', function(event) {
}
});

CMS.AWSUtils.filename_to_lang = function(file_name) {
// TODO: update if adding a new language to cms
// (need to also update the prism bundle then)
var extension_to_lang = {
'cs': 'csharp',
'cpp': 'cpp',
'c': 'c',
'h': 'c',
'go': 'go',
'hs': 'haskell',
'java': 'java',
'js': 'javascript',
'php': 'php',
'py': 'python',
'rs': 'rust',
}
var file_ext = file_name.split('.').pop();
return extension_to_lang[file_ext] || file_ext;
}

/**
* This is called when we receive file content, or an error message.
Expand All @@ -114,23 +133,7 @@ CMS.AWSUtils.prototype.file_received = function(response, error) {
this.display_subpage(elements);
return;
}
// TODO: update if adding a new language to cms
// (need to also update the prism bundle then)
var extension_to_lang = {
'cs': 'csharp',
'cpp': 'cpp',
'c': 'c',
'h': 'c',
'go': 'go',
'hs': 'haskell',
'java': 'java',
'js': 'javascript',
'php': 'php',
'py': 'python',
'rs': 'rust',
}
var file_ext = file_name.split('.').pop();
var lang_name = extension_to_lang[file_ext] || file_ext;
var lang_name = CMS.AWSUtils.filename_to_lang(file_name);

elements.push($('<h1>').text(file_name));
elements.push($('<a>').text("Download").prop("href", url));
Expand Down Expand Up @@ -903,3 +906,70 @@ CMS.AWSUtils.prototype.render_markdown_preview = function(target) {
},
});
}

/**
* Handlers for diffing submissions.
*/

/**
* Shows/hides the diff radio buttons when opening/closing the diff section.
*/
CMS.AWSUtils.prototype.update_diffchooser = function() {
var el = document.getElementById("diffchooser");
if(el.open) {
$("#submissions_table").addClass("diff-open");
} else {
$("#submissions_table").removeClass("diff-open");
}
}

/**
* Updates the submission ID inputs when clicking diff radio buttons.
*/
CMS.AWSUtils.prototype.update_diff_ids = function(ev) {
var name = ev.target.name;
var sub_id = ev.target.dataset.submission;
if(name == "diff-radio-old") {
$("#diff-old-input").val(sub_id);
} else {
$("#diff-new-input").val(sub_id);
}
}

/**
* Renders a diff that was received from the server.
*/
CMS.AWSUtils.prototype.show_diff = function(response, error) {
if(error !== null) {
this.display_subpage([$('<p>').text('Error: ' + error)]);
return;
}
var elements = [];
if(response.message !== null) {
elements.push($('<p>').text(response.message));
}
for(let x of response.files) {
elements.push($('<h2>').text(x.fname));
if('status' in x) {
elements.push($('<p>').text(x.status));
continue;
}
var lang_name = CMS.AWSUtils.filename_to_lang(x.fname);
var codearea = $('<code>').text(x.diff)
.addClass('language-diff-' + lang_name)
.addClass('diff-highlight');
elements.push($('<pre>').append(codearea));
}
this.display_subpage(elements);
Prism.highlightAllUnder(document.getElementById('subpage_content'));
}

/**
* Called when "Diff" button is clicked, requests the diff from the server.
*/
CMS.AWSUtils.prototype.do_diff = function() {
var old_id = $("#diff-old-input").val();
var new_id = $("#diff-new-input").val();
var show_diff = this.bind_func(this, this.show_diff);
this.ajax_request(this.url("submission_diff", old_id, new_id), null, show_diff);
}
5 changes: 3 additions & 2 deletions cms/server/admin/static/prism.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading