Skip to main content

v1.1.0 StudentTask definition

· 8 min read
John Renfrew
Programmer and data architect

Definition of StudentTasks

This article assembles the tech tests undertaken, and review notes for investigations inot the best mechanisms to implement the SurevyJS code for StudentTasks within the DigiFolio application under development. While these are presented as a structured document, and might appear as finalised, it is also for the purpose of discussion of the key decisions represented, to ensure that the right architectural direction is taken.

The core of this document is saved as /


StudentTask Workflow & Implementation

Overview

This document presents the current design patterns and implementation decisions for the StudentTasks, including multi-role handling, autosave, final submission concept, and review workflows.

Actors:

  • Student – completes one or more survey questions (Task)
  • Supervisor – reviews and optionally adds comments
  • Marker – oversight and comments specifically at mid-year/end-of-year
  • External Examiner – audits combined data from selected student task records

Design Principles:

  • Separation of student answers (savedData) and reviewer/marker comments (reviewerData, markerData)
  • Finalised answers pushed to additional log file for audit/ academic governance
  • Autosave only for students
  • Final submission triggers addition of timestamp and webhook audit/export
  • Short reviewer/ marker questions do not require autosave; manual submission only

note: marker/tutor may end up with discrete set of tasks fitted into a student task list, and discussion is needed about additional marker interaction with individual StudentTasks.


Field Definitions

Source: FileMaker records

FieldPurposeUpdate Rules
savedData (JSON)Student task answersUpdated on Save or Finalise
lastSubmitMost recent save timestampUpdated every save
finalSubmitTimestamp of final submissionUpdated only on Finalise
status (enum)Workflow stage: 0=Open/Active, 1=Submitted, 2=Viewed, 3=Reviwed, 4=Marked?, 5=ExaminedUpdated only on Finalise or Submit button by Super, Marker, External
isLateDerived from finalSubmit > deadlineDateAuto-calculated
superData (JSON)Supervisor/Marker commentsUpdated by reviewer/marker
superIdUser ID of SupervisorSet on review submission
reviewedAtTimestamp of reviewSet on super submission
markerData (JSON)Marker commentsSet by marker
markerIdMarker IDSet on moderation
markedAt (optional)Timestamp of reviewSet on marker submission
externalData (JSON)External examiner comments (optional)Set by external
externalIdExternal IDSet on moderation
externalAt (optional)Timestamp of external view of recordSet on external submission

Student Workflow

Student selects placement, then task record

  • From placements, student selects button (based on module and/or placement) to open a list of tasks to complete in deadline date order. Ordered by deadline date, oldest at bottom.
  • Student selects task (restricted if isLocked > 0 or too far in the future?)
  • Do we want student to see all submission in the past?? Once it is finalised it could be unlocked locally in FileMaker to allow modification - what happens to finalSubmit date?
  1. Open/Active

    • SurveyJS renders initial student questions editable, and others hidden
    • Autosave triggers write to savedData and lastSubmit
survey.onValueChanged.add(() => {
debounceSave(survey.data);
});

survey.onValueChanged.add((sender) => {
if (currentUser.role === "student") {
autosaveStudentAnswers(sender.data);
}
});
  1. Finalise Submission

    • Sets finalSubmit
    • Updates status => 1 (submitted)
    • Evaluates isLate
    • Triggers webhook for external audit/export
if (currentUser.role === "student") {
await fetch("/api/student-task/finalise", {
method: "POST",
body: JSON.stringify({
studentTaskId,
savedData: survey.data
})
});
}

Supervisor / Marker Workflow

  • SurveyJS renders student answers read-only
  • Supervisor/marker questions are visible and editable, based on role
  • No autosave; manual submission only
if (currentUser.role === "super" || currentUser.role === "marker") {
await fetch("/api/student-task/review", {
method: "POST",
body: JSON.stringify({
studentTaskId,
reviewerData: survey.data,
reviewerId: currentUser.id,
reviewedAt: new Date().toISOString()
})
});
}
  • reviewerData or markerData is stored separately from savedData
  • Short questions mean manual submit is sufficient

