From 16bec2f5e1b5b69789435988c578d5c1f3651186 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Sat, 27 Jul 2024 21:43:28 +0200 Subject: [PATCH 01/15] Resolving issues filtering with no organizations --- course.py | 14 ++++++++++++++ static/scripts/util.js | 14 +++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/course.py b/course.py index 36da38c..3da9bc3 100644 --- a/course.py +++ b/course.py @@ -65,6 +65,11 @@ def assign_teachers_to_course(course_id, course_year, assigned_teachers): raise e +def count_org(orgs): + if len(orgs) == 0: + return "Please select at least one organization", 400 + + @course_bp.route('/add_course', methods=['POST', 'GET']) @login_required @check_access_level(Role.ADMIN) @@ -87,6 +92,9 @@ def add_course(): language = request.form['language'] organization_ids = request.form.getlist('organization_code[]') + check_orgs = count_org(organization_ids) + if check_orgs: + return make_response(*check_orgs) try: is_course = db.session.query(Course).filter(Course.code == code, @@ -167,6 +175,9 @@ def update_course_info(): nbr_monitor_students = request.form['nbr_monitor_students'] assigned_teachers = request.form.getlist('assigned_teachers[]') organisation_code = request.form.getlist('organization_code[]') + check_orgs = count_org(organisation_code) + if check_orgs: + return make_response(*check_orgs) course = db.session.query(Course).filter(Course.id == course_id, Course.year == year).first() if not course: @@ -239,6 +250,9 @@ def add_duplicate_course(): course_id = request.form['course_id'] assigned_teachers = request.form.getlist('assigned_teachers[]') organisation_code = request.form.getlist('organization_code[]') + check_orgs = count_org(organisation_code) + if check_orgs: + return make_response(*check_orgs) try: duplicate_course = Course(id=course_id, code=code, title=title, quadri=quadri, year=year, diff --git a/static/scripts/util.js b/static/scripts/util.js index 6856b2e..e0b7cec 100644 --- a/static/scripts/util.js +++ b/static/scripts/util.js @@ -75,18 +75,10 @@ function filter(page) { if (activeOrganizations.length > 0) { items.each(function () { - let organizations; - let showItem; - - if (page === "course") { - organizations = $(this).data('organizations').toString().split(','); - showItem = activeOrganizations.some(org => organizations.includes(org.toString())); - } - else if (page === "user") { - organizations = $(this).data('organizations'); - showItem = activeOrganizations.includes(organizations); - } + let organizations = $(this).data('organizations').toString().split(','); + let showItem = activeOrganizations.some(org => organizations.includes(org.toString())); + // Show or hide the item based on whether it belongs to any active organization if (showItem) { $(this).show(); } else { From 585ace3daa0cfc7793086573913faff95df46685 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Mon, 29 Jul 2024 20:22:54 +0200 Subject: [PATCH 02/15] use toast to display errors --- course.py | 21 ++++++++++----------- templates/add_course.html | 1 + templates/course_template.html | 1 + templates/toast_check_orgs.html | 29 +++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 templates/toast_check_orgs.html diff --git a/course.py b/course.py index 3da9bc3..fe0a969 100644 --- a/course.py +++ b/course.py @@ -66,8 +66,7 @@ def assign_teachers_to_course(course_id, course_year, assigned_teachers): def count_org(orgs): - if len(orgs) == 0: - return "Please select at least one organization", 400 + return len(orgs) == 0 @course_bp.route('/add_course', methods=['POST', 'GET']) @@ -92,9 +91,9 @@ def add_course(): language = request.form['language'] organization_ids = request.form.getlist('organization_code[]') - check_orgs = count_org(organization_ids) - if check_orgs: - return make_response(*check_orgs) + if count_org(organization_ids): + flash("Please select at least one organization", "danger") + return redirect(url_for('course.add_course')) try: is_course = db.session.query(Course).filter(Course.code == code, @@ -175,9 +174,9 @@ def update_course_info(): nbr_monitor_students = request.form['nbr_monitor_students'] assigned_teachers = request.form.getlist('assigned_teachers[]') organisation_code = request.form.getlist('organization_code[]') - check_orgs = count_org(organisation_code) - if check_orgs: - return make_response(*check_orgs) + if count_org(organisation_code): + flash("Please select at least one organization", "danger") + return redirect(url_for('course.course_info', course_id=course_id)) course = db.session.query(Course).filter(Course.id == course_id, Course.year == year).first() if not course: @@ -250,9 +249,9 @@ def add_duplicate_course(): course_id = request.form['course_id'] assigned_teachers = request.form.getlist('assigned_teachers[]') organisation_code = request.form.getlist('organization_code[]') - check_orgs = count_org(organisation_code) - if check_orgs: - return make_response(*check_orgs) + if count_org(organisation_code): + flash("Please select at least one organization", "danger") + return redirect(url_for('course.duplicate_course', course_id=course_id, year=year)) try: duplicate_course = Course(id=course_id, code=code, title=title, quadri=quadri, year=year, diff --git a/templates/add_course.html b/templates/add_course.html index 4b45b11..5e3cf8f 100644 --- a/templates/add_course.html +++ b/templates/add_course.html @@ -13,6 +13,7 @@ {% endblock %} {% block pagecontent %} + {% include "toast_check_orgs.html" %}

New Course

diff --git a/templates/course_template.html b/templates/course_template.html index e8fa3ac..dedc6ca 100644 --- a/templates/course_template.html +++ b/templates/course_template.html @@ -14,6 +14,7 @@ {% endblock %} {% block pagecontent %} + {% include "toast_check_orgs.html" %}

diff --git a/templates/toast_check_orgs.html b/templates/toast_check_orgs.html new file mode 100644 index 0000000..c0949cf --- /dev/null +++ b/templates/toast_check_orgs.html @@ -0,0 +1,29 @@ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + + {% endif %} + {% endwith %} +
+{% block additionalfooter %} + +{% endblock %} \ No newline at end of file From eada3ca914b062e131dc6f40349d43bc0d8aed0d Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Tue, 30 Jul 2024 13:07:59 +0200 Subject: [PATCH 03/15] Fix small issues --- course.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/course.py b/course.py index fe0a969..ead2686 100644 --- a/course.py +++ b/course.py @@ -65,10 +65,6 @@ def assign_teachers_to_course(course_id, course_year, assigned_teachers): raise e -def count_org(orgs): - return len(orgs) == 0 - - @course_bp.route('/add_course', methods=['POST', 'GET']) @login_required @check_access_level(Role.ADMIN) @@ -91,7 +87,7 @@ def add_course(): language = request.form['language'] organization_ids = request.form.getlist('organization_code[]') - if count_org(organization_ids): + if not len(organization_ids): flash("Please select at least one organization", "danger") return redirect(url_for('course.add_course')) @@ -174,7 +170,7 @@ def update_course_info(): nbr_monitor_students = request.form['nbr_monitor_students'] assigned_teachers = request.form.getlist('assigned_teachers[]') organisation_code = request.form.getlist('organization_code[]') - if count_org(organisation_code): + if not len(organisation_code): flash("Please select at least one organization", "danger") return redirect(url_for('course.course_info', course_id=course_id)) @@ -249,7 +245,7 @@ def add_duplicate_course(): course_id = request.form['course_id'] assigned_teachers = request.form.getlist('assigned_teachers[]') organisation_code = request.form.getlist('organization_code[]') - if count_org(organisation_code): + if not len(organisation_code): flash("Please select at least one organization", "danger") return redirect(url_for('course.duplicate_course', course_id=course_id, year=year)) From 7cae21838595e6a277b0edc179ca2230cab9f456 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Tue, 30 Jul 2024 15:22:27 +0200 Subject: [PATCH 04/15] create unique file for toast and use it --- templates/add_course.html | 2 +- templates/course_template.html | 2 +- templates/evaluations.html | 28 +--------------------------- templates/home.html | 24 +----------------------- templates/toast.html | 27 +++++++++++++++++++++++++++ templates/toast_check_orgs.html | 29 ----------------------------- 6 files changed, 31 insertions(+), 81 deletions(-) create mode 100644 templates/toast.html delete mode 100644 templates/toast_check_orgs.html diff --git a/templates/add_course.html b/templates/add_course.html index 5e3cf8f..481f3db 100644 --- a/templates/add_course.html +++ b/templates/add_course.html @@ -13,7 +13,7 @@ {% endblock %} {% block pagecontent %} - {% include "toast_check_orgs.html" %} + {% include "toast.html" %}

New Course

diff --git a/templates/course_template.html b/templates/course_template.html index dedc6ca..1438b82 100644 --- a/templates/course_template.html +++ b/templates/course_template.html @@ -14,7 +14,7 @@ {% endblock %} {% block pagecontent %} - {% include "toast_check_orgs.html" %} + {% include "toast.html" %}

diff --git a/templates/evaluations.html b/templates/evaluations.html index 5bd3202..099a115 100644 --- a/templates/evaluations.html +++ b/templates/evaluations.html @@ -1,26 +1,7 @@ {% extends "layout.html" %} {% block pagetitle %}Evaluation form{% endblock %} {% block pagecontent %} -
-
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} -
-
+ {% include "toast.html" %}

Evaluation form

@@ -103,11 +84,4 @@

Focus on your (first) course of the semester

{% endblock %} -{% block additionalfooter %} - -{% endblock %} diff --git a/templates/home.html b/templates/home.html index 1997232..5229658 100644 --- a/templates/home.html +++ b/templates/home.html @@ -2,22 +2,7 @@ {% block pagetitle %}Home Page{% endblock %} {% block pagecontent %}
-
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
-
- Notification -
-
- {{ message }} -
-
- {% endfor %} - {% endif %} - {% endwith %} -
+ {% include "toast.html" %}

{{ session.first_name }} {{ session.name }}


{% if user.is_researcher %} @@ -113,11 +98,4 @@

Course assignments for my researchers

{% endif %} -{% endblock %} -{% block additionalfooter %} - {% endblock %} \ No newline at end of file diff --git a/templates/toast.html b/templates/toast.html new file mode 100644 index 0000000..d745e2b --- /dev/null +++ b/templates/toast.html @@ -0,0 +1,27 @@ +
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} +
+
+{% block additionalfooter %} + +{% endblock %} diff --git a/templates/toast_check_orgs.html b/templates/toast_check_orgs.html deleted file mode 100644 index c0949cf..0000000 --- a/templates/toast_check_orgs.html +++ /dev/null @@ -1,29 +0,0 @@ -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - - {% endif %} - {% endwith %} -
-{% block additionalfooter %} - -{% endblock %} \ No newline at end of file From 1177b1192418adf37c7b666da3963e710343a9b8 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Wed, 9 Oct 2024 13:01:20 +0200 Subject: [PATCH 05/15] indicate the type of user to be displayed --- templates/users.html | 2 +- user.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/templates/users.html b/templates/users.html index 03878d1..6580e3f 100644 --- a/templates/users.html +++ b/templates/users.html @@ -10,7 +10,7 @@ {% endblock %} {% block pagecontent %}
-

List of Users

+

List of {{ list_name }}


{% include 'filter.html' %}
diff --git a/user.py b/user.py index 8c63d4b..7c6e1c5 100644 --- a/user.py +++ b/user.py @@ -80,18 +80,23 @@ def add_user(): @check_access_level(Role.ADMIN) def users(user_type): base_query = db.session.query(User).filter() + list_name = '' if user_type == 'teacher': base_query = base_query.filter(User.is_teacher == True, User.active == True) + list_name = 'Teachers' elif user_type == 'researcher': base_query = base_query.filter(User.is_researcher == True, User.active == True) + list_name = 'Researchers' elif user_type == 'archived': base_query = base_query.filter(User.active == False) + list_name = 'Archived Users' elif user_type == 'other': base_query = base_query.filter(User.active == True, User.is_teacher == False, User.is_researcher == False) + list_name = 'Other Users' all_users = base_query.all() - return render_template('users.html', users=all_users, user_type=user_type) + return render_template('users.html', users=all_users, user_type=user_type, list_name=list_name) def is_allowed_user(user_id): From 9f8b4cc244c992f0f61803c34a912609408febee Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Wed, 9 Oct 2024 13:06:15 +0200 Subject: [PATCH 06/15] Fix the bug that prevents you from changing organisation if none was selected --- templates/user_profile.html | 11 +++++------ user.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/templates/user_profile.html b/templates/user_profile.html index 366882b..b065e94 100644 --- a/templates/user_profile.html +++ b/templates/user_profile.html @@ -70,13 +70,12 @@

Your informations

disabled {% endif %}> {% if requested_user.organization == None and current_user %} - {% else %} - {% for code in organizations_code %} - - {% endfor %} {% endif %} + {% for code in organizations_code %} + + {% endfor %} diff --git a/user.py b/user.py index 7c6e1c5..a525c7f 100644 --- a/user.py +++ b/user.py @@ -79,7 +79,7 @@ def add_user(): @login_required @check_access_level(Role.ADMIN) def users(user_type): - base_query = db.session.query(User).filter() + base_query = db.session.query(User) list_name = '' if user_type == 'teacher': From df910c4e3547ea8ebf8e61c3fe308a4cc731c205 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Tue, 16 Jul 2024 19:30:05 +0200 Subject: [PATCH 07/15] Add handsontable to the app # Conflicts: # app.py # Conflicts: # app.py # assignment.py # db.py # templates/assignment.html --- static/scripts/index.js | 499 ++++++++++++++++++++++++++++++++++++++ templates/assignment.html | 2 + templates/layout.html | 13 + 3 files changed, 514 insertions(+) create mode 100644 static/scripts/index.js diff --git a/static/scripts/index.js b/static/scripts/index.js new file mode 100644 index 0000000..0aa2f84 --- /dev/null +++ b/static/scripts/index.js @@ -0,0 +1,499 @@ +/** + import Handsontable from 'handsontable'; + import {TextEditor} from "handsontable/editors"; + **/ + +//import {TextEditor} from "./textEditor"; + +fetch('/assignment/load_data') + .then(response => response.json()) + .then(loadData => { + console.log(loadData); + + const {courses, users, teachers, researchers, preferences, organizations, current_year} = loadData; + const example = document.getElementById("handsontable"); + + courses.map(course => { + let teachersId = teachers.filter(teacher => teacher.course_id === course.id && teacher.course_year === current_year); + let teachersName = teachersId.map(teacher => { + let teacherUser = users.find(user => user.id === teacher.user_id); + return teacherUser ? teacherUser.name + " " + teacherUser.first_name : ''; + }); + course.assigned_teachers = teachersName.join(', '); + }); + + const fixedHeaders = [ + "Fgs", + "Researchers", + "Org", + "Promoter", + "Total Load", + "Load Q1", + "Load Q2", + ]; + + const fixedRowsText = [ + "Id", + "Quadri", + "Acads", + "Nbr Student 22/23", + "Assistant", + "Total for now", + ]; + + const lenFixedRowsText = fixedRowsText.length + const lenFixedHeaders = fixedHeaders.length + + //Split long text in the course header + const coursesHeaders = courses.map(course => { + const maxLength = 30; + const courseName = course.title; + + if (courseName.length > maxLength) { + const firstLine = courseName.substring(0, maxLength / 2); + const secondLine = courseName.substring(maxLength / 2); + const finalResult = `${secondLine}
${firstLine}
${course.code}`; + return finalResult + } else { + const finalResult = `${courseName}
${course.code}`; + return finalResult; + } + }); + + //Create all course header + const allHeaders = fixedHeaders.concat(coursesHeaders); + + function fixedRowsData() { + let rows = []; + const properties = ['id', 'quadri', 'assigned_teachers', 'nbr_students', 'nbr_teaching_assistants', 'tutors']; + for (let i = 0; i < lenFixedRowsText; i++) { + let row = {}; + row.researchers = {}; + row.researchers.fgs = ""; + row.researchers.name = ""; + //row.researchers.name = fixedRowsText[i]; + row.org = ""; + row.charge1 = ""; + row.charge2 = ""; + row.charge3 = ""; + row.check = ""; + + courses.forEach(course => { + row[course.code] = properties[i] !== 'tutors' ? course[properties[i]] : 0; + }); + rows.push(row); + } + return rows; + } + + function buildRow(user, isFilled) { + const row = {}; + row.researchers = { + fgs: user.id, + name: isFilled ? `${user.name} ${user.first_name}` : "" + }; + return row; + } + + function userRowsData() { + const rows = []; + const researcherUsers = users.filter(user => user.is_researcher === true); + + researcherUsers.forEach(user => { + const row = buildRow(user, true); + const emptyRow = buildRow(user, false); + + const matchingAssistant = researchers.find(researcher => researcher.user_id === user.id); + const assistantOrg = organizations.find(org => org.id === user.organization_id); + + row.org = assistantOrg ? assistantOrg.name : ""; + row.charge1 = matchingAssistant ? (users.find(sup => sup.id === user.supervisor_id)?.name ?? "") : ""; + row.charge2 = matchingAssistant ? matchingAssistant.max_loads : 0; + row.charge3 = 0; + row.check = 0; + + emptyRow.org = ""; + emptyRow.charge1 = ""; + emptyRow.charge2 = ""; + emptyRow.charge3 = ""; + emptyRow.check = ""; + + const researcher = researchers.find(researcher => researcher.user_id === user.id); + const userPrefs = preferences.filter(pref => pref.researcher_id === researcher.id && pref.course_year === current_year); + + let pos = 1; + courses.forEach(course => { + const isPref = userPrefs.find(pref => pref.course_id === course.id && pref.course_year === current_year); + const code = course.code; + row[code] = isPref ? pos++ : ""; + emptyRow[code] = ""; + }); + + rows.push(row); + rows.push(emptyRow); + }); + + return rows; + } + + const fixedRows = fixedRowsData(); + const userRows = userRowsData(); + + function getCourseColumns() { + const fixedColumns = [ + {data: 'researchers.fgs'}, + {data: 'researchers.name'}, + {data: 'org'}, + {data: 'charge1'}, + {data: 'charge2'}, + {data: 'charge3'}, + {data: 'check'} + /*{ + type: 'dropdown', + source: userNames, + filter: true + }*/ + ]; + + courses.forEach(course => { + const code = course.code; + let col = {data: code} + fixedColumns.push(col) + }); + return fixedColumns; + } + + const columns = getCourseColumns(); + + let data = fixedRows.concat(userRows); + const nbrLines = data.length - 1; + const nbrCols = columns.length - 1; + + const startRow = lenFixedRowsText; + const startCol = 1; + const endRow = nbrLines; + const endCol = lenFixedHeaders - 1; + + const mergeCellsSettings = generateMergeCellSettings(startRow, startCol, endRow, endCol); + + function generateMergeCellSettings(startRow, startCol, endRow, endCol) { + const mergeCellsSettings = []; + for (let row = startRow; row < endRow; row += 2) { + for (let col = startCol; col <= endCol; col++) { + mergeCellsSettings.push({ + row: row, + col: col, + rowspan: 2, + colspan: 1 + }); + } + } + + return mergeCellsSettings; + } + + function storeDataLocally(data) { + localStorage.setItem('table', JSON.stringify(data)); + } + + function storeCellsMeta(cellsMeta) { + localStorage.setItem('cellsMeta', JSON.stringify(cellsMeta)); + } + + function retrieveDataLocally() { + const storedData = localStorage.getItem('table'); + if (storedData) { + data = JSON.parse(storedData); + } + } + + function retrieveCellsMetaLocally() { + //localStorage.removeItem('cellsMeta'); + const cellsMeta = localStorage.getItem('cellsMeta'); + return cellsMeta ? JSON.parse(cellsMeta) : null; + } + + function resetDataLocally() { + localStorage.clear(); + } + + // Verify if the table is modified locally + retrieveDataLocally(); + let comments = retrieveCellsMetaLocally(); + let isCollectedMetaData = true; + + const table = new Handsontable(example, { + data: data, + //editor: TextEditor, + fixedColumnsLeft: lenFixedHeaders, + fixedRowsTop: lenFixedRowsText, + manualColumnMove: true, + manualColumnResize: true, + manualRowResize: true, + mergeCells: mergeCellsSettings, + comments: true, + hiddenColumns: { + columns: [0], + indicators: true, + }, + hiddenRows: { + rows: [0], + indicators: true, + }, + contextMenu: ['commentsAddEdit', 'commentsRemove', 'hidden_columns_hide', 'hidden_rows_hide', 'hidden_columns_show', 'hidden_rows_show'], + filters: true, + dropdownMenu: ['filter_by_value', 'filter_action_bar'], + className: 'controlsQuickFilter htCenter htMiddle', + colHeaders: allHeaders, + //className: 'htCenter htMiddle', + columns: columns, + colWidths: 100, + columnHeaderHeight: 225, + rowHeaders: fixedRowsText, + rowHeaderWidth: 125, + init: function () { + if (comments) { + comments.forEach(comment => { + this.setCellMeta(comment.row, comment.col, comment.key, comment.value); + }); + } + isCollectedMetaData = false; + }, + afterGetColHeader: function (col, th) { + th.style.transform = 'rotate(180deg)'; + th.style.writingMode = 'vertical-lr'; + th.style.textAlign = 'center'; + th.style.fontWeight = 'bold'; + th.style.height = '100px'; + th.style.lineHeight = '100px'; + + if (col > lenFixedHeaders - 1) { + let colData = this.getDataAtCol(col); + if (colData[5] >= colData[4]) { + th.style.backgroundColor = 'green'; + } + } + }, + afterChange: function (changes) { + if (changes) { + changes.forEach(([row, prop, oldValue, newValue]) => { + let col = this.propToCol(prop); + if ((col >= lenFixedHeaders && row >= lenFixedRowsText) && (row % 2 === 1)) { + //Boolean to determine whether a cell is updated or empty + let isDeleted = false; + + //Update total course assistants + if (oldValue !== null && newValue === null) { + isDeleted = true; + } + + const colInfos = this.getDataAtProp(prop); + let nbrAssistants = colInfos[5]; + + if (isDeleted) { + nbrAssistants--; + } else { + nbrAssistants++; + } + this.setDataAtCell(5, col, nbrAssistants); + + //Update user load + const rowInfos = this.getDataAtRow(row - 1); + const quadri = colInfos[1]; + if (quadri === 1) { + let load_q1 = rowInfos[5]; + if (isDeleted) { + load_q1--; + } else { + load_q1++; + } + this.setDataAtCell(row - 1, 5, load_q1); + } else { + let load_q2 = rowInfos[6]; + if (isDeleted) { + load_q2--; + } else { + load_q2++; + } + this.setDataAtCell(row - 1, 6, load_q2); + } + } + }); + //storeDataLocally(data); + } + }, + afterSetCellMeta: function (row, col, key, value) { + if (key === 'comment' && !isCollectedMetaData) { + let comments = JSON.parse(localStorage.getItem('cellsMeta')) || []; + const comment = {'row': row, 'col': col, 'key': key, 'value': value} + comments.push(comment); + storeCellsMeta(comments); + } + }, + afterRenderer: function (TD, row, col, prop, value, cellProperties) { + if ((col >= lenFixedHeaders && row >= lenFixedRowsText) && (row % 2 === 1) && (value !== '')) { + TD.style.fontWeight = 'bold'; // Bold text + TD.style.textAlign = 'left'; // Left alignment + } + //Style first col if needed + if (col === 0 && row < lenFixedRowsText) { + TD.style.fontWeight = 'bold'; + TD.style.textAlign = 'left'; + } + //(row%2) === 1 to avoid empty lines + if (row >= lenFixedRowsText && (row % 2) === 0 && col < lenFixedHeaders) { + const rowValue = this.getDataAtRow(row); + if (col === 5 || col === 6) { + const total_load = rowValue[4]; + const load_q1 = rowValue[5]; + const load_q2 = rowValue[6]; + + //First case: load Q1 + Q2 = total load + //Second case: load Q1 or Q2 = half of total load + if (load_q1 + load_q2 > total_load) { + TD.style.backgroundColor = 'red'; + } else if ((total_load === 4 || total_load === 2) && (total_load / value <= 2 || load_q1 + load_q2 === total_load)) { + TD.style.backgroundColor = 'green'; + } else if (total_load === 1) { + if (value === 1) { + TD.style.backgroundColor = 'green'; + } else { + if ((load_q1 === 1 && load_q2 === 0) || (load_q1 === 0 && load_q2 === 1)) { + TD.style.backgroundColor = 'green'; + } + } + } + } + } + }, + customBorders: [ + { + range: { + from: { + row: 0, + col: 0, + }, + to: { + row: nbrLines, + col: 6, + }, + }, + end: { + width: 2, + color: 'black' + }, + }, + { + range: { + from: { + row: 0, + col: 0, + }, + to: { + row: 5, + col: nbrCols, + }, + }, + bottom: { + width: 2, + color: 'black' + }, + }, + { + row: 5, + col: 6, + bottom: { + width: 2, + color: 'black' + } + }, + ], + beforeFilter(conditionsStack) { + const filtersPlugin = this.getPlugin('filters'); + const tab = this.getData(); + + let values = []; + const filteredResults = []; + const col = conditionsStack[0].column; + + if (conditionsStack && conditionsStack.length > 0) { + for (let i = 0; i < conditionsStack.length; i++) { + values = conditionsStack[i].conditions[0].args.flat(); + + for (const row of tab) { + if (values.includes(row[col])) { + filteredResults.push(row[0]); + } + } + } + } + filtersPlugin.clearConditions(); + filtersPlugin.addCondition(0, 'by_value', [filteredResults]); + }, + afterFilter(conditionsStack) { + const filtersPlugin = this.getPlugin('filters'); + const filtersRowsMap = filtersPlugin.filtersRowsMap; + + // Exclude fixed lines from the filter + for (let i = 0; i < lenFixedRowsText; i++) { + filtersRowsMap.setValueAtIndex(i, false); + } + }, + licenseKey: "non-commercial-and-evaluation", + }); + + $(document).ready(function () { + $('#button-export').click(exportToCSV); + $('#button-create-assignments').click(saveAssignments); + $('#button-clear-assignments').click(clearAssignments) + $('#button-publish-assignments').click(publishAssignments); + }); + + function exportToCSV() { + console.log('Export to CSV clicked'); + } + + function saveAssignments() { + storeDataLocally(data); + } + + function clearAssignments() { + resetDataLocally(); + } + + async function publishAssignments() { + const slicedData = data.slice(lenFixedRowsText); + const result = []; + + for (let i = 1; i < slicedData.length - 1; i += 2) { + const row = slicedData[i]; + let temp_result = Object.keys(row).reduce((filteredRow, key) => { + if (row[key] !== null && row[key] !== "") { + filteredRow[key] = row[key]; + } + return filteredRow; + }, {}); + result.push(temp_result); + } + + try { + const response = await fetch('/assignment/publish_assignments', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(result), + }); + + if (response.ok) { + const responseData = await response.json(); + console.log('Success:', responseData); + } else { + console.error('Failed to publish assignments:', response.statusText); + } + } catch (error) { + console.error('Error:', error); + } + } + }); + diff --git a/templates/assignment.html b/templates/assignment.html index ba2f264..af55082 100644 --- a/templates/assignment.html +++ b/templates/assignment.html @@ -120,4 +120,6 @@

+{% endblock %} +{% block additionalfooter %} {% endblock %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index 2e992c2..5b0fff8 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -116,6 +116,19 @@ + {% endif %} {% block additionalnav %}{% endblock %} From 9cdc7e5653f4e6bb4f8f8f8b6c054c81ac76d3c5 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Tue, 16 Jul 2024 22:47:47 +0200 Subject: [PATCH 08/15] Finish integrating the handsontable panel --- static/scripts/index.js | 146 +++++++++++++++++++++----------------- templates/assignment.html | 2 - 2 files changed, 80 insertions(+), 68 deletions(-) diff --git a/static/scripts/index.js b/static/scripts/index.js index 0aa2f84..52c87bb 100644 --- a/static/scripts/index.js +++ b/static/scripts/index.js @@ -1,10 +1,3 @@ -/** - import Handsontable from 'handsontable'; - import {TextEditor} from "handsontable/editors"; - **/ - -//import {TextEditor} from "./textEditor"; - fetch('/assignment/load_data') .then(response => response.json()) .then(loadData => { @@ -71,7 +64,6 @@ fetch('/assignment/load_data') row.researchers = {}; row.researchers.fgs = ""; row.researchers.name = ""; - //row.researchers.name = fixedRowsText[i]; row.org = ""; row.charge1 = ""; row.charge2 = ""; @@ -148,11 +140,6 @@ fetch('/assignment/load_data') {data: 'charge2'}, {data: 'charge3'}, {data: 'check'} - /*{ - type: 'dropdown', - source: userNames, - filter: true - }*/ ]; courses.forEach(course => { @@ -208,7 +195,6 @@ fetch('/assignment/load_data') } function retrieveCellsMetaLocally() { - //localStorage.removeItem('cellsMeta'); const cellsMeta = localStorage.getItem('cellsMeta'); return cellsMeta ? JSON.parse(cellsMeta) : null; } @@ -224,7 +210,6 @@ fetch('/assignment/load_data') const table = new Handsontable(example, { data: data, - //editor: TextEditor, fixedColumnsLeft: lenFixedHeaders, fixedRowsTop: lenFixedRowsText, manualColumnMove: true, @@ -242,10 +227,9 @@ fetch('/assignment/load_data') }, contextMenu: ['commentsAddEdit', 'commentsRemove', 'hidden_columns_hide', 'hidden_rows_hide', 'hidden_columns_show', 'hidden_rows_show'], filters: true, - dropdownMenu: ['filter_by_value', 'filter_action_bar'], + dropdownMenu: ['filter_by_value', 'filter_action_bar', 'undo'], className: 'controlsQuickFilter htCenter htMiddle', colHeaders: allHeaders, - //className: 'htCenter htMiddle', columns: columns, colWidths: 100, columnHeaderHeight: 225, @@ -319,7 +303,6 @@ fetch('/assignment/load_data') } } }); - //storeDataLocally(data); } }, afterSetCellMeta: function (row, col, key, value) { @@ -443,57 +426,88 @@ fetch('/assignment/load_data') }); $(document).ready(function () { - $('#button-export').click(exportToCSV); - $('#button-create-assignments').click(saveAssignments); - $('#button-clear-assignments').click(clearAssignments) - $('#button-publish-assignments').click(publishAssignments); - }); - - function exportToCSV() { - console.log('Export to CSV clicked'); - } - - function saveAssignments() { - storeDataLocally(data); - } - - function clearAssignments() { - resetDataLocally(); - } + function updateToastContent(message) { + let toastBody = document.querySelector('#toast-notification .toast-body'); + toastBody.textContent = message; + } - async function publishAssignments() { - const slicedData = data.slice(lenFixedRowsText); - const result = []; + let toastNotification = new bootstrap.Toast(document.getElementById('toast-notification')); + + $('#button-export').click(function () { + const exportPlugin = table.getPlugin('exportFile'); + exportPlugin.downloadFile('csv', { + bom: false, + columnDelimiter: ',', + columnHeaders: true, + exportHiddenColumns: false, + exportHiddenRows: false, + fileExtension: 'csv', + filename: 'ICTM-CSV-file_[YYYY]-[MM]-[DD]', + mimeType: 'text/csv', + rowDelimiter: '\r\n', + rowHeaders: true + }); + updateToastContent('Data exported to CSV'); + toastNotification.show(); + }) + $('#button-create-assignments').click(function () { + storeDataLocally(data); + updateToastContent('Data saved'); + toastNotification.show(); + }); + $('#button-clear-assignments').click(function () { + resetDataLocally(); + updateToastContent('Data cleared'); + toastNotification.show(); + setTimeout(function () { + location.reload(); + }, 1500); + }) + $('#button-publish-assignments').click(async function () { + const slicedData = data.slice(lenFixedRowsText); + console.log("Sliced Data", slicedData); + const result = []; + + for (let i = 0; i < slicedData.length; i += 2) { + const user_row = slicedData[i]; + const course_row = slicedData[i + 1]; + + const userData = { + user_id: user_row.researchers.fgs, + load_q1: user_row.charge3, + load_q2: user_row.check, + } - for (let i = 1; i < slicedData.length - 1; i += 2) { - const row = slicedData[i]; - let temp_result = Object.keys(row).reduce((filteredRow, key) => { - if (row[key] !== null && row[key] !== "") { - filteredRow[key] = row[key]; + const courseData = {}; + courses.forEach(course => { + if (course_row[course.code] !== '') { + courseData[course.id] = course_row[course.code]; + } + }) + result.push({userData, courseData}); } - return filteredRow; - }, {}); - result.push(temp_result); - } - try { - const response = await fetch('/assignment/publish_assignments', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(result), + try { + const response = await fetch('/assignment/publish_assignments', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(result), + }); + + if (response.ok) { + updateToastContent('Assignments published'); + toastNotification.show(); + } else { + updateToastContent('Failed to publish assignments' + response.statusText); + toastNotification.show(); + } + } catch (error) { + updateToastContent('Error: ' + error); + toastNotification.show(); + } }); - - if (response.ok) { - const responseData = await response.json(); - console.log('Success:', responseData); - } else { - console.error('Failed to publish assignments:', response.statusText); - } - } catch (error) { - console.error('Error:', error); - } - } - }); + }); + }); diff --git a/templates/assignment.html b/templates/assignment.html index af55082..ba2f264 100644 --- a/templates/assignment.html +++ b/templates/assignment.html @@ -120,6 +120,4 @@

-{% endblock %} -{% block additionalfooter %} {% endblock %} \ No newline at end of file From 690e8b3b293ed01db62b35b167f7d913af64b838 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Tue, 16 Jul 2024 22:55:24 +0200 Subject: [PATCH 09/15] update README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d09526f..34fd1e6 100644 --- a/README.md +++ b/README.md @@ -136,4 +136,11 @@ ``` Replace the `` with the desired information for the first admin. -1. Run the IdP server and the app. \ No newline at end of file +1. Run the IdP server and the app. + +### Setting up Handsontable + +1. Install the npm package `handsontable`: + ```shell + [user@localhost ictm-teaching]$ npm install handsontable + ``` \ No newline at end of file From 841ef0418f9e69d546409102984a6b2197c931ff Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Fri, 26 Jul 2024 18:09:55 +0200 Subject: [PATCH 10/15] Fix PR request --- README.md | 9 +- static/scripts/index.js | 513 ---------------------------------------- 2 files changed, 1 insertion(+), 521 deletions(-) delete mode 100644 static/scripts/index.js diff --git a/README.md b/README.md index 34fd1e6..d09526f 100644 --- a/README.md +++ b/README.md @@ -136,11 +136,4 @@ ``` Replace the `` with the desired information for the first admin. -1. Run the IdP server and the app. - -### Setting up Handsontable - -1. Install the npm package `handsontable`: - ```shell - [user@localhost ictm-teaching]$ npm install handsontable - ``` \ No newline at end of file +1. Run the IdP server and the app. \ No newline at end of file diff --git a/static/scripts/index.js b/static/scripts/index.js deleted file mode 100644 index 52c87bb..0000000 --- a/static/scripts/index.js +++ /dev/null @@ -1,513 +0,0 @@ -fetch('/assignment/load_data') - .then(response => response.json()) - .then(loadData => { - console.log(loadData); - - const {courses, users, teachers, researchers, preferences, organizations, current_year} = loadData; - const example = document.getElementById("handsontable"); - - courses.map(course => { - let teachersId = teachers.filter(teacher => teacher.course_id === course.id && teacher.course_year === current_year); - let teachersName = teachersId.map(teacher => { - let teacherUser = users.find(user => user.id === teacher.user_id); - return teacherUser ? teacherUser.name + " " + teacherUser.first_name : ''; - }); - course.assigned_teachers = teachersName.join(', '); - }); - - const fixedHeaders = [ - "Fgs", - "Researchers", - "Org", - "Promoter", - "Total Load", - "Load Q1", - "Load Q2", - ]; - - const fixedRowsText = [ - "Id", - "Quadri", - "Acads", - "Nbr Student 22/23", - "Assistant", - "Total for now", - ]; - - const lenFixedRowsText = fixedRowsText.length - const lenFixedHeaders = fixedHeaders.length - - //Split long text in the course header - const coursesHeaders = courses.map(course => { - const maxLength = 30; - const courseName = course.title; - - if (courseName.length > maxLength) { - const firstLine = courseName.substring(0, maxLength / 2); - const secondLine = courseName.substring(maxLength / 2); - const finalResult = `${secondLine}
${firstLine}
${course.code}`; - return finalResult - } else { - const finalResult = `${courseName}
${course.code}`; - return finalResult; - } - }); - - //Create all course header - const allHeaders = fixedHeaders.concat(coursesHeaders); - - function fixedRowsData() { - let rows = []; - const properties = ['id', 'quadri', 'assigned_teachers', 'nbr_students', 'nbr_teaching_assistants', 'tutors']; - for (let i = 0; i < lenFixedRowsText; i++) { - let row = {}; - row.researchers = {}; - row.researchers.fgs = ""; - row.researchers.name = ""; - row.org = ""; - row.charge1 = ""; - row.charge2 = ""; - row.charge3 = ""; - row.check = ""; - - courses.forEach(course => { - row[course.code] = properties[i] !== 'tutors' ? course[properties[i]] : 0; - }); - rows.push(row); - } - return rows; - } - - function buildRow(user, isFilled) { - const row = {}; - row.researchers = { - fgs: user.id, - name: isFilled ? `${user.name} ${user.first_name}` : "" - }; - return row; - } - - function userRowsData() { - const rows = []; - const researcherUsers = users.filter(user => user.is_researcher === true); - - researcherUsers.forEach(user => { - const row = buildRow(user, true); - const emptyRow = buildRow(user, false); - - const matchingAssistant = researchers.find(researcher => researcher.user_id === user.id); - const assistantOrg = organizations.find(org => org.id === user.organization_id); - - row.org = assistantOrg ? assistantOrg.name : ""; - row.charge1 = matchingAssistant ? (users.find(sup => sup.id === user.supervisor_id)?.name ?? "") : ""; - row.charge2 = matchingAssistant ? matchingAssistant.max_loads : 0; - row.charge3 = 0; - row.check = 0; - - emptyRow.org = ""; - emptyRow.charge1 = ""; - emptyRow.charge2 = ""; - emptyRow.charge3 = ""; - emptyRow.check = ""; - - const researcher = researchers.find(researcher => researcher.user_id === user.id); - const userPrefs = preferences.filter(pref => pref.researcher_id === researcher.id && pref.course_year === current_year); - - let pos = 1; - courses.forEach(course => { - const isPref = userPrefs.find(pref => pref.course_id === course.id && pref.course_year === current_year); - const code = course.code; - row[code] = isPref ? pos++ : ""; - emptyRow[code] = ""; - }); - - rows.push(row); - rows.push(emptyRow); - }); - - return rows; - } - - const fixedRows = fixedRowsData(); - const userRows = userRowsData(); - - function getCourseColumns() { - const fixedColumns = [ - {data: 'researchers.fgs'}, - {data: 'researchers.name'}, - {data: 'org'}, - {data: 'charge1'}, - {data: 'charge2'}, - {data: 'charge3'}, - {data: 'check'} - ]; - - courses.forEach(course => { - const code = course.code; - let col = {data: code} - fixedColumns.push(col) - }); - return fixedColumns; - } - - const columns = getCourseColumns(); - - let data = fixedRows.concat(userRows); - const nbrLines = data.length - 1; - const nbrCols = columns.length - 1; - - const startRow = lenFixedRowsText; - const startCol = 1; - const endRow = nbrLines; - const endCol = lenFixedHeaders - 1; - - const mergeCellsSettings = generateMergeCellSettings(startRow, startCol, endRow, endCol); - - function generateMergeCellSettings(startRow, startCol, endRow, endCol) { - const mergeCellsSettings = []; - for (let row = startRow; row < endRow; row += 2) { - for (let col = startCol; col <= endCol; col++) { - mergeCellsSettings.push({ - row: row, - col: col, - rowspan: 2, - colspan: 1 - }); - } - } - - return mergeCellsSettings; - } - - function storeDataLocally(data) { - localStorage.setItem('table', JSON.stringify(data)); - } - - function storeCellsMeta(cellsMeta) { - localStorage.setItem('cellsMeta', JSON.stringify(cellsMeta)); - } - - function retrieveDataLocally() { - const storedData = localStorage.getItem('table'); - if (storedData) { - data = JSON.parse(storedData); - } - } - - function retrieveCellsMetaLocally() { - const cellsMeta = localStorage.getItem('cellsMeta'); - return cellsMeta ? JSON.parse(cellsMeta) : null; - } - - function resetDataLocally() { - localStorage.clear(); - } - - // Verify if the table is modified locally - retrieveDataLocally(); - let comments = retrieveCellsMetaLocally(); - let isCollectedMetaData = true; - - const table = new Handsontable(example, { - data: data, - fixedColumnsLeft: lenFixedHeaders, - fixedRowsTop: lenFixedRowsText, - manualColumnMove: true, - manualColumnResize: true, - manualRowResize: true, - mergeCells: mergeCellsSettings, - comments: true, - hiddenColumns: { - columns: [0], - indicators: true, - }, - hiddenRows: { - rows: [0], - indicators: true, - }, - contextMenu: ['commentsAddEdit', 'commentsRemove', 'hidden_columns_hide', 'hidden_rows_hide', 'hidden_columns_show', 'hidden_rows_show'], - filters: true, - dropdownMenu: ['filter_by_value', 'filter_action_bar', 'undo'], - className: 'controlsQuickFilter htCenter htMiddle', - colHeaders: allHeaders, - columns: columns, - colWidths: 100, - columnHeaderHeight: 225, - rowHeaders: fixedRowsText, - rowHeaderWidth: 125, - init: function () { - if (comments) { - comments.forEach(comment => { - this.setCellMeta(comment.row, comment.col, comment.key, comment.value); - }); - } - isCollectedMetaData = false; - }, - afterGetColHeader: function (col, th) { - th.style.transform = 'rotate(180deg)'; - th.style.writingMode = 'vertical-lr'; - th.style.textAlign = 'center'; - th.style.fontWeight = 'bold'; - th.style.height = '100px'; - th.style.lineHeight = '100px'; - - if (col > lenFixedHeaders - 1) { - let colData = this.getDataAtCol(col); - if (colData[5] >= colData[4]) { - th.style.backgroundColor = 'green'; - } - } - }, - afterChange: function (changes) { - if (changes) { - changes.forEach(([row, prop, oldValue, newValue]) => { - let col = this.propToCol(prop); - if ((col >= lenFixedHeaders && row >= lenFixedRowsText) && (row % 2 === 1)) { - //Boolean to determine whether a cell is updated or empty - let isDeleted = false; - - //Update total course assistants - if (oldValue !== null && newValue === null) { - isDeleted = true; - } - - const colInfos = this.getDataAtProp(prop); - let nbrAssistants = colInfos[5]; - - if (isDeleted) { - nbrAssistants--; - } else { - nbrAssistants++; - } - this.setDataAtCell(5, col, nbrAssistants); - - //Update user load - const rowInfos = this.getDataAtRow(row - 1); - const quadri = colInfos[1]; - if (quadri === 1) { - let load_q1 = rowInfos[5]; - if (isDeleted) { - load_q1--; - } else { - load_q1++; - } - this.setDataAtCell(row - 1, 5, load_q1); - } else { - let load_q2 = rowInfos[6]; - if (isDeleted) { - load_q2--; - } else { - load_q2++; - } - this.setDataAtCell(row - 1, 6, load_q2); - } - } - }); - } - }, - afterSetCellMeta: function (row, col, key, value) { - if (key === 'comment' && !isCollectedMetaData) { - let comments = JSON.parse(localStorage.getItem('cellsMeta')) || []; - const comment = {'row': row, 'col': col, 'key': key, 'value': value} - comments.push(comment); - storeCellsMeta(comments); - } - }, - afterRenderer: function (TD, row, col, prop, value, cellProperties) { - if ((col >= lenFixedHeaders && row >= lenFixedRowsText) && (row % 2 === 1) && (value !== '')) { - TD.style.fontWeight = 'bold'; // Bold text - TD.style.textAlign = 'left'; // Left alignment - } - //Style first col if needed - if (col === 0 && row < lenFixedRowsText) { - TD.style.fontWeight = 'bold'; - TD.style.textAlign = 'left'; - } - //(row%2) === 1 to avoid empty lines - if (row >= lenFixedRowsText && (row % 2) === 0 && col < lenFixedHeaders) { - const rowValue = this.getDataAtRow(row); - if (col === 5 || col === 6) { - const total_load = rowValue[4]; - const load_q1 = rowValue[5]; - const load_q2 = rowValue[6]; - - //First case: load Q1 + Q2 = total load - //Second case: load Q1 or Q2 = half of total load - if (load_q1 + load_q2 > total_load) { - TD.style.backgroundColor = 'red'; - } else if ((total_load === 4 || total_load === 2) && (total_load / value <= 2 || load_q1 + load_q2 === total_load)) { - TD.style.backgroundColor = 'green'; - } else if (total_load === 1) { - if (value === 1) { - TD.style.backgroundColor = 'green'; - } else { - if ((load_q1 === 1 && load_q2 === 0) || (load_q1 === 0 && load_q2 === 1)) { - TD.style.backgroundColor = 'green'; - } - } - } - } - } - }, - customBorders: [ - { - range: { - from: { - row: 0, - col: 0, - }, - to: { - row: nbrLines, - col: 6, - }, - }, - end: { - width: 2, - color: 'black' - }, - }, - { - range: { - from: { - row: 0, - col: 0, - }, - to: { - row: 5, - col: nbrCols, - }, - }, - bottom: { - width: 2, - color: 'black' - }, - }, - { - row: 5, - col: 6, - bottom: { - width: 2, - color: 'black' - } - }, - ], - beforeFilter(conditionsStack) { - const filtersPlugin = this.getPlugin('filters'); - const tab = this.getData(); - - let values = []; - const filteredResults = []; - const col = conditionsStack[0].column; - - if (conditionsStack && conditionsStack.length > 0) { - for (let i = 0; i < conditionsStack.length; i++) { - values = conditionsStack[i].conditions[0].args.flat(); - - for (const row of tab) { - if (values.includes(row[col])) { - filteredResults.push(row[0]); - } - } - } - } - filtersPlugin.clearConditions(); - filtersPlugin.addCondition(0, 'by_value', [filteredResults]); - }, - afterFilter(conditionsStack) { - const filtersPlugin = this.getPlugin('filters'); - const filtersRowsMap = filtersPlugin.filtersRowsMap; - - // Exclude fixed lines from the filter - for (let i = 0; i < lenFixedRowsText; i++) { - filtersRowsMap.setValueAtIndex(i, false); - } - }, - licenseKey: "non-commercial-and-evaluation", - }); - - $(document).ready(function () { - function updateToastContent(message) { - let toastBody = document.querySelector('#toast-notification .toast-body'); - toastBody.textContent = message; - } - - let toastNotification = new bootstrap.Toast(document.getElementById('toast-notification')); - - $('#button-export').click(function () { - const exportPlugin = table.getPlugin('exportFile'); - exportPlugin.downloadFile('csv', { - bom: false, - columnDelimiter: ',', - columnHeaders: true, - exportHiddenColumns: false, - exportHiddenRows: false, - fileExtension: 'csv', - filename: 'ICTM-CSV-file_[YYYY]-[MM]-[DD]', - mimeType: 'text/csv', - rowDelimiter: '\r\n', - rowHeaders: true - }); - updateToastContent('Data exported to CSV'); - toastNotification.show(); - }) - $('#button-create-assignments').click(function () { - storeDataLocally(data); - updateToastContent('Data saved'); - toastNotification.show(); - }); - $('#button-clear-assignments').click(function () { - resetDataLocally(); - updateToastContent('Data cleared'); - toastNotification.show(); - setTimeout(function () { - location.reload(); - }, 1500); - }) - $('#button-publish-assignments').click(async function () { - const slicedData = data.slice(lenFixedRowsText); - console.log("Sliced Data", slicedData); - const result = []; - - for (let i = 0; i < slicedData.length; i += 2) { - const user_row = slicedData[i]; - const course_row = slicedData[i + 1]; - - const userData = { - user_id: user_row.researchers.fgs, - load_q1: user_row.charge3, - load_q2: user_row.check, - } - - const courseData = {}; - courses.forEach(course => { - if (course_row[course.code] !== '') { - courseData[course.id] = course_row[course.code]; - } - }) - result.push({userData, courseData}); - } - - try { - const response = await fetch('/assignment/publish_assignments', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(result), - }); - - if (response.ok) { - updateToastContent('Assignments published'); - toastNotification.show(); - } else { - updateToastContent('Failed to publish assignments' + response.statusText); - toastNotification.show(); - } - } catch (error) { - updateToastContent('Error: ' + error); - toastNotification.show(); - } - }); - }); - }); - From d38814defb486f119a589c2368c16969b8433907 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Mon, 21 Oct 2024 18:48:03 +0200 Subject: [PATCH 11/15] rabse + modifying the code to display several promoters --- db.py | 1 + 1 file changed, 1 insertion(+) diff --git a/db.py b/db.py index bdff5ec..d5ad1c7 100644 --- a/db.py +++ b/db.py @@ -109,6 +109,7 @@ class Teacher(db.Model): class PreferenceAssignment(db.Model): __tablename__ = 'preference_assignment' + rank = db.Column(db.Integer, nullable=False) id = db.Column(db.Integer, primary_key=True) rank = db.Column(db.Integer, nullable=False) course_id = db.Column(db.Integer, nullable=False) From 81dff636fa2a28558e08aad5aa134568c573d6b0 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Mon, 7 Oct 2024 19:01:56 +0200 Subject: [PATCH 12/15] remove useless code --- static/scripts/assignment_table.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/static/scripts/assignment_table.js b/static/scripts/assignment_table.js index f89fdc5..bb1c672 100644 --- a/static/scripts/assignment_table.js +++ b/static/scripts/assignment_table.js @@ -282,11 +282,6 @@ fetch('/assignment/load_data') TD.style.fontWeight = 'bold'; // Bold text TD.style.textAlign = 'left'; // Left alignment } - //Style first col if needed - if (col === 0 && row < lenFixedRowsText) { - TD.style.fontWeight = 'bold'; - TD.style.textAlign = 'left'; - } //(row%2) === 1 to avoid empty lines if (row >= lenFixedRowsText && (row % 2) === 0 && col < lenFixedHeaders) { const rowValue = this.getDataAtRow(row); From f6ed468ccfa7f55f6426c07939e198f8030ed10f Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Mon, 7 Oct 2024 22:23:27 +0200 Subject: [PATCH 13/15] Add a way of displaying the number of times an assistant has given a lesson using colours --- assignment.py | 25 +++++++++++++++++++++ static/scripts/assignment_table.js | 35 +++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/assignment.py b/assignment.py index 3a0c48b..212cd44 100644 --- a/assignment.py +++ b/assignment.py @@ -1,3 +1,4 @@ +from sqlalchemy import func from decorators import login_required, check_access_level from db import db, User, Course, PreferenceAssignment, Teacher, Researcher, Organization, \ ResearcherSupervisor, Role, AssignmentDraft, AssignmentPublished @@ -11,6 +12,7 @@ @assignment_bp.route('/assignments', methods=['GET']) @login_required +@check_access_level(Role.ADMIN) def assignments(): return render_template('assignment.html') @@ -72,6 +74,7 @@ def load_data(): @assignment_bp.route('/publish_assignments', methods=['POST']) @login_required +@check_access_level(Role.ADMIN) def publish_assignments(): data = request.get_json() if not data: @@ -130,3 +133,25 @@ def publish_assignments(): except Exception as e: db.session.rollback() return jsonify({"error": f"Failed to publish assignments: {str(e)}"}), 500 + + +def count_course_assignments(): + # Counts the number of times each user has taught each course + assignment_counts = db.session.query( + AssignmentDraft.user_id, + AssignmentDraft.course_id, + func.count(AssignmentDraft.course_id).label('count') + ).group_by( + AssignmentDraft.user_id, + AssignmentDraft.course_id + ).all() + + return assignment_counts + + +@assignment_bp.route('/course_assignments_count', methods=['GET']) +@login_required +@check_access_level(Role.ADMIN) +def get_course_assignments_count(): + results = count_course_assignments() + return jsonify(results=[{'user_id': user_id, 'course_id': course_id, 'count': count} for user_id, course_id, count in results]) diff --git a/static/scripts/assignment_table.js b/static/scripts/assignment_table.js index bb1c672..af7f994 100644 --- a/static/scripts/assignment_table.js +++ b/static/scripts/assignment_table.js @@ -184,6 +184,15 @@ fetch('/assignment/load_data') } } + function getCountCourseAssignment() { + return fetch('/assignment/course_assignments_count') + .then(response => response.json()) + .then(data => { + return data.results; + }) + .catch(error => console.log(error)) + } + const table = new Handsontable(document.getElementById("handsontable"), { data: data, fixedColumnsLeft: lenFixedHeaders, @@ -277,11 +286,35 @@ fetch('/assignment/load_data') }); } }, - afterRenderer: function (TD, row, col, prop, value, cellProperties) { + afterRenderer: async function (TD, row, col, prop, value, cellProperties) { if ((col >= lenFixedHeaders && row >= lenFixedRowsText) && (row % 2 === 1) && (value !== '')) { TD.style.fontWeight = 'bold'; // Bold text TD.style.textAlign = 'left'; // Left alignment } + + if (row >= lenFixedRowsText && col >= lenFixedHeaders && (row % 2 === 0)) { + const assignmentCount = await getCountCourseAssignment(); + + console.log("Assignment count", assignmentCount); + + const user_id = this.getDataAtRowProp(row, 'researchers.id'); // Retrieves user ID + const course_id = this.getDataAtCol(col)[0]; // Retrieves the course ID + + const assignment = assignmentCount.find(item => item.user_id === user_id && item.course_id === course_id); + + if (assignment) { + // Coloration basée sur le nombre d'affectations + const count = assignment.count; + if (count === 1) { + TD.style.backgroundColor = '#5DADE2'; + } else if (count === 2) { + TD.style.backgroundColor = '#87CEEB'; + } else if (count >= 3) { + TD.style.backgroundColor = '#0A74DA'; + } + } + } + //(row%2) === 1 to avoid empty lines if (row >= lenFixedRowsText && (row % 2) === 0 && col < lenFixedHeaders) { const rowValue = this.getDataAtRow(row); From fdfc2ccaa692d3f276ab260923baf86fe9ba1099 Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Tue, 15 Oct 2024 22:39:11 +0200 Subject: [PATCH 14/15] small fix --- assignment.py | 11 ++++++----- course.py | 2 +- static/scripts/assignment_table.js | 4 ++-- templates/courses.html | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/assignment.py b/assignment.py index 212cd44..0757c6b 100644 --- a/assignment.py +++ b/assignment.py @@ -134,17 +134,17 @@ def publish_assignments(): db.session.rollback() return jsonify({"error": f"Failed to publish assignments: {str(e)}"}), 500 - def count_course_assignments(): # Counts the number of times each user has taught each course - assignment_counts = db.session.query( + current_year = get_current_year() + assignment_counts = (db.session.query( AssignmentDraft.user_id, AssignmentDraft.course_id, func.count(AssignmentDraft.course_id).label('count') - ).group_by( + ).filter(AssignmentDraft.course_year < current_year).group_by( AssignmentDraft.user_id, AssignmentDraft.course_id - ).all() + ).distinct().all()) return assignment_counts @@ -154,4 +154,5 @@ def count_course_assignments(): @check_access_level(Role.ADMIN) def get_course_assignments_count(): results = count_course_assignments() - return jsonify(results=[{'user_id': user_id, 'course_id': course_id, 'count': count} for user_id, course_id, count in results]) + return jsonify( + results=[{'user_id': user_id, 'course_id': course_id, 'count': count} for user_id, course_id, count in results]) diff --git a/course.py b/course.py index cac8d77..e09e83e 100644 --- a/course.py +++ b/course.py @@ -117,7 +117,7 @@ def add_course(): @check_access_level(Role.ADMIN) def courses(year): courses = db.session.query(Course).filter_by(year=year).all() - return render_template('courses.html', courses=courses, current_year=year) + return render_template('courses.html', courses=courses, year=year) @course_bp.route('/search_teachers') diff --git a/static/scripts/assignment_table.js b/static/scripts/assignment_table.js index af7f994..acadae7 100644 --- a/static/scripts/assignment_table.js +++ b/static/scripts/assignment_table.js @@ -306,9 +306,9 @@ fetch('/assignment/load_data') // Coloration basée sur le nombre d'affectations const count = assignment.count; if (count === 1) { - TD.style.backgroundColor = '#5DADE2'; - } else if (count === 2) { TD.style.backgroundColor = '#87CEEB'; + } else if (count === 2) { + TD.style.backgroundColor = '#5DADE2'; } else if (count >= 3) { TD.style.backgroundColor = '#0A74DA'; } diff --git a/templates/courses.html b/templates/courses.html index f574819..0e8b65a 100644 --- a/templates/courses.html +++ b/templates/courses.html @@ -52,7 +52,7 @@

Courses

- + {{ course.code }} - {{ course.title }} From 6287ba59d7026bc295b73d97f8a9bf67474349bf Mon Sep 17 00:00:00 2001 From: Samuel Van Campenhout Date: Thu, 5 Dec 2024 16:02:09 +0100 Subject: [PATCH 15/15] update legend in the table --- assignment.py | 5 +++-- static/scripts/assignment_table.js | 13 ++++++------- templates/assignment.html | 27 +++++++++++++++++++++++---- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/assignment.py b/assignment.py index 0757c6b..bd407cb 100644 --- a/assignment.py +++ b/assignment.py @@ -134,15 +134,16 @@ def publish_assignments(): db.session.rollback() return jsonify({"error": f"Failed to publish assignments: {str(e)}"}), 500 + def count_course_assignments(): # Counts the number of times each user has taught each course current_year = get_current_year() assignment_counts = (db.session.query( - AssignmentDraft.user_id, + AssignmentDraft.researcher_id, AssignmentDraft.course_id, func.count(AssignmentDraft.course_id).label('count') ).filter(AssignmentDraft.course_year < current_year).group_by( - AssignmentDraft.user_id, + AssignmentDraft.researcher_id, AssignmentDraft.course_id ).distinct().all()) diff --git a/static/scripts/assignment_table.js b/static/scripts/assignment_table.js index acadae7..2d2b8ba 100644 --- a/static/scripts/assignment_table.js +++ b/static/scripts/assignment_table.js @@ -295,22 +295,21 @@ fetch('/assignment/load_data') if (row >= lenFixedRowsText && col >= lenFixedHeaders && (row % 2 === 0)) { const assignmentCount = await getCountCourseAssignment(); - console.log("Assignment count", assignmentCount); - const user_id = this.getDataAtRowProp(row, 'researchers.id'); // Retrieves user ID const course_id = this.getDataAtCol(col)[0]; // Retrieves the course ID const assignment = assignmentCount.find(item => item.user_id === user_id && item.course_id === course_id); - if (assignment) { - // Coloration basée sur le nombre d'affectations + if (assignment && value !== '' && value !== null) { + // Colouring based on the number of assignments const count = assignment.count; + TD.style.color = 'black'; // to better see the preferences when the cell is coloured if (count === 1) { - TD.style.backgroundColor = '#87CEEB'; + TD.style.backgroundColor = '#A0A7D5'; } else if (count === 2) { - TD.style.backgroundColor = '#5DADE2'; + TD.style.backgroundColor = '#555DB0'; } else if (count >= 3) { - TD.style.backgroundColor = '#0A74DA'; + TD.style.backgroundColor = '#1C2793'; } } } diff --git a/templates/assignment.html b/templates/assignment.html index ba2f264..015ff1d 100644 --- a/templates/assignment.html +++ b/templates/assignment.html @@ -51,11 +51,11 @@
Course Header Colors :
  • Desired - number of assistants reached + number of assistants/loads reached
  • - Too many researchers assigned + Too many researchers/loads assigned
  • @@ -63,6 +63,26 @@
    Course Header Colors :
+
+
Cell Colors:
+
    +
  • + + The researcher has already given this course 1 time +
  • +
  • + + The researcher has already given this course 2 times +
  • +
  • + + The researcher has already given this course 3 times or more +
  • +
+
@@ -85,8 +105,7 @@