Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1e1c279
report data visualization front-end
MImran2002 Apr 11, 2025
4bbf7ef
graph is working
MImran2002 Apr 15, 2025
3439508
the chart has been created and it can be downloaded
MImran2002 Apr 17, 2025
e8282d5
Merge branch 'development' of https://github.com/BCStudentSoftwareDev…
MImran2002 Apr 17, 2025
77906ce
the chart has been created and it can be downloaded and console.log a…
MImran2002 Apr 17, 2025
59ea065
Merge branch 'development' into visualizeReportData
MImran2002 Apr 17, 2025
e56fb8e
add x and y labels
MImran2002 Apr 17, 2025
dbcdc17
Merge branch 'visualizeReportData' of https://github.com/BCStudentSof…
MImran2002 Apr 17, 2025
0bf17ad
changes are met
MImran2002 Apr 21, 2025
ced1d39
changes are met again
MImran2002 Apr 21, 2025
b579cfd
Change font and address feedback
stevensonmichel Apr 23, 2025
22cc9eb
change the x and y values and remove the decimals for the number of e…
MImran2002 Apr 24, 2025
e3c3876
Merge branch 'visualizeReportData' of https://github.com/BCStudentSof…
MImran2002 Apr 24, 2025
f345dfc
Merge branch 'development' into visualizeReportData
stevensonmichel May 2, 2025
293dd63
Merge branch 'development' of https://github.com/BCStudentSoftwareDev…
MImran2002 Oct 20, 2025
3a7599f
Merge branch 'development' into visualizeReportData
BrianRamsay Nov 24, 2025
bde378a
Merge branch 'bonnerCheckmarkProfile' of https://github.com/BCStudent…
MImran2002 Nov 27, 2025
8f492ce
Merge branch 'visualizeReportData' of https://github.com/BCStudentSof…
MImran2002 Nov 27, 2025
d09f193
just fixing
MImran2002 Nov 28, 2025
af05ff4
line chart implemented
MImran2002 Dec 2, 2025
2f8dee0
not working but thinking of creating a fdunction to expand the terms
MImran2002 Dec 12, 2025
fc1c831
terms population corrected, chart sizes correct and bar chart legend …
MImran2002 Dec 15, 2025
48456f2
Merge branch 'development' of https://github.com/BCStudentSoftwareDev…
MImran2002 Dec 17, 2025
c7029d2
committing the new changes so when downloaded th button labels are co…
MImran2002 Dec 17, 2025
d2c7b87
clean up code tweak data visualization
MImran2002 Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions app/controllers/admin/minor.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
from flask import render_template, g, abort, request, redirect, url_for, send_file

from flask import render_template, g, abort, request, redirect, url_for, send_file, jsonify
from app.logic.minor import getMinorProgress
from app.models.user import User

from app.controllers.admin import admin_bp

from app.logic.minor import getMinorInterest, getMinorProgress, toggleMinorInterest, getMinorSpreadsheet, getDeclaredMinorStudents

@admin_bp.route('/profile/<username>/cceMinorChart', methods=['GET'])
def cceMinorChart(username):
if not g.current_user.isAdmin:
abort(403)
else:
progressList = getMinorProgress()
turnToChart = []
for progress in progressList:
turnToChart.append({'name':progress["firstName"] + " " + progress["lastName"], "engagementCount" : progress['engagementCount'], "completeSummer": "Yes" if progress['hasSummer'] == "Completed" else "No", "termDescription": progress['engagementTerm']})
return jsonify(turnToChart)

@admin_bp.route('/admin/cceMinor', methods=['GET','POST'])
def manageMinor():
if not g.current_user.isAdmin:
Expand All @@ -26,14 +37,16 @@ def manageMinor():
interestedStudentEmailString = ';'.join([student['email'] for student in interestedStudentsList])
sustainedEngagement = getMinorProgress()
declaredStudentsList = getDeclaredMinorStudents()
declaredStudentEmailString = ';'.join([student['email'] for student in declaredStudentsList])
declaredStudentEmailString = ';'.join([student['email'] for student in declaredStudentsList])
adminUsername = g.current_user.username

return render_template('/admin/cceMinor.html',
interestedStudentsList = interestedStudentsList,
declaredStudentsList = declaredStudentsList,
interestedStudentEmailString = interestedStudentEmailString,
declaredStudentEmailString = declaredStudentEmailString,
sustainedEngagement = sustainedEngagement,
adminUsername = adminUsername,
)

@admin_bp.route("/admin/cceMinor/download")
Expand Down
21 changes: 14 additions & 7 deletions app/logic/minor.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,20 @@ def getMinorProgress():
summerCase = Case(None, [(CCEMinorProposal.proposalType == "Summer Experience", 1)], 0)