External Examiner Workflow

  • Receives concatenated export snapshot including:

    • Student answers from (savedData)
    • Reviewer comments from (reviewerData)
    • Timestamps (lastSubmit, finalSubmit, reviewedAt) ??
    • Late flag (isLate) ??
  • No SurveyJS rendering required, simpler - could also be a PDF format to download?


Role-Based Save / Submission Rules

RoleSave TargetAutosave?Submission / Finalise
StudentsavedDataYesFinalise → sets finalSubmit, triggers webhook
SupervisorreviewerDataNoManual Submit → sets reviewedAt
MarkermarkerDataNoManual Submit → sets markedAt
ExternalN/A (read-only)N/AN/A

StudentTask Lifecycle Diagram (ASCII)

        ┌───────────────────────────────────┐
│ Student Placement Tasks │
└───────────────────────────────────┘


┌─────────────────────┐
│ Open/Active. │
│ (savedData updated, │
│ lastSubmit) │
└───────┬─────────────┘

Save / Autosave (student only)


┌─────────────────────┐
│ Finalise Submission │
│ finalSubmit set │
│ status==1 │
│ isLate evaluated │
└───────┬─────────────┘


┌───────────────┐
│ ++ Webhook │
│ Export Trigger│
└───────┬───────┘


┌────────────────────────┐
│ Role-Based Review │
└─────────────┬──────────┘

┌───────────┴───────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Supervisor │ │ Marker │
│ Review Task │ │ Review Task │
│ (reviewerData │ │ (markerData │
│ + reviewedAt) │ │ + markedAt) │
└───────┬───────┘ └───────┬───────┘
│ │
└───────► Manual Submit ◄┘


┌─────────────────────┐
│ Final Marking │
│ Done in FileMaker │
│ from aggregate + │
│ isLate flags + │
│ time records data │
│ transferred to ARLT │
└─────────────────────┘


┌─────────────────────┐
│ External Examiner │
│ Receives full export│
│ (student answers + │
│ reviewerData + │
│ timestamps + isLate)│
└─────────────────────┘

Key Definitions & Conclusions

  1. Student and reviewer/marker answer data stored separately

    • savedData → student
    • reviewerData → supervisor
    • markerData → marker/tutor/admin
  2. Autosave limited to student role

    • Prevents overwriting by other roles
    • Reduces chance of lost-in-translation transport errors
    • Only needed for student editing mode
  3. Finalise submission / Submit button

    • Students → triggers finalise + webhook
    • Supervisor/marker → manual submit of short answers only
  4. Late flag (isLate)

    • Derived from finalSubmit vs deadlineDate in FileMaker
    • Students can submit late; flag used for audit/reporting
  5. Workflow metadata tracked via timestamps

    • lastSubmit → autosave / last student activity
    • finalSubmit → 'finalised' submission
    • reviewedAt → supervisor/reviewer submission
    • markedAt → marker comments???
  6. External audit/export

    • Webhook captures snapshot on finalise, to audit log endpoint
    • Supports external Examiner review without SurveyJS

Language

SurveyJS 'questions' we refer to a 'Task', comprising one or more questions A collection of Tasks that is against a student, additionally with an academicYear and deadline, is a 'StudentTask' The StudentTask is considered Open until a second button both saves answer data, and 'finalises' the record by adding a second timestamp value. The submitted answers for a StudentTask is available to be reviewed, firstly by 'Supervisor', and subsequently by a 'Marker' If the submission of a Task is after the Deadline, then it is considered 'Late', based on a field which auto-calculates the

Model considerations

By storing the JSON for the questions on the StudentTask records, then the data will match the related questions. If there is a need to modify (say a question wording) then that is on the Task record until pulled into the StudentTask record(s). Structural changes (particularly modifiying question numbers) should therefore ONLY be done mid-year, for Tasks that are not yet near their deadline. To get the Tasks dynamically runs the risk of an update being prepared for another year then being returned to be answered. This also involves two api calls - one for model and one for data.

If status > 0 (finlised), then:

  • savedData must never change again
  • *unless* unlocked by an admin (this will be logged) - what happens to deadline, isLate, finalSubmit??
  • Only superData can be added
  • Only markerData can be added
tip

createExternalRecord - present as Markdown?

