WebFund 2024F: Tutorial 7: Difference between revisions
(5 intermediate revisions by the same user not shown) | |||
Line 6: | Line 6: | ||
# Create a file that does not validate and attempt to upload it. What happens? Why? | # Create a file that does not validate and attempt to upload it. What happens? Why? | ||
# Are the validation and error messages the same as in Tutorial 6? | # Are the validation and error messages the same as in Tutorial 6? | ||
# Where is the database schema specified? In that schema, what is the type of Student ID? | |||
# Is it possible to upload an assignment with a non-numeric student ID? How do you know? Is this surprising? | |||
# Change the app so it accepts a field "Section: " at the top of of the assignment just after the Student ID. This section should be added to the output of the validation, the output after a submission is uploaded, and the submission listing. What files did you change? How did you change them? | # Change the app so it accepts a field "Section: " at the top of of the assignment just after the Student ID. This section should be added to the output of the validation, the output after a submission is uploaded, and the submission listing. What files did you change? How did you change them? | ||
Line 15: | Line 17: | ||
<syntaxhighlight lang="javascript" line> | <syntaxhighlight lang="javascript" line> | ||
// SPDX-License-Identifier: GPL-3.0-or-later | |||
// Copyright (C) 2024 Anil Somayaji | |||
// | |||
// submitdemo.js | |||
// for COMP 2406 (Fall 2024), Carleton University | |||
// | |||
// Initial version: November 6, 2024 | |||
// | |||
// run with the following command: | |||
// deno run --allow-net --allow-read --allow-write submitdemo.js | |||
// | |||
import { DB } from "https://deno.land/x/sqlite/mod.ts"; | |||
const status_NOT_FOUND = 404; | |||
const status_OK = 200; | |||
const status_INTERNAL_SERVER_ERROR = 500; | |||
const status_NOT_IMPLEMENTED = 501; | |||
const appTitle = "COMP 2406 Submissions Demo"; | |||
const dbFile = "submissions.db"; | |||
const table = "tutorial7"; | |||
const db = new DB(dbFile); | |||
db.execute(` | |||
CREATE TABLE IF NOT EXISTS ${table} ( | |||
id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
studentID INTEGER, | |||
name TEXT, | |||
q1 TEXT, | |||
q2 TEXT, | |||
q3 TEXT, | |||
q4 TEXT, | |||
q5 TEXT | |||
) | |||
`); | |||
function addSubmissionDB(db, table, r) { | |||
return db.query(`INSERT INTO ${table} ` + | |||
"(studentID, name, q1, q2, q3, q4, q5) " + | |||
"VALUES (?, ?, ?, ?, ?, ?, ?)", | |||
[r.studentID, r.name, | |||
r["1"], r["2"], r["3"], r["4"], r["5"]]); | |||
} | |||
function getAllSubmissionsDB(db, table) { | |||
var state = []; | |||
const query = | |||
db.prepareQuery( | |||
"SELECT id, studentID, name, q1, q2, q3, q4, q5 FROM " + | |||
table + " ORDER BY name ASC LIMIT 50"); | |||
for (const [id, studentID, name, q1, q2, q3, q4, q5] | |||
of query.iter()) { | |||
state.push({id, studentID, name, q1, q2, q3, q4, q5}); | |||
} | |||
query.finalize(); | |||
return state; | |||
} | |||
function analyzeSubmissionsDB(db, table) { | |||
var analysis = {}; | |||
analysis.count = db.query("SELECT COUNT(*) FROM " + table); | |||
analysis.studentIDList = | |||
db.query("SELECT DISTINCT studentID FROM " + table); | |||
return analysis; | |||
} | |||
function MIMEtype(filename) { | |||
const MIME_TYPES = { | |||
'css': 'text/css', | |||
'gif': 'image/gif', | |||
'htm': 'text/html', | |||
'html': 'text/html', | |||
'ico': 'image/x-icon', | |||
'jpeg': 'image/jpeg', | |||
'jpg': 'image/jpeg', | |||
'js': 'text/javascript', | |||
'json': 'application/json', | |||
'pdf': 'application/pdf', | |||
'png': 'image/png', | |||
'txt': 'text/text' | |||
}; | |||
var extension = ""; | |||
if (filename) { | |||
extension = filename.slice(filename.lastIndexOf('.')+1).toLowerCase(); | |||
} | |||
return MIME_TYPES[extension] || "application/octet-stream"; | |||
}; | |||
function template_header(title) { | |||
const fullTitle = appTitle + ": " + title; | |||
return `<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<title>${fullTitle}</title> | |||
<link rel="stylesheet" href="/style.css"> | |||
</head> | |||
` | |||
} | |||
function template_notFound(path) { | |||
return template_header("Page not found") + | |||
`<body> | |||
<h1>Page not found</h1> | |||
<p>Sorry, the requested page was not found.</p> | |||
</body> | |||
</html> | |||
` | |||
} | |||
function template_addRecord(obj) { | |||
return template_header("Submission just added") + | |||
`<body> | |||
<body> | |||
<h1>Submission just added</h1> | |||
<p>Student ID: ${obj.studentID}</p> | |||
<p>Name: ${obj.name}</p> | |||
<p>Q1: ${obj.q1}</p> | |||
<p>Q2: ${obj.q2}</p> | |||
<p>Q3: ${obj.q3}</p> | |||
<p>Q4: ${obj.q4}</p> | |||
<p>Q5: ${obj.q5}</p> | |||
<form method="get" action="/"> | |||
<button type="submit">Home</button> | |||
</form> | |||
</body> | |||
</html> | |||
` | |||
} | |||
function listSubmissions() { | |||
var state = getAllSubmissionsDB(db, table); | |||
var response = { contentType: "application/JSON", | |||
status: status_OK, | |||
contents: JSON.stringify(state), | |||
}; | |||
return response; | |||
} | |||
async function routeGet(req) { | |||
const path = new URL(req.url).pathname; | |||
if (path === "/list") { | |||
return listSubmissions(); | |||
} else if (path === "/analyze") { | |||
return await showAnalysis(); | |||
} else { | |||
return null; | |||
} | |||
} | |||
async function showAnalysis() { | |||
var analysis = analyzeSubmissionsDB(db, table); | |||
var studentIDList = | |||
'<li>' + analysis.studentIDList.join('</li> <li>') + '</li>'; | |||
var analysisBody = ` <body> | |||
<body> | |||
<h1>Submissions analysis</h1> | |||
<p># Records: ${analysis.count}</p> | |||
<p>Student IDs: | |||
<ol> | |||
${studentIDList} | |||
</ol> | |||
</p> | |||
<form method="get" action="/"> | |||
<button type="submit">Home</button> | |||
</form> | |||
</body> | |||
</html>` | |||
var contents = | |||
template_header("Submission analysis") + analysisBody; | |||
var response = { contentType: "text/html", | |||
status: status_OK, | |||
contents: contents, | |||
}; | |||
return response; | |||
} | |||
async function addSubmission(req) { | |||
const submission = await req.json(); | |||
const result = addSubmissionDB(db, table, submission); | |||
var response; | |||
if (result) { | |||
response = { | |||
contentType: "text/plain", | |||
status: status_OK, | |||
contents: "Got the data", | |||
}; | |||
} else { | |||
response = { | |||
contentType: "text/plain", | |||
status: status_INTERNAL_SERVER_ERROR, | |||
contents: "Internal server error" | |||
}; | |||
} | |||
return response; | |||
} | |||
async function routePost(req) { | |||
const path = new URL(req.url).pathname; | |||
if (path === "/uploadSubmission") { | |||
return await addSubmission(req); | |||
} else { | |||
return null; | |||
} | |||
} | |||
async function route(req) { | |||
if (req.method === "GET") { | |||
return await routeGet(req); | |||
} else if (req.method === "POST") { | |||
return await routePost(req); | |||
} else { | |||
return { | |||
contents: "Method not implemented.", | |||
status: status_NOT_IMPLEMENTED, | |||
contentType: "text/plain" | |||
}; | |||
} | |||
} | |||
async function fileData(path) { | |||
var contents, status, contentType; | |||
try { | |||
contents = await Deno.readFile("./static" + path); | |||
status = status_OK; | |||
contentType = MIMEtype(path); | |||
} catch (e) { | |||
contents = template_notFound(path); | |||
status = status_NOT_FOUND; | |||
contentType = "text/html"; | |||
} | |||
return { contents, status, contentType }; | |||
} | |||
async function handler(req) { | |||
var origpath = new URL(req.url).pathname; | |||
var path = origpath; | |||
var r = await route(req); | |||
if (!r) { | |||
if (path === "/") { | |||
path = "/index.html"; | |||
} | |||
r = await fileData(path); | |||
} | |||
console.log(`${r.status} ${req.method} ${r.contentType} ${origpath}`); | |||
return new Response(r.contents, | |||
{status: r.status, | |||
headers: { | |||
"content-type": r.contentType, | |||
}}); | |||
} | |||
const ac = new AbortController(); | |||
const server = Deno.serve( | |||
{ | |||
signal: ac.signal, | |||
port: 8000, | |||
hostname: "0.0.0.0" | |||
}, | |||
handler); | |||
Deno.addSignalListener("SIGINT", () => { | |||
console.log("SIGINT received, terminating..."); | |||
ac.abort(); | |||
}); | |||
server.finished.then(() => { | |||
console.log("Server terminating, closing database.") | |||
db.close(); | |||
}); | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 20: | Line 332: | ||
<syntaxhighlight lang="html" line> | <syntaxhighlight lang="html" line> | ||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<title>COMP 2406 Submissions Demo</title> | |||
<link rel="stylesheet" href="/style.css"> | |||
</head> | |||
<body> | |||
<h1>COMP 2406 Submissions Demo</h1> | |||
<p>Please select what you would like to do: | |||
<ol> | |||
<li><a href="/upload.html">Upload a submission.</a></li> | |||
<li><a href="/list.html">List submissions.</a></li> | |||
<li><a href="/analyze">Analyze submissions.</a></li> | |||
</ol> | |||
</p> | |||
</body> | |||
</html> | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 26: | Line 355: | ||
<syntaxhighlight lang="html" line> | <syntaxhighlight lang="html" line> | ||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<title>COMP 2406 Submissions Demo: Upload</title> | |||
<link rel="stylesheet" href="/style.css"> | |||
<script type="text/javascript" src="/validator.js"></script> | |||
</head> | |||
<body onload="hideAnalysis()"> | |||
<h1>Upload Submission</h1> | |||
<form> | |||
<input type="file" id="assignmentFile" | |||
onchange="loadAssignment(this, false)" /> | |||
<button type="button" onclick="uploadSubmission()">Upload</button> | |||
</form> | |||
<div id="analysis"> | |||
<p><b>Status:</b> <span id="status">UNKNOWN</span></p> | |||
<p>Filename: <span id="filename"></span></p> | |||
<p>Name: <span id="name"></span><br> | |||
Student ID: <span id="studentID"></span> <i>(Please check that your ID number is correct!)</i></p> | |||
<hr> | |||
<div id="questions"><p><b>Questions:</b> <i>(Please check that your answers are numbered correctly!)</i></p></div> | |||
</div> | |||
</body> | |||
</html> | |||
</syntaxhighlight> | </syntaxhighlight> | ||
===static/validator.js=== | ===static/validator.js=== | ||
<syntaxhighlight lang="javascript" line> | <syntaxhighlight lang="javascript" line> | ||
// SPDX-License-Identifier: GPL-3.0-or-later | |||
// Copyright (C) 2024 Anil Somayaji | |||
// | |||
// validator.js, part of submitdemo | |||
// for COMP 2406 (Fall 2024), Carleton University | |||
// | |||
// Initial version: November 6, 2024 | |||
// | |||
const filePrefix = "comp2406-tutorial7"; | |||
const submissionName = "COMP 2406 2024F Tutorial 7" | |||
const expectedQuestionList = "1,2,3,4,5"; | |||
var analysisSection; | |||
function loadAssignment(fileInput, upload) { | |||
analysisSection.hidden = false; | |||
if(fileInput.files[0] == undefined) { | |||
updateTag("status", "ERROR: No files to examine.") | |||
return; | |||
} | |||
var reader = new FileReader(); | |||
reader.onload = function(ev) { | |||
var content = ev.target.result; | |||
var fn = fileInput.files[0].name | |||
var q = checkSubmission(fn, content); | |||
if (upload) { | |||
doUploadSubmission(fn, q); | |||
} | |||
}; | |||
reader.onerror = function(err) { | |||
updateTag("status", "ERROR: Failed to read file"); | |||
} | |||
reader.readAsText(fileInput.files[0]); | |||
} | |||
var numQuestions = expectedQuestionList.split(",").length; | |||
function updateTag(id, val) { | |||
document.getElementById(id).innerHTML = val; | |||
} | |||
function lineEncoding(lines) { | |||
var n, i, s; | |||
for (i = 0; i < lines.length; i++) { | |||
s = lines[i]; | |||
n = s.length; | |||
if (s[n-1] === '\r') { | |||
return "DOS/Windows Text (CR/LF)"; | |||
} | |||
} | |||
return "UNIX"; | |||
} | |||
function checkSubmission(fn, f) { | |||
var lines = f.split('\n') | |||
var c = 0; | |||
var questionList = []; | |||
var questionString; | |||
var q = {}; | |||
var i; | |||
var lastQuestion = null; | |||
const fnPattern = filePrefix + "-[a-zA-Z0-9]+\.txt"; | |||
const fnRexp = new RegExp(fnPattern); | |||
if (!fnRexp.test(fn)) { | |||
updateTag("status", "ERROR " + fn + | |||
" doesn't follow the pattern " + fnRexp); | |||
return; | |||
} | |||
if (fn === filePrefix + "-template.txt") { | |||
updateTag("status", "ERROR " + fn + | |||
" has the default name, please change template to your mycarletonone username"); | |||
return; | |||
} | |||
updateTag("filename", fn); | |||
let encoding = lineEncoding(lines); | |||
if (encoding !== "UNIX") { | |||
updateTag("status", "ERROR " + fn + | |||
" is not a UNIX textfile, it is a " | |||
+ encoding + " file."); | |||
return; | |||
} | |||
if (submissionName !== lines[0]) { | |||
updateTag("status", "ERROR " + fn + | |||
" doesn't start with \"" + submissionName + "\""); | |||
return; | |||
} | |||
try { | |||
q.name = lines[1].match(/^Name:(.+)/m)[1].trim(); | |||
q.studentID = lines[2].match(/^Student ID:(.+)/m)[1].trim(); | |||
} catch (error) { | |||
updateTag("status", "ERROR " + fn + | |||
" has bad Name or Student ID field"); | |||
return; | |||
} | |||
updateTag("name", q.name); | |||
updateTag("studentID", q.studentID); | |||
var questionRE = /^([0-9a-g]+)\.(.*)/; | |||
for (i = 4; i < lines.length; i++) { | |||
if (typeof(lines[i]) === 'string') { | |||
lines[i] = lines[i].replace('\r',''); | |||
} | |||
let m = lines[i].match(questionRE); | |||
if (m) { | |||
c++; | |||
questionList.push(m[1]); | |||
q[m[1]] = m[2]; | |||
lastQuestion = m[1]; | |||
} else { | |||
if (lastQuestion !== null) { | |||
if ((q[lastQuestion] === '') || (q[lastQuestion] === ' ')) { | |||
q[lastQuestion] = lines[i]; | |||
} else { | |||
q[lastQuestion] = q[lastQuestion] + "\n" + lines[i]; | |||
} | |||
} | |||
} | |||
} | |||
console.log(JSON.stringify(q, null, ' ')); | |||
questionString = questionList.toString(); | |||
if (questionString !== expectedQuestionList) { | |||
updateTag("status", "ERROR expected questions " + | |||
expectedQuestionList + " but got questions " + | |||
questionString); | |||
} else { | |||
updateTag("status", "PASSED " + | |||
fn + ": " + q.name + " (" + q.studentID + ")"); | |||
} | |||
var newP, newText, newPre, newPreText; | |||
let questionDiv = document.getElementById("questions"); | |||
for (let qName of questionList) { | |||
let qText = q[qName]; | |||
newP = document.createElement("p"); | |||
newText = document.createTextNode(qName + ":"); | |||
newP.appendChild(newText); | |||
questionDiv.appendChild(newP); | |||
newPre = document.createElement("pre"); | |||
newPreText = document.createTextNode(qText); | |||
newPre.appendChild(newPreText); | |||
newP.appendChild(newPre); | |||
} | |||
return q; | |||
} | |||
function hideAnalysis() { | |||
analysisSection = document.getElementById("analysis"); | |||
analysisSection.hidden = true; | |||
} | |||
function uploadSubmission() { | |||
var assignmentFile = document.getElementById("assignmentFile"); | |||
loadAssignment(assignmentFile, true); | |||
} | |||
function doUploadSubmission(fn, q) { | |||
console.log("sending data to server..."); | |||
if (q) { | |||
const request = new Request("/uploadSubmission", { | |||
method: "POST", | |||
body: JSON.stringify(q), | |||
headers: { | |||
'Content-Type': 'application/json' | |||
} | |||
}); | |||
fetch(request).then((response) => { | |||
if (response.status === 200) { | |||
updateTag("status", "UPLOAD COMPLETE of " + fn); | |||
} else { | |||
updateTag("status", "UPLOAD ERROR of " + fn + | |||
", failed with status " + | |||
response.status); | |||
} | |||
}); | |||
} else { | |||
updateTag("status", "ERROR No valid data to upload!"); | |||
} | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
===static/list.html=== | ===static/list.html=== |
Latest revision as of 22:00, 7 November 2024
In this tutorial you will be playing with Submission Demo, which is an integration of the code from Tutorial 5 and Tutorial 6.
Tasks
- Create a file that validates correctly and upload it. Then, check the listing and analysis pages: is your upload reflected there?
- Create a file that does not validate and attempt to upload it. What happens? Why?
- Are the validation and error messages the same as in Tutorial 6?
- Where is the database schema specified? In that schema, what is the type of Student ID?
- Is it possible to upload an assignment with a non-numeric student ID? How do you know? Is this surprising?
- Change the app so it accepts a field "Section: " at the top of of the assignment just after the Student ID. This section should be added to the output of the validation, the output after a submission is uploaded, and the submission listing. What files did you change? How did you change them?
Code
submitdemo.js
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// submitdemo.js
// for COMP 2406 (Fall 2024), Carleton University
//
// Initial version: November 6, 2024
//
// run with the following command:
// deno run --allow-net --allow-read --allow-write submitdemo.js
//
import { DB } from "https://deno.land/x/sqlite/mod.ts";
const status_NOT_FOUND = 404;
const status_OK = 200;
const status_INTERNAL_SERVER_ERROR = 500;
const status_NOT_IMPLEMENTED = 501;
const appTitle = "COMP 2406 Submissions Demo";
const dbFile = "submissions.db";
const table = "tutorial7";
const db = new DB(dbFile);
db.execute(`
CREATE TABLE IF NOT EXISTS ${table} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
studentID INTEGER,
name TEXT,
q1 TEXT,
q2 TEXT,
q3 TEXT,
q4 TEXT,
q5 TEXT
)
`);
function addSubmissionDB(db, table, r) {
return db.query(`INSERT INTO ${table} ` +
"(studentID, name, q1, q2, q3, q4, q5) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)",
[r.studentID, r.name,
r["1"], r["2"], r["3"], r["4"], r["5"]]);
}
function getAllSubmissionsDB(db, table) {
var state = [];
const query =
db.prepareQuery(
"SELECT id, studentID, name, q1, q2, q3, q4, q5 FROM " +
table + " ORDER BY name ASC LIMIT 50");
for (const [id, studentID, name, q1, q2, q3, q4, q5]
of query.iter()) {
state.push({id, studentID, name, q1, q2, q3, q4, q5});
}
query.finalize();
return state;
}
function analyzeSubmissionsDB(db, table) {
var analysis = {};
analysis.count = db.query("SELECT COUNT(*) FROM " + table);
analysis.studentIDList =
db.query("SELECT DISTINCT studentID FROM " + table);
return analysis;
}
function MIMEtype(filename) {
const MIME_TYPES = {
'css': 'text/css',
'gif': 'image/gif',
'htm': 'text/html',
'html': 'text/html',
'ico': 'image/x-icon',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'js': 'text/javascript',
'json': 'application/json',
'pdf': 'application/pdf',
'png': 'image/png',
'txt': 'text/text'
};
var extension = "";
if (filename) {
extension = filename.slice(filename.lastIndexOf('.')+1).toLowerCase();
}
return MIME_TYPES[extension] || "application/octet-stream";
};
function template_header(title) {
const fullTitle = appTitle + ": " + title;
return `<!DOCTYPE html>
<html>
<head>
<title>${fullTitle}</title>
<link rel="stylesheet" href="/style.css">
</head>
`
}
function template_notFound(path) {
return template_header("Page not found") +
`<body>
<h1>Page not found</h1>
<p>Sorry, the requested page was not found.</p>
</body>
</html>
`
}
function template_addRecord(obj) {
return template_header("Submission just added") +
`<body>
<body>
<h1>Submission just added</h1>
<p>Student ID: ${obj.studentID}</p>
<p>Name: ${obj.name}</p>
<p>Q1: ${obj.q1}</p>
<p>Q2: ${obj.q2}</p>
<p>Q3: ${obj.q3}</p>
<p>Q4: ${obj.q4}</p>
<p>Q5: ${obj.q5}</p>
<form method="get" action="/">
<button type="submit">Home</button>
</form>
</body>
</html>
`
}
function listSubmissions() {
var state = getAllSubmissionsDB(db, table);
var response = { contentType: "application/JSON",
status: status_OK,
contents: JSON.stringify(state),
};
return response;
}
async function routeGet(req) {
const path = new URL(req.url).pathname;
if (path === "/list") {
return listSubmissions();
} else if (path === "/analyze") {
return await showAnalysis();
} else {
return null;
}
}
async function showAnalysis() {
var analysis = analyzeSubmissionsDB(db, table);
var studentIDList =
'<li>' + analysis.studentIDList.join('</li> <li>') + '</li>';
var analysisBody = ` <body>
<body>
<h1>Submissions analysis</h1>
<p># Records: ${analysis.count}</p>
<p>Student IDs:
<ol>
${studentIDList}
</ol>
</p>
<form method="get" action="/">
<button type="submit">Home</button>
</form>
</body>
</html>`
var contents =
template_header("Submission analysis") + analysisBody;
var response = { contentType: "text/html",
status: status_OK,
contents: contents,
};
return response;
}
async function addSubmission(req) {
const submission = await req.json();
const result = addSubmissionDB(db, table, submission);
var response;
if (result) {
response = {
contentType: "text/plain",
status: status_OK,
contents: "Got the data",
};
} else {
response = {
contentType: "text/plain",
status: status_INTERNAL_SERVER_ERROR,
contents: "Internal server error"
};
}
return response;
}
async function routePost(req) {
const path = new URL(req.url).pathname;
if (path === "/uploadSubmission") {
return await addSubmission(req);
} else {
return null;
}
}
async function route(req) {
if (req.method === "GET") {
return await routeGet(req);
} else if (req.method === "POST") {
return await routePost(req);
} else {
return {
contents: "Method not implemented.",
status: status_NOT_IMPLEMENTED,
contentType: "text/plain"
};
}
}
async function fileData(path) {
var contents, status, contentType;
try {
contents = await Deno.readFile("./static" + path);
status = status_OK;
contentType = MIMEtype(path);
} catch (e) {
contents = template_notFound(path);
status = status_NOT_FOUND;
contentType = "text/html";
}
return { contents, status, contentType };
}
async function handler(req) {
var origpath = new URL(req.url).pathname;
var path = origpath;
var r = await route(req);
if (!r) {
if (path === "/") {
path = "/index.html";
}
r = await fileData(path);
}
console.log(`${r.status} ${req.method} ${r.contentType} ${origpath}`);
return new Response(r.contents,
{status: r.status,
headers: {
"content-type": r.contentType,
}});
}
const ac = new AbortController();
const server = Deno.serve(
{
signal: ac.signal,
port: 8000,
hostname: "0.0.0.0"
},
handler);
Deno.addSignalListener("SIGINT", () => {
console.log("SIGINT received, terminating...");
ac.abort();
});
server.finished.then(() => {
console.log("Server terminating, closing database.")
db.close();
});
static/index.html
<!DOCTYPE html>
<html>
<head>
<title>COMP 2406 Submissions Demo</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>COMP 2406 Submissions Demo</h1>
<p>Please select what you would like to do:
<ol>
<li><a href="/upload.html">Upload a submission.</a></li>
<li><a href="/list.html">List submissions.</a></li>
<li><a href="/analyze">Analyze submissions.</a></li>
</ol>
</p>
</body>
</html>
static/upload.html
<!DOCTYPE html>
<html>
<head>
<title>COMP 2406 Submissions Demo: Upload</title>
<link rel="stylesheet" href="/style.css">
<script type="text/javascript" src="/validator.js"></script>
</head>
<body onload="hideAnalysis()">
<h1>Upload Submission</h1>
<form>
<input type="file" id="assignmentFile"
onchange="loadAssignment(this, false)" />
<button type="button" onclick="uploadSubmission()">Upload</button>
</form>
<div id="analysis">
<p><b>Status:</b> <span id="status">UNKNOWN</span></p>
<p>Filename: <span id="filename"></span></p>
<p>Name: <span id="name"></span><br>
Student ID: <span id="studentID"></span> <i>(Please check that your ID number is correct!)</i></p>
<hr>
<div id="questions"><p><b>Questions:</b> <i>(Please check that your answers are numbered correctly!)</i></p></div>
</div>
</body>
</html>
static/validator.js
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// validator.js, part of submitdemo
// for COMP 2406 (Fall 2024), Carleton University
//
// Initial version: November 6, 2024
//
const filePrefix = "comp2406-tutorial7";
const submissionName = "COMP 2406 2024F Tutorial 7"
const expectedQuestionList = "1,2,3,4,5";
var analysisSection;
function loadAssignment(fileInput, upload) {
analysisSection.hidden = false;
if(fileInput.files[0] == undefined) {
updateTag("status", "ERROR: No files to examine.")
return;
}
var reader = new FileReader();
reader.onload = function(ev) {
var content = ev.target.result;
var fn = fileInput.files[0].name
var q = checkSubmission(fn, content);
if (upload) {
doUploadSubmission(fn, q);
}
};
reader.onerror = function(err) {
updateTag("status", "ERROR: Failed to read file");
}
reader.readAsText(fileInput.files[0]);
}
var numQuestions = expectedQuestionList.split(",").length;
function updateTag(id, val) {
document.getElementById(id).innerHTML = val;
}
function lineEncoding(lines) {
var n, i, s;
for (i = 0; i < lines.length; i++) {
s = lines[i];
n = s.length;
if (s[n-1] === '\r') {
return "DOS/Windows Text (CR/LF)";
}
}
return "UNIX";
}
function checkSubmission(fn, f) {
var lines = f.split('\n')
var c = 0;
var questionList = [];
var questionString;
var q = {};
var i;
var lastQuestion = null;
const fnPattern = filePrefix + "-[a-zA-Z0-9]+\.txt";
const fnRexp = new RegExp(fnPattern);
if (!fnRexp.test(fn)) {
updateTag("status", "ERROR " + fn +
" doesn't follow the pattern " + fnRexp);
return;
}
if (fn === filePrefix + "-template.txt") {
updateTag("status", "ERROR " + fn +
" has the default name, please change template to your mycarletonone username");
return;
}
updateTag("filename", fn);
let encoding = lineEncoding(lines);
if (encoding !== "UNIX") {
updateTag("status", "ERROR " + fn +
" is not a UNIX textfile, it is a "
+ encoding + " file.");
return;
}
if (submissionName !== lines[0]) {
updateTag("status", "ERROR " + fn +
" doesn't start with \"" + submissionName + "\"");
return;
}
try {
q.name = lines[1].match(/^Name:(.+)/m)[1].trim();
q.studentID = lines[2].match(/^Student ID:(.+)/m)[1].trim();
} catch (error) {
updateTag("status", "ERROR " + fn +
" has bad Name or Student ID field");
return;
}
updateTag("name", q.name);
updateTag("studentID", q.studentID);
var questionRE = /^([0-9a-g]+)\.(.*)/;
for (i = 4; i < lines.length; i++) {
if (typeof(lines[i]) === 'string') {
lines[i] = lines[i].replace('\r','');
}
let m = lines[i].match(questionRE);
if (m) {
c++;
questionList.push(m[1]);
q[m[1]] = m[2];
lastQuestion = m[1];
} else {
if (lastQuestion !== null) {
if ((q[lastQuestion] === '') || (q[lastQuestion] === ' ')) {
q[lastQuestion] = lines[i];
} else {
q[lastQuestion] = q[lastQuestion] + "\n" + lines[i];
}
}
}
}
console.log(JSON.stringify(q, null, ' '));
questionString = questionList.toString();
if (questionString !== expectedQuestionList) {
updateTag("status", "ERROR expected questions " +
expectedQuestionList + " but got questions " +
questionString);
} else {
updateTag("status", "PASSED " +
fn + ": " + q.name + " (" + q.studentID + ")");
}
var newP, newText, newPre, newPreText;
let questionDiv = document.getElementById("questions");
for (let qName of questionList) {
let qText = q[qName];
newP = document.createElement("p");
newText = document.createTextNode(qName + ":");
newP.appendChild(newText);
questionDiv.appendChild(newP);
newPre = document.createElement("pre");
newPreText = document.createTextNode(qText);
newPre.appendChild(newPreText);
newP.appendChild(newPre);
}
return q;
}
function hideAnalysis() {
analysisSection = document.getElementById("analysis");
analysisSection.hidden = true;
}
function uploadSubmission() {
var assignmentFile = document.getElementById("assignmentFile");
loadAssignment(assignmentFile, true);
}
function doUploadSubmission(fn, q) {
console.log("sending data to server...");
if (q) {
const request = new Request("/uploadSubmission", {
method: "POST",
body: JSON.stringify(q),
headers: {
'Content-Type': 'application/json'
}
});
fetch(request).then((response) => {
if (response.status === 200) {
updateTag("status", "UPLOAD COMPLETE of " + fn);
} else {
updateTag("status", "UPLOAD ERROR of " + fn +
", failed with status " +
response.status);
}
});
} else {
updateTag("status", "ERROR No valid data to upload!");
}
}
static/list.html
<!DOCTYPE html>
<html>
<head>
<title>COMP 2406 Submission Demo: Submission Listing</title>
<link rel="stylesheet" href="/style.css">
<script src="/list.js" defer></script>
</head>
<body>
<h1>Submission Listing</h1>
<div>
<div></div>
<table>
<thead>
<th>DB ID</th>
<th>Student ID</th>
<th>Name</th>
<th>Question 1</th>
<th>Question 2</th>
<th>Question 3</th>
<th>Question 4</th>
<th>Question 5</th>
</thead>
<tbody id="table"/>
</table>
</div>
<form method="get" action="/">
<button type="submit">Home</button>
</form>
</body>
</html>
static/list.js
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// list.js, part of submitdemo
// for COMP 2406 (Fall 2024), Carleton University
//
// Initial version: November 6, 2024
//
function insertTableData(tableData) {
var t = document.querySelector("#table");
var row = [];
function rowMarkup(s) {
return "<td>" + s + "</td>";
}
for (let r of tableData) {
row.push("<tr>")
row.push(rowMarkup(r.id));
row.push(rowMarkup(r.studentID));
row.push(rowMarkup(r.name));
row.push(rowMarkup(r.q1));
row.push(rowMarkup(r.q2));
row.push(rowMarkup(r.q3));
row.push(rowMarkup(r.q4));
row.push(rowMarkup(r.q5));
row.push("</tr>")
}
t.innerHTML = row.join("\n");
}
async function updateTable() {
console.log("Updating Table...");
try {
const response = await fetch("/list");
if (response.ok) {
const tableData = await response.json();
insertTableData(tableData);
} else {
console.error("Table loading error response: " + response.status);
}
} catch (error) {
console.error("Table loading fetch error: " + error.message);
}
}
updateTable().then(() => {console.log("Table updated!");});
static/style.css
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}