engagedStudentsWithCount = (
User.select(User, fn.COUNT(IndividualRequirement.id).alias('engagementCount'),
fn.SUM(summerCase).alias('hasSummer'),
fn.IF(fn.COUNT(CCEMinorProposal.id) > 0, True, False).alias('hasCCEMinorProposal'))
.join(IndividualRequirement, on=(User.username == IndividualRequirement.username))
User.select(
User,
IndividualRequirement,
Term,
IndividualRequirement.term_id,
fn.COUNT(IndividualRequirement.id).alias('engagementCount'),
fn.SUM(summerCase).alias('hasSummer'),
fn.IF(fn.COUNT(CCEMinorProposal.id) > 0, True, False).alias('hasCCEMinorProposal'))
.join(IndividualRequirement, on=(User.username == IndividualRequirement.username_id))
.join(CertificationRequirement, on=(IndividualRequirement.requirement_id == CertificationRequirement.id))
.switch(User).join(CCEMinorProposal, JOIN.LEFT_OUTER, on= (User.username == CCEMinorProposal.student))
.join(Term, on=(IndividualRequirement.term_id == Term.id))
.switch(User).join(CCEMinorProposal, JOIN.LEFT_OUTER, on= (User.username == CCEMinorProposal.student_id))
.where(CertificationRequirement.certification_id == Certification.CCE)
.group_by(User.firstName, User.lastName, User.username)
.group_by(User.username, IndividualRequirement.term_id, Term.id)
.order_by(SQL("engagementCount").desc())
)
engagedStudentsList = [{'firstName': student.firstName,
Expand All @@ -108,7 +114,8 @@ def getMinorProgress():
'hasGraduated': student.hasGraduated,
'engagementCount': student.engagementCount - student.hasSummer,
'hasCCEMinorProposal': student.hasCCEMinorProposal,
'hasSummer': "Completed" if student.hasSummer else "Incomplete"} for student in engagedStudentsWithCount]
'hasSummer': "Completed" if student.hasSummer else "Incomplete",
'engagementTerm': student.individualrequirement.term.description} for student in engagedStudentsWithCount]
return engagedStudentsList

def getMinorSpreadsheet():
Expand Down
201 changes: 197 additions & 4 deletions app/static/js/minorAdminPage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import searchUser from './searchUser.js'

import searchUser from './searchUser.js';

