Skip to content

Commit

Permalink
Add course class model and filter students by class in progress dashb…
Browse files Browse the repository at this point in the history
…oard (#87)

* Add course class model and filter students by class in progress dashboard (+ some CSS stuff)

* Update table when user changes class

* Fix dataSchema only showing the columns of the first student

---------

Co-authored-by: zMendes <[email protected]>
  • Loading branch information
toshikurauchi and zMendes authored Aug 15, 2023
1 parent aec7657 commit 297332d
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 85 deletions.
3 changes: 2 additions & 1 deletion backend/app/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import Student, Instructor, Course, Exercise, ExerciseTag, TelemetryData
from .models import Student, Instructor, Course, CourseClass, Exercise, ExerciseTag, TelemetryData


admin.site.register(Student, UserAdmin)
admin.site.register(Instructor, UserAdmin)
admin.site.register(Course)
admin.site.register(CourseClass)
admin.site.register(Exercise)
admin.site.register(ExerciseTag)
admin.site.register(TelemetryData)
24 changes: 24 additions & 0 deletions backend/app/core/migrations/0006_courseclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.4 on 2023-08-10 19:54

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('core', '0005_course_end_date_course_start_date'),
]

operations = [
migrations.CreateModel(
name='CourseClass',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=30, null=True)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.course')),
('students', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
],
),
]
9 changes: 9 additions & 0 deletions backend/app/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ def __str__(self) -> str:
return self.name


class CourseClass(models.Model):
name = models.CharField(max_length=30, blank=True, null=True)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
students = models.ManyToManyField(User)

def __str__(self) -> str:
return f"{self.name} ({self.course})"


class ExerciseTag(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE)
name = models.CharField(max_length=255, blank=True, null=True)
Expand Down
161 changes: 95 additions & 66 deletions backend/app/dashboard/static/dashboard-progress.js
Original file line number Diff line number Diff line change
@@ -1,154 +1,183 @@

function createHandsontable(data, columns_list) {

var container = document.getElementById('table');
Handsontable.renderers.registerRenderer('colorFormattingRenderer', function (
instance,
td,
row,
col,
prop,
value,
cellProperties
) {
cellProperties.editor = false;
Handsontable.renderers.TextRenderer.apply(this, arguments);
td.style.color = 'black';
var cell_value = parseFloat(value);
if (cell_value == 1) {
td.style.background = 'green';
}
else if (cell_value == 0) {
td.style.background = 'red';
}
else if ((cell_value > 0) && (cell_value < 1)) {
td.style.background = '#ffae00'
data = filterStudentsInClass(data);

const container = document.getElementById("table");
let dataSchema = getDataSchema(columns_list);
Handsontable.renderers.registerRenderer(
"colorFormattingRenderer",
function (instance, td, row, col, prop, value, cellProperties) {
cellProperties.editor = false;
Handsontable.renderers.TextRenderer.apply(this, arguments);
td.style.color = "black";
const cell_value = parseFloat(value);
if (cell_value == 1) {
td.style.background = "green";
} else if (cell_value == 0) {
td.style.background = "red";
} else if (cell_value > 0 && cell_value < 1) {
td.style.background = "#ffae00";
}
}
});
);

hot = new Handsontable(container, {
data: data,
dataSchema: dataSchema,
colHeaders: columns_list,
columns: function (column) {
columnMeta = {};
columnMeta.data = columns_list[column]
columnMeta.data = columns_list[column];
return columnMeta;
},
colWidths: [100].concat(Array(columns_list.length - 1).fill(30)),
fixedColumnsStart: 1,
cells: function (row, col, prop) {
var cellProperties = {};
cellProperties.renderer = 'colorFormattingRenderer';
const cellProperties = {};
cellProperties.renderer = "colorFormattingRenderer";
return cellProperties;
},
licenseKey: 'non-commercial-and-evaluation', // for non-commercial use only

licenseKey: "non-commercial-and-evaluation", // for non-commercial use only
});
}

function updateHandsontable(data, columns_list) {
data = filterStudentsInClass(data);

let dataSchema = getDataSchema(columns_list);
hot.updateSettings({
data: data,
dataSchema: dataSchema,
colHeaders: columns_list,
columns: function (column) {
columnMeta = {};
columnMeta.data = columns_list[column]
columnMeta.data = columns_list[column];
return columnMeta;
}, colWidths: [100].concat(Array(columns_list.length - 1).fill(30)),
},
colWidths: [100].concat(Array(columns_list.length - 1).fill(30)),
});
}