function createExternalRecord(studentTask) {
let text = `Student Inputs:\n`;

for (const [q, a] of Object.entries(studentTask.savedData)) {
text += ` ${q}: ${a}\n`;
}

if (studentTask.superData) {
text += `\nReviewer Comments (by ${studentTask.superId} at ${studentTask.reviewedAt}):\n`;
for (const [k, v] of Object.entries(studentTask.superData)) {
text += ` ${k}: ${v}\n`;
}
}

if (studentTask.markerData) {
text += `\nModerator Comments (by ${studentTask.markerId} at ${studentTask.markeddAt}):\n`;
for (const [k, v] of Object.entries(studentTask.markerData)) {
text += ` ${k}: ${v}\n`;
}
}

return text;
}

Useful code snippets

// App.tsx
<Route path="/tasks/:taskId" element={<TaskSurvey />} />
// TaskSurvey.tsx
const navigate = useNavigate();

//Express proxy
app.post("/submit", async (req, res) => {
try {
const response = await fetch(process.env.ODATA_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.ODATA_TOKEN}`
},
body: JSON.stringify(req.body)
});

const data = await response.json();
res.json(data);
} catch (err) {
res.status(500).json({ error: "Submission failed" });
}
});app.post("/submit", async (req, res) => {
try {
const response = await fetch(process.env.ODATA_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.ODATA_TOKEN}`
},
body: JSON.stringify(req.body)
});

const data = await response.json();
res.json(data);
} catch (err) {
res.status(500).json({ error: "Submission failed" });
}
});

//pattern
import { Model } from "survey-core";
import { Survey } from "survey-react-ui";

const TaskSurvey = () => {
const [model, setModel] = useState<Model>();

useEffect(() => {
async function loadSurvey() {
const definition = await fetchSurveyDefinition();
const savedData = await fetchSavedAnswers();

const survey = new Model(definition);
survey.data = savedData;

survey.onComplete.add(async (sender) => {
await submitSurvey(sender.data);
});

setModel(survey);
}

loadSurvey();
}, []);

return model ? <Survey model={model} /> : <div>Loading...</div>;
};

//express
import express from "express";
import fetch from "node-fetch";
import cors from "cors";

const app = express();

app.use(cors());
app.use(express.json());

app.post("/submit", async (req, res) => {
try {
const response = await fetch(process.env.ODATA_URL!, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization":
"Basic " +
Buffer.from(
process.env.ODATA_USER + ":" + process.env.ODATA_PASS
).toString("base64"),
},
body: JSON.stringify(req.body),
});

const text = await response.text();
res.status(response.status).send(text);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Submission failed" });
}
});

app.listen(3005, () => {
console.log("Proxy running on port 3005");
});

const surveyJson = JSON.parse(data.QuestionsField);
const themeJson = JSON.parse(data.ThemeField);
const savedData = JSON.parse(data.SavedField ?? "{}");

//task
import { Model } from "survey-core";
import { Survey } from "survey-react-ui";

const TaskSurvey = ({ surveyJson, themeJson, savedData }) => {
const survey = new Model(surveyJson);

survey.data = savedData ?? {};

survey.applyTheme(themeJson);

survey.onComplete.add(async (sender) => {
await submitSurvey(sender.data);
});

return <Survey model={survey} />;
};

//at first route load
let globalTheme: any = null;

export async function loadThemeOnce() {
if (globalTheme) return globalTheme;

const res = await fetch("/api/theme");
const data = await res.json();

globalTheme = JSON.parse(data.themeJson);
return globalTheme;
}
//inside page
const theme = await loadThemeOnce();
survey.applyTheme(theme);
info
Student clicks task

React route /task/:taskId

Backend fetches StudentTasks record

Returns:
surveyJson
savedData

Frontend:
Parse JSON
Load global theme
Create Survey Model
Inject saved data
Render

On complete:
POST to proxy

Proxy writes to OData (Basic auth)

//additonal button
survey.addNavigationItem({
id: "finalise",
title: "Finalise Submission",
action: async () => {
await finaliseSubmission(survey.data);
},
css: "sd-btn--action"
});
POST /api/student-task/save
POST /api/student-task/finalise