$(document).ready(function() {
// Load flash message from sessionStorage, if any
Expand Down Expand Up @@ -61,10 +60,205 @@ $('.remove_minor_candidate').on('click', function() {
if (activeTab) {
$('#studentTabs button[data-bs-target="#' + activeTab + '"]').tab('show');
}
let barChart = null;
let lineChart = null;
$("#cceMinor").on("click", function(){
let username = $(this).data("username");
$.ajax({
type: 'GET',
url: '/profile/' + username + '/cceMinorChart',
success: function (responses) {
const $barChart = $("#cceChartByEngagement");
const $lineChart = $("#cceChartByTerm");
const barCanvas = document.getElementById("cceChartByEngagement");
const lineCanvas = document.getElementById("cceChartByTerm");
const SEASONS = ["Spring", "Summer", "Fall"];
const termToIndex = (term) => {
const [season, yearStr] = term.split(" ");
return Number(yearStr) * 3 + SEASONS.indexOf(season);
};
const indexToTerm = (idx) => {
const year = Math.floor(idx / 3);
const season = SEASONS[idx % 3];
return `${season} ${year}`;
};

// Build: term { engagement, students[], studentCounts{} }
const byTerm = {};

for (const r of responses) {
const term = r.termDescription;
const name = r.name;
const count = Number(r.engagementCount) || 0;

if (!byTerm[term]) {
byTerm[term] = { engagement: 0, students: [], studentCounts: {} };
}

byTerm[term].engagement += count;
byTerm[term].students.push(name);
byTerm[term].studentCounts[name] = (byTerm[term].studentCounts[name] || 0) + count;
}
const existingTerms = Object.keys(byTerm);

if (!existingTerms.length) {
if (barChart) barChart.destroy();
if (lineChart) lineChart.destroy();
$barChart.hide();
$lineChart.hide();
return;
}
const indices = existingTerms.map(termToIndex);
const minIdx = Math.min(...indices);
const maxIdx = Math.max(...indices);
const labels = [];
for (let i = minIdx; i <= maxIdx; i++) labels.push(indexToTerm(i));

for (const term of labels) {
if (!byTerm[term]) byTerm[term] = { engagement: 0, students: [], studentCounts: {} };
}
const termEngagements = labels.map((t) => byTerm[t].engagement);
const maxEngagement = Math.max(...termEngagements) + 2;

const isSummer = (term) => term.startsWith("Summer ");
const barColorsByTerm = labels.map((t) => (isSummer(t) ? "blue" : "green"));

const formatStudentCounts = (term) => {
const counts = byTerm[term]?.studentCounts || {};
const entries = Object.entries(counts);
if (!entries.length) return "None";
return entries.map(([name, cnt]) => `${name} (${cnt})`).join(", ");
};

const baseScales = {
y: {
beginAtZero: true,
max: maxEngagement,
ticks: { stepSize: 1 },
title: { display: true, text: "Engagement Count" }
},
x: { title: { display: true, text: "Terms" } }
};

// Bar chart
if (barChart) barChart.destroy();

barChart = new Chart(barCanvas, {
type: "bar",
data: {
labels,
datasets: [
{
label: "Engagement by Term",
data: termEngagements,
backgroundColor: barColorsByTerm
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: baseScales,
plugins: {
title: {
display: true,
text: "CCE Engagements of Each Term",
font: { size: 18 }
},
legend: {
display: true,
position: "top",
labels: {
generateLabels: () => [
{ text: "Summer Term", fillStyle: "blue", strokeStyle: "blue", lineWidth: 1 },
{ text: "Non-Summer Term", fillStyle: "green", strokeStyle: "green", lineWidth: 1 }
]
}
},
tooltip: {
callbacks: {
label: (context) => {
const term = labels[context.dataIndex];
return [
`Engagements: ${context.raw}`,
`Students: ${formatStudentCounts(term)}`
];
}
}
}
}
}
});

// Line chart
if (lineChart) lineChart.destroy();

lineChart = new Chart(lineCanvas, {
type: "line",
data: {
labels,
datasets: [
{
label: "Engagement by Term",
data: termEngagements,
fill: false
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: baseScales,
plugins: {
title: {
display: true,
text: "CCE Engagements Trends over the Terms",
font: { size: 18 }
},
tooltip: {
callbacks: {
label: (context) => {
const term = labels[context.dataIndex];
return [
`Engagements: ${context.raw}`,
`Students: ${formatStudentCounts(term)}`
];
}
}
}
}
}
});

// Toggle handlers
const showBarChart = () => {
$barChart.show();
$lineChart.hide();
setTimeout(() => barChart?.resize(), 0);
};
const showLineChart = () => {
$barChart.hide();
$lineChart.show();
setTimeout(() => lineChart?.resize(), 0);
};

$("#chartButton").off("click").on("click", showBarChart);
$("#lineButton").off("click").on("click", showLineChart);
showBarChart();
}
});
});
$("#cceDownload").on("click", function(selected, fileName = "cceMinorChart.png"){
const element = $(".ccePrint")[0];
html2canvas(element).then(canvas => {
const downloadLink = document.createElement('a');
downloadLink.href = canvas.toDataURL();
downloadLink.download = fileName;
downloadLink.click();
})
})
})


function emailMinorCandidates(studentEmails){
// If there are any students interested or declared, open the mailto link
if (studentEmails.length) {
Expand All @@ -88,7 +282,6 @@ function emailAll(){
emailMinorCandidates(allMinorCandidateEmails);
}


function getInterestedStudents() {
// get all the checkboxes and return a list of users who's
// checkboxes are selected
Expand Down
33 changes: 30 additions & 3 deletions app/templates/admin/cceMinor.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

{% block scripts %}
{{super()}}
<script type="module" src="/static/js/minorAdminPage.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.js"></script>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script type="module" src="/static/js/minorAdminPage.js"></script>
{% block styles %}
{{super()}}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/jquery.dataTables.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
{% endblock %}
{% endblock %}
{% block app_content %}
Expand All @@ -20,6 +22,32 @@ <h2>CCE Minor Progress</h2>
</div>
<div class="col-auto">
<a class="btn btn-success" role="button" href="/admin/cceMinor/download">Download Report</a>
<button type="button" id="cceMinor" data-username="{{ adminUsername }}" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#cceChart">
View Chart
</button>
</div>
</div>


<!-- ################# Modal for CCE Minor Graph ################# -->
<div class="modal fade" id="cceChart" tabindex="-1" aria-labelledby="CCEMinorChart" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div>
<div class="d-flex justify-content-around">
<button type="button " class="btn m-3 btn-primary" id="chartButton">Engagement Bar Chart</button>
<button type="button" class="btn m-3 btn-primary" id="lineButton">Engagement Line Chart</button>
</div>
<div class="modal-body ccePrint p-2">
<canvas id="cceChartByEngagement" width="400" height="400"></canvas>
<canvas id="cceChartByTerm" width="400" height="300" style="display:none;"></canvas>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary me-auto" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" id="cceDownload">Download Chart</button>
</div>
</div>
</div>
</div>
<table class="display" id="engagedStudentsTable">
Expand Down Expand Up @@ -108,7 +136,6 @@ <h2>CCE Minor Candidates</h2>
</div>

<!-- ################# Modal ################# -->

<div class="modal fade" id="addInterestedStudentsModal" tabindex="-1" aria-labelledby="addInterestedStudentsLbl" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
Expand Down