function updateFilter() {
function getDataSchema(columns_list) {
var dataSchema = { name: null };
for (var i=0;i<columns_list.length;i++){
dataSchema[columns_list[i]] = null
}
return dataSchema;

}

function getTagSelect() {
return document.getElementById("select-tag");
}

function getClassSelect() {
return document.getElementById("select-class");
}

function getCurrentStudents() {
const classSelect = getClassSelect();
const selectedClass = courseClasses[classSelect.selectedIndex];
return selectedClass.students;
}

var select = document.getElementById("select-tag");
var newValue = select.value;
function filterStudentsInClass(data) {
const currentStudents = getCurrentStudents();
return data.filter((row) => currentStudents.has(row.Name));
}

function updateFilter() {
const select = getTagSelect();
const newValue = select.value;
select.value = "";

if (!Object.keys(tagsObj).includes(newValue))
return;
if (!Object.keys(tagsObj).includes(newValue)) return;
tags.add(newValue);
generateTagView();
updateTableContent();
}

function updateTableContent() {
const clonedData = structuredClone(data);
const clonedColumns = structuredClone(columns);

var clonedData = structuredClone(data);
var clonedColumns = structuredClone(columns);

var filteredColumns = ["Name"]
exerciseList = []
let filteredColumns = ["Name"];
exerciseList = [];
if (tags.size == 0) {
updateHandsontable(clonedData, clonedColumns);
return;
}
tags.forEach(tag => {
tags.forEach((tag) => {
exerciseList = exerciseList.concat(tagsObj[tag]);
});
for (var i = 0; i < exerciseList.length; i++) {
var column_index = clonedColumns.indexOf(exerciseList[i]);
const column_index = clonedColumns.indexOf(exerciseList[i]);
if (column_index != -1)
filteredColumns = filteredColumns.concat(clonedColumns.splice(column_index,1));
filteredColumns = filteredColumns.concat(
clonedColumns.splice(column_index, 1)
);
}

updateHandsontable(clonedData, filteredColumns);
}

function removeTag(ev) {
tags.delete(ev.target.id);
generateTagView();
updateTableContent()
updateTableContent();
}

function generateTagView() {
var tagsDiv = document.getElementById("tags-list");
const tagsDiv = document.getElementById("tags-list");
tagsDiv.innerHTML = "";
tags.forEach(element => {
var tagDiv = document.createElement("div");
tags.forEach((element) => {
const tagDiv = document.createElement("div");
tagDiv.className = "tag";
var remove = document.createElement("button")
remove.className = "btn btn-secondary"
const remove = document.createElement("button");
remove.className = "btn btn-secondary";
remove.onclick = removeTag;
remove.innerText = element
remove.innerText = element;
remove.id = element;
tagDiv.appendChild(remove);
tagsDiv.appendChild(tagDiv);

});
}


var tags = new Set();
var table;
var columns;
var data;
var courseClasses;
var tags;
var hot;

document.addEventListener('DOMContentLoaded', function () {

table = document.getElementById('table');
columns = table.getAttribute('data-columns');
data = table.getAttribute('data-data');
tagsObj = table.getAttribute('data-tags')
document.addEventListener("DOMContentLoaded", function () {
table = document.getElementById("table");
columns = table.getAttribute("data-columns");
data = table.getAttribute("data-data");
tagsObj = table.getAttribute("data-tags");
courseClasses = table.getAttribute("data-classes");

//cleaning
data = data.replace(/'/g, '"');
columns = columns.replace(/'/g, '"');
courseClasses = courseClasses.replace(/'/g, '"');
tagsObj = tagsObj.replace(/'/g, '"').replace(/(\w+):/g, '"$1":');



data = JSON.parse(data);
columns = JSON.parse(columns)
columns = JSON.parse(columns);
courseClasses = JSON.parse(courseClasses);
tagsObj = JSON.parse(tagsObj);

courseClasses.forEach((courseClass) => {
courseClass.students = new Set(courseClass.students);
});

var select = document.getElementById("select-tag")
select.onchange = updateFilter;
const tagSelect = getTagSelect();
tagSelect.onchange = updateFilter;

const classSelect = getClassSelect();
classSelect.onchange = updateTableContent;
tableData = structuredClone(data);

createHandsontable(tableData, columns);
});
});
20 changes: 18 additions & 2 deletions backend/app/dashboard/static/progress-table.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
main {
margin: 1.5em 1em 4em;
}

.error-msg {
color: red;
}

.filterset {
display: flex;
gap: 2em;
align-items: center;
margin-bottom: 0.5em;
}

#tags-list {
display: flex;
margin-bottom: 1em;
}

.tag {
padding: .2rem;
padding: 0.2rem;
}

.btn btn-primary{
.btn btn-primary {
width: 1rem;
height: 1rem;
}
Expand Down
Loading

0 comments on commit 297332d

Please sign in to comment.