Browse Source

feat: Implement html template and js functions to process ivprog data

Add functions to generate TES, DES and D/T data from exercisees
Lucas de Souza 2 years ago
parent
commit
8da9efd891
4 changed files with 237 additions and 1 deletions
  1. 6 1
      js/main.js
  2. 185 0
      js/util/dataProcess.js
  3. 42 0
      templates/process.html
  4. 4 0
      webpack.config.js

+ 6 - 1
js/main.js

@@ -21,11 +21,13 @@ import {
   prepareActivityToStudentHelper,
   autoEval,
 } from "./util/iassignHelpers";
-import { openAssessmentDetail } from "./util/utils";
+import { openAssessmentDetail, levenshteinDistance } from "./util/utils";
 import { Config } from "./util/config";
+import { processData } from "./util/dataProcess";
 import { parseExpression } from "./util/parseFromVisual";
 import * as CodeEditorAll from "./visualUI/text_editor";
 import { autoGenerateTestCaseOutput } from "./util/auto_gen_output";
+import { generate } from "./visualUI/code_generator";
 
 const CodeEditor = {
   initTextEditor: CodeEditorAll.initTextEditor,
@@ -61,4 +63,7 @@ export {
   autoGenerateTestCaseOutput,
   Config,
   parseExpression,
+  generate as generateCode,
+  levenshteinDistance,
+  processData,
 };

+ 185 - 0
js/util/dataProcess.js

@@ -0,0 +1,185 @@
+export function processData () {
+  const folderInput = document.querySelector("#folder");
+  const header = [
+    "submissionid",
+    "file",
+    "filesize",
+    "timestamp",
+    "humandate",
+    "grade",
+    "userid",
+    "exerciseid",
+  ];
+
+  function generateSubCode (algorithmInIlm) {
+    window.program_obj.functions = JSON.parse(algorithmInIlm).functions;
+    window.program_obj.globals = JSON.parse(algorithmInIlm).globals;
+    return window.ivprogCore.generateCode();
+  }
+
+  function prepareData (map, submission) {
+    if (!submission) {
+      return map;
+    }
+    const exercID = submission["exerciseid"];
+    const studentID = submission["userid"];
+    let exercMap = null;
+    if (map.has(exercID)) {
+      exercMap = map.get(exercID);
+    } else {
+      exercMap = new Map();
+      map.set(exercID, exercMap);
+    }
+    if (exercMap.has(studentID)) {
+      exercMap.get(studentID).push(submission);
+    } else {
+      exercMap.set(studentID, [submission]);
+    }
+    return map;
+  }
+
+  folderInput.addEventListener("change", async () => {
+    const files = Array.from(folderInput.files);
+    const idx = files.find((f) => f.name == "index.csv");
+    const folderName = idx.webkitRelativePath.replace(idx.name, "");
+    console.debug(idx);
+    console.debug(folderName);
+    const data = await idx.text();
+    console.debug(data);
+    const csvEntries = data
+      .split("\n")
+      .slice(1)
+      .filter((line) => line.length > 0)
+      .map((line) => line.split(","))
+      .map((vec) => {
+        const obj = {};
+        vec.forEach((val, i) => (obj[header[i]] = val));
+        return obj;
+      });
+    console.debug(csvEntries);
+    const blockExercMap = csvEntries.reduce(prepareData, new Map());
+    //console.log(Array.from(blockExercMatrix.entries()));
+    const getFilePath = async function (submission) {
+      const path = `${folderName}${submission["file"]}`;
+      const file = files.find((f) => f.webkitRelativePath == path);
+      const text = await file.text();
+      return text;
+    };
+    const matrix = {};
+    let counter = 0;
+    blockExercMap.forEach((studentsMap, exercID) => {
+      const column = [];
+      studentsMap.forEach(async (submissions, studentID) => {
+        submissions = submissions.sort((a, b) => {
+          return parseInt(a["timestamp"]) - parseInt(b["timestamp"]);
+        });
+        counter++;
+        submissions.forEach(async (submission, index, array) => {
+          counter++;
+          const student = {};
+          student["grade"] = Math.max(0, parseFloat(submission["grade"]));
+          student["timestamp"] = parseInt(submission["timestamp"]);
+          student["student_id"] = studentID;
+          student["TES"] = 0;
+          student["DES"] = 0;
+          student["D/T"] = 0;
+          let previousCode = "";
+          if (index > 0) {
+            student["TES"] =
+              parseInt(submission["timestamp"]) -
+              parseInt(array[index - 1]["timestamp"]);
+            const previousFile = await getFilePath(array[index - 1]);
+            const previous = window.ivprogCore
+              .prepareActivityToStudentHelper(previousFile)
+              .getOrElse(1);
+            if (previous == 1) {
+              console.error(
+                `A submission from ${studentID} to ${exercID} is invalid`
+              );
+              return;
+            }
+            previousCode = generateSubCode(previous.algorithmInIlm);
+          }
+          const currentFile = await getFilePath(submission);
+          const current = window.ivprogCore
+            .prepareActivityToStudentHelper(currentFile)
+            .getOrElse(2);
+          if (current == 2) {
+            console.error(
+              `A submission from ${studentID} to ${exercID} is invalid`
+            );
+            return;
+          }
+          const currentCode = generateSubCode(current.algorithmInIlm);
+          if (previousCode === "") {
+            const logs = JSON.parse(currentFile.split("::logs::")[1]);
+            const event = logs[0];
+            if (event == null) {
+              // empty submission
+              student["TES"] = 0;
+            } else if (event.length === 4) {
+              student["TES"] =
+                parseInt(submission["timestamp"]) -
+                Math.floor(parseInt(event[2]) / 1000);
+            } else {
+              student["TES"] =
+                parseInt(submission["timestamp"]) -
+                Math.floor(parseInt(event[1]) / 1000);
+            }
+          }
+          student["DES"] = window.ivprogCore.levenshteinDistance(
+            previousCode,
+            currentCode
+          );
+          const ratio =
+            student["TES"] === 0 ? 0 : student["DES"] / student["TES"];
+          student["D/T"] = isNaN(ratio) ? 0 : ratio;
+          column.push(student);
+          counter--;
+        });
+        counter--;
+      });
+      matrix[exercID] = column;
+    });
+    function download (file, text) {
+      //creating an invisible element
+      const element = document.createElement("a");
+      element.setAttribute(
+        "href",
+        "data:text/plain," + encodeURIComponent(text)
+      );
+      element.setAttribute("download", file);
+      element.innerHTML = file;
+      element.classList.add("ui", "primary", "button");
+
+      // Above code is equivalent to
+      // <a href="path of file" download="file name">
+
+      document.querySelector("#downloads").appendChild(element);
+    }
+    function getExercHeader (id) {
+      const props = ["TES", "DES", "grade", "D/T", "timestamp"];
+      return props.reduce((acc, prop) => acc + `${id}_${prop},`, "");
+    }
+    const id = setInterval(() => {
+      if (counter == 0) {
+        clearInterval(id);
+        for (const exercID in matrix) {
+          let csv = "";
+          let firstLine = "student_id,";
+          firstLine += getExercHeader(exercID);
+          for (const submission of matrix[exercID]) {
+            csv += `${submission["student_id"]},`;
+            csv += `${submission["TES"]},`;
+            csv += `${submission["DES"]},`;
+            csv += `${submission["grade"]},`;
+            csv += `${submission["D/T"]},`;
+            csv += `${submission["timestamp"]}`;
+            csv += "\n";
+          }
+          download(`${exercID}.csv`, `${firstLine}\n${csv}`);
+        }
+      }
+    }, 1000);
+  });
+}

+ 42 - 0
templates/process.html

@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+    <link href="css/roboto.css" type="text/css" rel="stylesheet" />
+    <link rel="stylesheet" href="css/semantic.min.css" />
+    <link rel="stylesheet" type="text/css" href="css/ivprog-visual-1.0.css" />
+    <link rel="stylesheet" type="text/css" href="css/ivprog-term.css" />
+    <link rel="stylesheet" type="text/css" href="css/ivprog-term.css" />
+    <link rel="stylesheet" type="text/css" href="css/ivprog-editor.css" />
+    <script src="js/jquery.min.js"></script>
+    <script src="js/jquery-ui.min.js"></script>
+    <script src="js/semantic.min.js"></script>
+    <script type="text/javascript" src="js/jquery.json-editor.min.js"></script>
+  </head>
+  <body>
+    <div style="padding-top: 50px; content: '';"></div>
+    <div class="ui container grid">
+      <div class="six wide column">
+        <div class="row">
+          <div class="ui form">
+            <div class="field">
+              <input
+                type="file"
+                id="folder"
+                webkitdirectory
+                directory
+                multiple
+              />
+            </div>
+            <div id="downloads" class="field"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </body>
+  <script>
+    (function () {
+          window.ivprogCore.processData()
+    })();
+  </script>
+</html>

+ 4 - 0
webpack.config.js

@@ -71,6 +71,10 @@ module.exports = {
       template: "templates/runner.html",
       filename: path.resolve(__dirname, "build", "runner.html"),
     }),
+    new HtmlWebpackPlugin({
+      template: "templates/process.html",
+      filename: path.resolve(__dirname, "build", "process.html"),
+    }),
     /*new ChangeScriptSourcePlugin(),*/
     new CopyPlugin([
       {