The grade-history method counted distinct academic years, which over-advanced standing for students who repeated/failed a year (e.g. showing Year 5 when the student is genuinely Year 4, since a partial 2025 year didn't advance them). Reorder the hybrid so the UNITS math (total prospectus units minus still-lacking units = units actually passed) is primary, and View Grades history is only a fallback when units data is unavailable.
349 lines
14 KiB
JavaScript
349 lines
14 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
// Cross-browser API namespace (see background.js). No-op in Firefox.
|
|
if (typeof browser === 'undefined' || browser == null) {
|
|
var browser = (typeof chrome !== 'undefined' ? chrome : {});
|
|
}
|
|
|
|
const MASTER_KEY = 'enabled';
|
|
|
|
async function isEnabled() {
|
|
const { enabled } = await browser.storage.local.get(MASTER_KEY);
|
|
return enabled !== false;
|
|
}
|
|
|
|
function autoAcceptPrivacy() {
|
|
const modal = document.getElementById('myModal');
|
|
if (!modal) return;
|
|
const checkbox = document.getElementById('Confirmation');
|
|
const submit = document.getElementById('Submit');
|
|
if (checkbox && !checkbox.checked) checkbox.click();
|
|
if (submit) submit.click();
|
|
}
|
|
|
|
function watchPrivacyModal() {
|
|
autoAcceptPrivacy();
|
|
const observer = new MutationObserver(() => autoAcceptPrivacy());
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
setTimeout(() => observer.disconnect(), 30000);
|
|
}
|
|
|
|
async function autoLogin() {
|
|
const { profiles } = await browser.storage.local.get('profiles');
|
|
const active = (profiles || []).find((p) => p.enabled);
|
|
if (!active) return;
|
|
const userField = document.querySelector(
|
|
'input[name="Username"], input[id="Username"], input[name="UserName"], input[name="username"], input[id="UserName"], input[id="username"]'
|
|
);
|
|
const passField = document.querySelector('input[type="password"]');
|
|
if (!userField || !passField) return;
|
|
userField.value = active.username;
|
|
passField.value = active.password;
|
|
userField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
passField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
const form = userField.closest('form') || passField.closest('form');
|
|
if (!form) return;
|
|
setTimeout(() => {
|
|
const submitBtn = form.querySelector('input[type="submit"], button[type="submit"]');
|
|
if (submitBtn) submitBtn.click();
|
|
else form.submit();
|
|
}, 300);
|
|
}
|
|
|
|
async function fetchCourseDescription(courseCode, academicPeriod, academicYear, token) {
|
|
const body = new URLSearchParams();
|
|
body.set('Courses', courseCode);
|
|
body.set('AcademicPeriod', academicPeriod);
|
|
body.set('AcademicYear', academicYear);
|
|
body.set('AjaxFormVM.Controller', 'CourseSchedule');
|
|
body.set('AjaxFormVM.Action', 'CourseScheduleOffered');
|
|
body.set('AjaxFormVM.ResultTargetId', 'CourseScheduleOfferedList');
|
|
body.set('AjaxFormVM.LinkToReturn', '');
|
|
body.set('AjaxFormVM.LinkToReturnTargetId', '');
|
|
body.set('AjaxFormVM.FormHeader', 'Choose term to view courses offered');
|
|
body.set('AjaxFormVM.FormHeaderIcon', '');
|
|
body.set('AjaxFormVM.FormId', 'CourseScheduleCourseScheduleOffered');
|
|
body.set('AjaxFormVM.ShowCloseButton', 'True');
|
|
body.set('AjaxFormVM.PreventAutoUpdateTarget', 'True');
|
|
body.set('AjaxFormVM.CloseModalAfterSubmit', 'False');
|
|
body.set('AjaxFormVM.AjaxFormType', 'Search');
|
|
body.set('AjaxFormVM.FormNoteLocation', '');
|
|
body.set('AjaxFormVM.SubmitButton.FaIcon', '');
|
|
body.set('AjaxFormVM.SubmitButton.ButtonText', '');
|
|
body.set('AjaxFormVM.SubmitButton.ButtonSize', '');
|
|
body.set('__RequestVerificationToken', token);
|
|
const resp = await fetch('/CourseSchedule/CourseScheduleOffered?Length=14', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: body.toString(),
|
|
credentials: 'same-origin'
|
|
});
|
|
if (!resp.ok) throw new Error('Offered courses request failed: ' + resp.status);
|
|
const html = await resp.text();
|
|
const courses = ISMIStools.parseOfferedCourses(html);
|
|
const match = courses.find((c) => c.courseCode === courseCode);
|
|
return match ? match.description : '';
|
|
}
|
|
|
|
function extractTotalUnits(text) {
|
|
const m = /Total Units:\s*([\d.]+)/i.exec(text);
|
|
return m ? m[1] : '';
|
|
}
|
|
|
|
async function scrapeStudyLoad() {
|
|
const table = document.querySelector('table.table-forum');
|
|
if (!table) throw new Error('No study load table found on this page.');
|
|
const courses = ISMIStools.parseStudyLoadTable(table.outerHTML);
|
|
if (!courses.length) throw new Error('No courses found in study load table.');
|
|
|
|
const periodEl = document.getElementById('AcademicPeriod');
|
|
const yearEl = document.getElementById('AcademicYear');
|
|
const tokenEl = document.querySelector('input[name="__RequestVerificationToken"]');
|
|
const academicPeriod = periodEl ? periodEl.value : '';
|
|
const academicYear = yearEl ? yearEl.value : '';
|
|
const token = tokenEl ? tokenEl.value : '';
|
|
|
|
// Semester/year come from the search dropdowns the user picked.
|
|
// AcademicPeriod's selected option label (e.g. "1ST Semester") + year.
|
|
const periodLabel = (periodEl && periodEl.selectedOptions && periodEl.selectedOptions[0])
|
|
? periodEl.selectedOptions[0].textContent.trim()
|
|
: '';
|
|
const selectedPeriod = periodLabel || academicPeriod;
|
|
const selectedYear = academicYear;
|
|
|
|
const uniqueCodes = [...new Set(courses.map((c) => c.courseCode))];
|
|
const descMap = {};
|
|
for (const code of uniqueCodes) {
|
|
try {
|
|
descMap[code] = await fetchCourseDescription(code, academicPeriod, academicYear, token);
|
|
} catch (e) {
|
|
descMap[code] = '';
|
|
}
|
|
}
|
|
courses.forEach((c) => { c.description = descMap[c.courseCode] || ''; });
|
|
|
|
let header = {};
|
|
try {
|
|
const resp = await fetch('/Manage/Student', { credentials: 'same-origin' });
|
|
if (!resp.ok) throw new Error('Profile request failed: ' + resp.status);
|
|
const html = await resp.text();
|
|
header = ISMIStools.parseProfilePage(html);
|
|
} catch (e) {
|
|
header = {};
|
|
}
|
|
|
|
// Semester goes into the header so the PDF shows the dropdown's term/year.
|
|
header.semester = (selectedPeriod && selectedYear)
|
|
? selectedPeriod + ' ' + selectedYear
|
|
: (header.semester || '');
|
|
|
|
// Resolve the Year Level for the PRINTED semester (hybrid, see design
|
|
// spec): custom setting wins; else View Grades history matching; else the
|
|
// units math; else the profile-scraped value.
|
|
header.yearLevel = await resolveYearLevel(header, selectedPeriod, selectedYear);
|
|
|
|
return { header, courses, period: selectedPeriod, year: selectedYear };
|
|
}
|
|
|
|
// Fetch a same-origin page and return its text, or null on failure.
|
|
async function fetchText(path) {
|
|
try {
|
|
const resp = await fetch(path, { credentials: 'same-origin' });
|
|
if (!resp.ok) return null;
|
|
return await resp.text();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Hybrid year-level resolution. Returns null only if every source failed.
|
|
// mode: 'auto' (default) runs the full hybrid; 'custom' uses only the custom
|
|
// value; 'off' uses only the profile-scraped value.
|
|
async function resolveYearLevel(header, period, year) {
|
|
const settings = await browser.storage.local.get(['yearMode', 'customYearLevel']);
|
|
const mode = settings.yearMode || 'auto';
|
|
const custom = String(settings.customYearLevel || '').trim();
|
|
|
|
if (mode === 'off') {
|
|
return header.yearLevel ? String(header.yearLevel) : null;
|
|
}
|
|
if (mode === 'custom') {
|
|
return custom ? custom : null;
|
|
}
|
|
// auto: custom value wins, then the hybrid sources.
|
|
if (custom) return custom;
|
|
|
|
const yearNum = parseInt(year, 10);
|
|
|
|
// 1. UNITS MATH — primary. Standing from units actually passed:
|
|
// total prospectus units minus still-lacking units. This reflects real
|
|
// earned units and is correct even for students who repeated or failed
|
|
// a year (the naive grade-history count would over-advance them).
|
|
const [prospectusHtml, lackingHtml] = await Promise.all([
|
|
fetchText('/prospectus/StudentProspectus'),
|
|
fetchText('/Extras/ViewLackingSubjectsStudent1')
|
|
]);
|
|
if (prospectusHtml) {
|
|
const yearUnits = ISMIStools.parseProspectusYearUnits(prospectusHtml);
|
|
const lacking = lackingHtml ? ISMIStools.parseLackingSubjectsUnits(lackingHtml) : 0;
|
|
const level = ISMIStools.computeYearStandingFromUnits(yearUnits, lacking);
|
|
if (level != null) return String(level);
|
|
}
|
|
|
|
// 2. GRADE HISTORY — fallback only when the units path is unavailable.
|
|
// It counts distinct academic years, so it is only an approximation for
|
|
// the printed semester and is not used when units data is present.
|
|
const gradesHtml = await fetchText('/ViewGrades');
|
|
if (gradesHtml) {
|
|
const history = ISMIStools.parseGradeHistory(gradesHtml);
|
|
const level = ISMIStools.yearLevelForSemester(history, yearNum);
|
|
if (level != null) return String(level);
|
|
}
|
|
|
|
// 3. Profile-scraped value as a last resort.
|
|
if (header.yearLevel) return String(header.yearLevel);
|
|
return null;
|
|
}
|
|
|
|
// ---- On-page Download PDF button ----
|
|
const BTN_ID = 'ismisHelperDownloadBtn';
|
|
|
|
function showToast(msg, isError) {
|
|
if (window.toastr) {
|
|
if (isError) window.toastr.error(msg);
|
|
else window.toastr.success(msg);
|
|
} else if (isError) {
|
|
alert(msg);
|
|
}
|
|
}
|
|
|
|
function findTotalUnitsP() {
|
|
const ps = document.querySelectorAll('#StudyLoad p');
|
|
for (const p of ps) {
|
|
if (/Total Units/i.test(p.textContent || '')) return p;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function onDownloadPdf() {
|
|
const btn = document.getElementById(BTN_ID);
|
|
try {
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Downloading...';
|
|
const { header, courses } = await scrapeStudyLoad();
|
|
const included = ISMIStools.filterStudyLoadCourses(courses);
|
|
if (!included.length) throw new Error('No enrolled courses found.');
|
|
const totalUnits = ISMIStools.sumUnits(included);
|
|
const settings = await browser.storage.local.get(['truncateDescription', 'showYearLevel']);
|
|
header.showYearLevel = settings.showYearLevel !== false;
|
|
const res = await browser.runtime.sendMessage({
|
|
type: 'generate-pdf',
|
|
data: { header, courses: included, totalUnits, truncateDescription: settings.truncateDescription !== false }
|
|
});
|
|
if (!res || !res.ok) throw new Error((res && res.error) || 'PDF generation failed');
|
|
showToast('PDF downloaded.');
|
|
} catch (err) {
|
|
showToast('Error: ' + ((err && err.message) || err), true);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fa fa-download"></i> Download PDF';
|
|
}
|
|
}
|
|
|
|
// Export another format (csv/json/xlsx/nextup). Reuses scrapeStudyLoad so
|
|
// every format gets the same header/courses/totalUnits payload and the same
|
|
// hybrid year-level resolution.
|
|
async function onExport(format) {
|
|
try {
|
|
const { header, courses, period, year } = await scrapeStudyLoad();
|
|
const included = ISMIStools.filterStudyLoadCourses(courses);
|
|
if (!included.length) throw new Error('No enrolled courses found.');
|
|
const totalUnits = ISMIStools.sumUnits(included);
|
|
const res = await browser.runtime.sendMessage({
|
|
type: 'export',
|
|
format,
|
|
data: { header, courses: included, totalUnits, period, year }
|
|
});
|
|
if (!res || !res.ok) throw new Error((res && res.error) || 'Export failed');
|
|
showToast('Exported ' + format.toUpperCase() + '.');
|
|
} catch (err) {
|
|
showToast('Error: ' + ((err && err.message) || err), true);
|
|
}
|
|
}
|
|
|
|
function makeExportButton(id, label, icon, format) {
|
|
const b = document.createElement('button');
|
|
b.id = id;
|
|
b.type = 'button';
|
|
b.className = 'btn green';
|
|
b.style.marginLeft = '6px';
|
|
b.innerHTML = '<i class="' + icon + '"></i> ' + label;
|
|
b.addEventListener('click', () => onExport(format));
|
|
return b;
|
|
}
|
|
|
|
function injectDownloadButton() {
|
|
if (document.getElementById(BTN_ID)) return;
|
|
const unitsP = findTotalUnitsP();
|
|
if (!unitsP) return;
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.style.display = 'flex';
|
|
wrapper.style.alignItems = 'center';
|
|
wrapper.style.justifyContent = 'space-between';
|
|
wrapper.style.margin = '10px 0';
|
|
wrapper.style.flexWrap = 'wrap';
|
|
|
|
const btn = document.createElement('button');
|
|
btn.id = BTN_ID;
|
|
btn.type = 'button';
|
|
btn.className = 'btn green';
|
|
btn.innerHTML = '<i class="fa fa-download"></i> Download PDF';
|
|
btn.addEventListener('click', onDownloadPdf);
|
|
|
|
const btnCsv = makeExportButton('ismisHelperExportCsvBtn', 'CSV', 'fa fa-file-text-o', 'csv');
|
|
const btnXlsx = makeExportButton('ismisHelperExportXlsxBtn', 'Excel', 'fa fa-file-o', 'xlsx');
|
|
const btnJson = makeExportButton('ismisHelperExportJsonBtn', 'JSON', 'fa fa-file-code-o', 'json');
|
|
const btnNextUp = makeExportButton('ismisHelperExportNextUpBtn', 'Next Up', 'fa fa-calendar', 'nextup');
|
|
|
|
const btnGroup = document.createElement('div');
|
|
btnGroup.style.display = 'flex';
|
|
btnGroup.appendChild(btn);
|
|
btnGroup.appendChild(btnCsv);
|
|
btnGroup.appendChild(btnXlsx);
|
|
btnGroup.appendChild(btnJson);
|
|
btnGroup.appendChild(btnNextUp);
|
|
|
|
unitsP.parentNode.insertBefore(wrapper, unitsP);
|
|
wrapper.appendChild(btnGroup);
|
|
wrapper.appendChild(unitsP);
|
|
}
|
|
|
|
function tryInjectButton() {
|
|
if (document.getElementById(BTN_ID)) return;
|
|
if (document.querySelector('#StudyLoad table.table-forum')) injectDownloadButton();
|
|
}
|
|
|
|
function watchStudyLoad() {
|
|
tryInjectButton();
|
|
const observer = new MutationObserver((mutations) => {
|
|
const inStudyLoad = mutations.some(
|
|
(m) => m.target && m.target.closest && m.target.closest('#StudyLoad')
|
|
);
|
|
if (inStudyLoad) tryInjectButton();
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
|
|
(async function init() {
|
|
if (!(await isEnabled())) return;
|
|
watchPrivacyModal();
|
|
if (/\/Account\/Login/i.test(location.href)) autoLogin();
|
|
if (document.getElementById('StudyLoad')) watchStudyLoad();
|
|
})();
|
|
})();
|