v1.1.0 StudentTask definition
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
| Field | Purpose | Update Rules |
|---|---|---|
savedData (JSON) | Student task answers | Updated on Save or Finalise |
lastSubmit | Most recent save timestamp | Updated every save |
finalSubmit | Timestamp of final submission | Updated only on Finalise |
status (enum) | Workflow stage: 0=Open/Active, 1=Submitted, 2=Viewed, 3=Reviwed, 4=Marked?, 5=Examined | Updated only on Finalise or Submit button by Super, Marker, External |
isLate | Derived from finalSubmit > deadlineDate | Auto-calculated |
superData (JSON) | Supervisor/Marker comments | Updated by reviewer/marker |
superId | User ID of Supervisor | Set on review submission |
reviewedAt | Timestamp of review | Set on super submission |
markerData (JSON) | Marker comments | Set by marker |
markerId | Marker ID | Set on moderation |
markedAt (optional) | Timestamp of review | Set on marker submission |
externalData (JSON) | External examiner comments (optional) | Set by external |
externalId | External ID | Set on moderation |
externalAt (optional) | Timestamp of external view of record | Set 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?
-
Open/Active
- SurveyJS renders initial student questions editable, and others hidden
- Autosave triggers write to
savedDataandlastSubmit
survey.onValueChanged.add(() => {
debounceSave(survey.data);
});
survey.onValueChanged.add((sender) => {
if (currentUser.role === "student") {
autosaveStudentAnswers(sender.data);
}
});
-
Finalise Submission
- Sets
finalSubmit - Updates
status => 1(submitted) - Evaluates
isLate - Triggers webhook for external audit/export
- Sets
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()
})
});
}
reviewerDataormarkerDatais stored separately fromsavedData- 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) ??
- Student answers from (
-
No SurveyJS rendering required, simpler - could also be a PDF format to download?
Role-Based Save / Submission Rules
| Role | Save Target | Autosave? | Submission / Finalise |
|---|---|---|---|
| Student | savedData | Yes | Finalise → sets finalSubmit, triggers webhook |
| Supervisor | reviewerData | No | Manual Submit → sets reviewedAt |
| Marker | markerData | No | Manual Submit → sets markedAt |
| External | N/A (read-only) | N/A | N/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
-
Student and reviewer/marker answer data stored separately
savedData→ studentreviewerData→ supervisormarkerData→ marker/tutor/admin
-
Autosave limited to student role
- Prevents overwriting by other roles
- Reduces chance of lost-in-translation transport errors
- Only needed for student editing mode
-
Finalise submission / Submit button
- Students → triggers finalise + webhook
- Supervisor/marker → manual submit of short answers only
-
Late flag (
isLate)- Derived from
finalSubmitvsdeadlineDatein FileMaker - Students can submit late; flag used for audit/reporting
- Derived from
-
Workflow metadata tracked via timestamps
lastSubmit→ autosave / last student activityfinalSubmit→ 'finalised' submissionreviewedAt→ supervisor/reviewer submissionmarkedAt→ marker comments???
-
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
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);
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