diff --git a/assignments/static/assignments/detail.js b/assignments/static/assignments/detail.js
index e264624..cf46d04 100644
--- a/assignments/static/assignments/detail.js
+++ b/assignments/static/assignments/detail.js
@@ -176,8 +176,10 @@ async function getVersioningPages() {
return versioningPages;
}
-const courseId = JSON.parse(document.getElementById('course_id').textContent);
-const assignmentId = JSON.parse(document.getElementById('assignment_id').textContent);
+const courseIdElement = document.getElementById('course_id');
+const assignmentIdElement = document.getElementById('assignment_id');
+const courseId = courseIdElement ? JSON.parse(courseIdElement.textContent) : null;
+const assignmentId = assignmentIdElement ? JSON.parse(assignmentIdElement.textContent) : null;
// function to set grade inputs step size from checked_pages
async function initializeCheckedPages() {
@@ -991,8 +993,10 @@ function syncCarouselSlide(event) {
const cardCarousels = document.querySelectorAll('.card .carousel');
// get the corresponding set of bootstrap carousel instances
const bsCarousels = []
-for (const carousel of cardCarousels) {
- bsCarousels.push(bootstrap.Carousel.getOrCreateInstance(carousel));
+if (window.bootstrap && bootstrap.Carousel) {
+ for (const carousel of cardCarousels) {
+ bsCarousels.push(bootstrap.Carousel.getOrCreateInstance(carousel));
+ }
}
const btnCarouselPrev = document.querySelectorAll('.card .carousel-control-prev');
@@ -1164,215 +1168,361 @@ function deleteAllSubs(event) {
}
async function fetchGrades() {
+ if (!assignmentId) {
+ throw new Error("Missing assignment_id for grade fetch.");
+ }
const url = `/assignments/${assignmentId}/grades/`;
const response = await fetch(url);
- console.log(response);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch grades: ${response.status}`);
+ }
const data = await response.json();
- console.log("data", data);
- console.log(data["grades"]);
return data["grades"];
}
var chart = document.getElementById('myChart')
if (chart) {
- const groupButtonContainer = document.createElement('div');
- groupButtonContainer.classList.add('d-flex', 'justify-content-end', 'my-2');
- // add the button group to the parent of the chart
- chart.parentElement.append(groupButtonContainer);
- // or with innerHTML
- groupButtonContainer.innerHTML = `
-
- `;
- const groupByDefaultButton = document.getElementById('group-by-default');
- const groupByVersionButton = document.getElementById('group-by-version');
- const groupByQuestionButton = document.getElementById('group-by-question');
-
- var ctx = chart.getContext('2d');
- // await the maxScore from the server
- const maxScore = JSON.parse(document.getElementById('max_score').textContent);
- const all_grades_old = JSON.parse(document.getElementById('all_grades').textContent);
- console.log(all_grades_old);
- // find the min and max of the grades from all_grades
- // const values = Object.values(all_grades);
-
- let fetched_grades = [];
- try {
- fetched_grades = await fetchGrades();
- } catch (error) {
- console.log(error);
- }
+ (async function initializeAssignmentChart() {
+ if (chart.dataset.chartInit === "1") {
+ return;
+ }
+ chart.dataset.chartInit = "1";
- // get the "grade" entry of each object and store them in an array
- const all_grades = fetched_grades.map(obj => obj.grade).filter(grade => grade !== "" && grade !== null);
-
- // set the min value of the x-axis to 0 and the max value to the max possible grade of the assignment
- const minm = 0;
- const maxm = maxScore;
- var histGenerator = d3.bin()
- .domain([minm, maxm]) // TODO: get the min and max of the grades
- .thresholds(39); // number of thresholds; this will create 19+1 bins
-
- var bins = histGenerator(all_grades);
- console.log(bins);
- const x_axis = []
- const y_axis = []
- for (var i = 0; i < bins.length; i++) {
- x_axis.push(bins[i].x0)
- y_axis.push(bins[i].length)
- }
- const data = {
- labels: x_axis,
- datasets: [{
- label: 'Submission Grades',
- data: y_axis,
- backgroundColor: [
- 'rgba(255, 99, 132, 0.2)',
- ],
- borderColor: [
- 'rgba(255, 99, 132, 1)'
- ],
- borderWidth: 1,
- }]
- };
+ const chartContainer = chart.parentElement;
+ // Remove legacy dynamically injected chart control rows from older JS versions.
+ const legacyControlRows = document.querySelectorAll(
+ '.d-flex.justify-content-end.my-2, .d-flex.justify-content-end.mb-2'
+ );
+ legacyControlRows.forEach((row) => {
+ if (row.id === 'chart-controls') {
+ return;
+ }
+ if (row.querySelector('input[name="chart-type"]')) {
+ row.remove();
+ }
+ });
+
+ const groupButtonContainer = chartContainer.querySelector('#chart-controls');
+ if (!groupButtonContainer) {
+ console.warn('Missing #chart-controls in template; skipping chart control initialization.');
+ return;
+ }
+
+ const groupByDefaultButton = groupButtonContainer.querySelector('#group-by-default');
+ const groupByVersionButton = groupButtonContainer.querySelector('#group-by-version');
+ const groupByQuestionButton = groupButtonContainer.querySelector('#group-by-question');
+ const groupBySectionButton = groupButtonContainer.querySelector('#group-by-section');
+ if (!groupByDefaultButton || !groupByVersionButton || !groupByQuestionButton || !groupBySectionButton) {
+ console.warn('Missing one or more chart mode buttons; skipping chart mode initialization.');
+ return;
+ }
- var myChart = new Chart(ctx);
-
- function updateChartGroupDefault() {
- myChart.config.type = 'bar';
- myChart.config.data = data;
- myChart.config.options.scales = {
- y: {
- beginAtZero: true,
- ticks: {
- autoSkip: true,
- maxTicksLimit: 10
+ var ctx = chart.getContext('2d');
+ const fallbackAllGrades = JSON.parse(
+ document.getElementById('all_grades').textContent
+ );
+ const maxScore = Number(
+ JSON.parse(document.getElementById('max_score').textContent)
+ );
+
+ let fetched_grades = [];
+ try {
+ fetched_grades = await fetchGrades();
+ } catch (error) {
+ console.log(error);
+ }
+
+ if (!Array.isArray(fetched_grades) || fetched_grades.length === 0) {
+ fetched_grades = fallbackAllGrades.map((grade, idx) => ({
+ submission_id: `fallback-${idx}`,
+ version: '',
+ grade: grade,
+ section_name: 'Unassigned',
+ }));
+ }
+
+ function toNumericGrade(grade) {
+ if (grade === "" || grade === null || grade === undefined) {
+ return null;
+ }
+ const numeric = Number(grade);
+ if (Number.isNaN(numeric)) {
+ return null;
+ }
+ return numeric;
+ }
+
+ function sectionNameFromObject(obj) {
+ const sectionName = (obj.section_name || "").toString().trim();
+ return sectionName || "Unassigned";
+ }
+
+ function getGradeValues(records) {
+ return records
+ .map((obj) => toNumericGrade(obj.grade))
+ .filter((grade) => grade !== null);
+ }
+
+ function groupByVersion(records) {
+ return records.reduce((acc, obj) => {
+ const grade = toNumericGrade(obj.grade);
+ if (grade === null) {
+ return acc;
}
- },
- x: {
- ticks: {
- min: 0,
- max: maxScore,
- stepSize: 1,
- maxTicksLimit: 10,
+ const version = obj.version || '';
+ if (!acc[version]) {
+ acc[version] = [];
+ }
+ acc[version].push(grade);
+ return acc;
+ }, {});
+ }
+
+ function groupByQuestion(records) {
+ return records.reduce((acc, obj) => {
+ for (const [key, value] of Object.entries(obj)) {
+ if (!key.startsWith('question_') || !key.endsWith('_grade')) {
+ continue;
+ }
+ const question = key.split('_')[1];
+ if (!acc[question]) {
+ acc[question] = [];
+ }
+ const numericValue = toNumericGrade(value);
+ if (numericValue !== null) {
+ acc[question].push(numericValue);
+ }
+ }
+ return acc;
+ }, {});
+ }
+
+ function groupBySection(records) {
+ return records.reduce((acc, obj) => {
+ const grade = toNumericGrade(obj.grade);
+ if (grade === null) {
+ return acc;
+ }
+ const sectionName = sectionNameFromObject(obj);
+ if (!acc[sectionName]) {
+ acc[sectionName] = [];
}
+ acc[sectionName].push(grade);
+ return acc;
+ }, {});
+ }
+
+ function updateGroupedButtonsDisabledState(records) {
+ const groupedVersion = groupByVersion(records);
+ const groupedQuestion = groupByQuestion(records);
+ const groupedSection = groupBySection(records);
+ groupByVersionButton.disabled = Object.keys(groupedVersion).length <= 1;
+ groupByQuestionButton.disabled = Object.keys(groupedQuestion).length <= 1;
+ groupBySectionButton.disabled = Object.keys(groupedSection).length === 0;
+ return { groupedVersion, groupedQuestion, groupedSection };
+ }
+
+ var histGenerator = d3.bin()
+ .domain([0, maxScore])
+ .thresholds(39);
+
+ var myChart = new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: [],
+ datasets: [],
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: true,
}
- };
- myChart.update();
- }
- updateChartGroupDefault();
- groupByDefaultButton.addEventListener('click', updateChartGroupDefault);
+ });
-
- // add event listener for the group-by-version button
- // when the button is clicked, group the grades by version
- if (groupByVersionButton) {
- // group by version
- const grouped_version = fetched_grades.reduce((acc, obj) => {
- const version = obj.version;
- if (!acc[version]) {
- acc[version] = [];
+ function updateChartGroupDefault(records) {
+ const grades = getGradeValues(records);
+ const bins = histGenerator(grades);
+ const x_axis = [];
+ const y_axis = [];
+ for (let i = 0; i < bins.length; i++) {
+ x_axis.push(bins[i].x0);
+ y_axis.push(bins[i].length);
}
- // skip empty grades
- if (obj.grade !== "" && obj.grade !== null)
- acc[version].push(obj.grade);
- return acc;
- }, {});
- console.log(grouped_version);
- // if there is at most one version, disable the button
- if (Object.keys(grouped_version).length <= 1) {
- groupByVersionButton.disabled = true;
+ myChart.config.type = 'bar';
+ myChart.config.data = {
+ labels: x_axis,
+ datasets: [{
+ label: 'Submission Grades',
+ data: y_axis,
+ backgroundColor: ['rgba(255, 99, 132, 0.2)'],
+ borderColor: ['rgba(255, 99, 132, 1)'],
+ borderWidth: 1,
+ }]
+ };
+ myChart.config.options = {
+ responsive: true,
+ maintainAspectRatio: true,
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ autoSkip: true,
+ maxTicksLimit: 10
+ }
+ },
+ x: {
+ ticks: {
+ min: 0,
+ max: maxScore,
+ stepSize: 1,
+ maxTicksLimit: 10,
+ }
+ }
+ }
+ };
+ myChart.update();
}
- function updateChartGroupVersions() {
+
+ function updateChartGroupVersions(records, groupedVersion) {
+ const versionKeys = Object.keys(groupedVersion).sort((a, b) => {
+ if (a === '' && b !== '') {
+ return -1;
+ }
+ if (b === '' && a !== '') {
+ return 1;
+ }
+ const aNumber = Number(a);
+ const bNumber = Number(b);
+ if (!Number.isNaN(aNumber) && !Number.isNaN(bNumber)) {
+ return aNumber - bNumber;
+ }
+ return a.localeCompare(b);
+ });
+ const grades = versionKeys.map((versionKey) => groupedVersion[versionKey]);
myChart.config.type = 'violin';
myChart.config.data = {
- labels: Object.keys(grouped_version).map(version => {
- version = version === '' ? 'Outliers' : `Version ${version}`
- return version;
+ labels: versionKeys.map((version) => {
+ if (version === '') {
+ return 'Outliers';
+ }
+ return `Version ${version}`;
+ }),
+ datasets: [{
+ label: 'Versions',
+ data: grades,
+ backgroundColor: 'rgba(255, 99, 132, 0.2)',
+ borderColor: 'rgba(255, 99, 132, 1)',
+ borderWidth: 1,
+ outlierColor: '#999999',
+ padding: 10,
+ itemRadius: 0,
+ }]
+ };
+ myChart.config.options = {
+ responsive: true,
+ maintainAspectRatio: true,
+ scales: {
+ y: {
+ min: 0,
+ max: maxScore,
+ }
}
- ),
- datasets: []
};
- const grades = Object.values(grouped_version);
-
- myChart.config.data.datasets.push({
- label: `Versions`,
- data: grades,
- backgroundColor: 'rgba(255, 99, 132, 0.2)',
- borderColor: 'rgba(255, 99, 132, 1)',
- borderWidth: 1,
- outlierColor: '#999999',
- padding: 10,
- itemRadius: 0,
- });
-
- // // change the y axis limit to the min and max of the grades
- myChart.config.options.scales.y.min = 0;
- myChart.config.options.scales.y.max = Math.max(...all_grades);
myChart.update();
- console.log(myChart.config.data);
}
- groupByVersionButton.addEventListener('click', updateChartGroupVersions);
- }
-
- // add event listener for the group-by-question button
- // when the button is clicked, group the grades by question
- if (groupByQuestionButton) {
- const grouped_question = fetched_grades.reduce((acc, obj) => {
- // fields `question_{i}_grade` are the grades for each question
- for (const [key, value] of Object.entries(obj)) {
- if (key.startsWith('question_')) {
- const question = key.split('_')[1];
- if (!acc[question]) {
- acc[question] = [];
+ function updateChartGroupQuestions(records, groupedQuestion) {
+ const questionKeys = Object.keys(groupedQuestion).sort(
+ (a, b) => Number(a) - Number(b)
+ );
+ const grades = questionKeys.map((questionKey) => groupedQuestion[questionKey]);
+ myChart.config.type = 'violin';
+ myChart.config.data = {
+ labels: questionKeys.map((question) => `Question ${question}`),
+ datasets: [{
+ label: 'Questions',
+ data: grades,
+ backgroundColor: 'rgba(255, 99, 132, 0.2)',
+ borderColor: 'rgba(255, 99, 132, 1)',
+ borderWidth: 1,
+ outlierColor: '#999999',
+ padding: 10,
+ itemRadius: 0,
+ }]
+ };
+ myChart.config.options = {
+ responsive: true,
+ maintainAspectRatio: true,
+ scales: {
+ y: {
+ min: 0,
+ max: (grades.flat().length === 0) ? 1 : Math.max(...grades.flat()),
}
- // skip empty grades
- if (value !== "" && value !== null)
- acc[question].push(value);
-
}
- }
- return acc;
- }, {});
- console.log(grouped_question);
- if (Object.keys(grouped_question).length <= 1) {
- groupByQuestionButton.disabled = true;
+ };
+ myChart.update();
}
- function updateChartGroupQuestions() {
+
+ function updateChartGroupSections(records, groupedSection) {
+ const sectionKeys = Object.keys(groupedSection).sort(
+ (a, b) => a.localeCompare(b)
+ );
+ const grades = sectionKeys.map((sectionKey) => groupedSection[sectionKey]);
myChart.config.type = 'violin';
myChart.config.data = {
- labels: Object.keys(grouped_question).map(question => `Question ${question}`),
- datasets: []
+ labels: sectionKeys,
+ datasets: [{
+ label: 'Sections',
+ data: grades,
+ backgroundColor: 'rgba(255, 99, 132, 0.2)',
+ borderColor: 'rgba(255, 99, 132, 1)',
+ borderWidth: 1,
+ outlierColor: '#999999',
+ padding: 10,
+ itemRadius: 0,
+ }]
+ };
+ myChart.config.options = {
+ responsive: true,
+ maintainAspectRatio: true,
+ scales: {
+ y: {
+ min: 0,
+ max: (grades.flat().length === 0) ? 1 : Math.max(...grades.flat()),
+ }
+ }
};
- const grades = Object.values(grouped_question);
- myChart.config.data.datasets.push({
- label: `Questions`,
- data: grades,
- backgroundColor: 'rgba(255, 99, 132, 0.2)',
- borderColor: 'rgba(255, 99, 132, 1)',
- borderWidth: 1,
- outlierColor: '#999999',
- padding: 10,
- itemRadius: 0,
- });
- // change the y axis limit to the min and max of the grades
- myChart.config.options.scales.y.min = 0;
- myChart.config.options.scales.y.max = (grades.flat().length === 0) ? 1 : Math.max(...grades.flat());
myChart.update();
- console.log(myChart.config.data);
}
- groupByQuestionButton.addEventListener('click', updateChartGroupQuestions);
- }
+ function renderActiveChart() {
+ const selectedRecords = fetched_grades;
+ const { groupedVersion, groupedQuestion, groupedSection } = (
+ updateGroupedButtonsDisabledState(selectedRecords)
+ );
+
+ if (groupByVersionButton.checked && !groupByVersionButton.disabled) {
+ updateChartGroupVersions(selectedRecords, groupedVersion);
+ return;
+ }
+ if (groupByQuestionButton.checked && !groupByQuestionButton.disabled) {
+ updateChartGroupQuestions(selectedRecords, groupedQuestion);
+ return;
+ }
+ if (groupBySectionButton.checked && !groupBySectionButton.disabled) {
+ updateChartGroupSections(selectedRecords, groupedSection);
+ return;
+ }
+ groupByDefaultButton.checked = true;
+ updateChartGroupDefault(selectedRecords);
+ }
+
+ groupByDefaultButton.addEventListener('click', renderActiveChart);
+ groupByVersionButton.addEventListener('click', renderActiveChart);
+ groupByQuestionButton.addEventListener('click', renderActiveChart);
+ groupBySectionButton.addEventListener('click', renderActiveChart);
+
+ renderActiveChart();
+ })();
}
// add event listener for the .search-bar__button
diff --git a/assignments/templates/assignments/detail.html b/assignments/templates/assignments/detail.html
index e0fbb0d..983eeac 100644
--- a/assignments/templates/assignments/detail.html
+++ b/assignments/templates/assignments/detail.html
@@ -17,7 +17,7 @@
{{ assignment.pk|json_script:"assignment_id"}}
{{ course_pk|json_script:"course_id"}}
-
+
{% endblock scripts %}
@@ -32,14 +32,29 @@