feat(v1.4.0): CSV/Excel/JSON/NextUp exports + hybrid smart year standing
- Export modules (export-csv/json/xlsx) mirroring the PDF layout, plus a Next Up import CSV matching the raycast/nextup column contract. - Hybrid Year Level: View Grades history (exact per-semester standing), falling back to units math (prospectus total - lacking), then profile. Popup adds a year-mode setting (auto/custom/off). - On-page button row now offers PDF / CSV / Excel / JSON / Next Up. - Bump version to 1.4.0. - Tag-only build workflow with an optional AMO sign step for a signed .xpi; separate ci.yml keeps tests on pushes/PRs. - Tests: year-standing parser tests + export tests (CSV quoting, JSON schema, XLSX structure); CI validates XLSX via openpyxl.
This commit is contained in:
+109
-13
@@ -141,9 +141,69 @@
|
||||
? 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. View Grades history matching.
|
||||
const gradesHtml = await fetchText('/ViewGrades');
|
||||
if (gradesHtml) {
|
||||
const history = ISMIStools.parseGradeHistory(gradesHtml);
|
||||
const level = ISMIStools.yearLevelForSemester(history, yearNum);
|
||||
if (level != null) return String(level);
|
||||
}
|
||||
|
||||
// 2. Units math: prospectus total minus lacking units.
|
||||
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);
|
||||
}
|
||||
|
||||
// 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';
|
||||
|
||||
@@ -169,22 +229,12 @@
|
||||
try {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Downloading...';
|
||||
const { header, courses, period: selectedPeriod } = await scrapeStudyLoad();
|
||||
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', 'customYearLevel']);
|
||||
// Resolve the Year Level: custom value wins; otherwise prefer the scraped
|
||||
// value, smart-adjusted (minus one) during SUMMER when the year level has
|
||||
// not yet advanced.
|
||||
const settings = await browser.storage.local.get(['truncateDescription', 'showYearLevel']);
|
||||
header.showYearLevel = settings.showYearLevel !== false;
|
||||
const custom = String(settings.customYearLevel || '').trim();
|
||||
if (custom) {
|
||||
header.yearLevel = custom;
|
||||
} else if (/summer/i.test(selectedPeriod || '')) {
|
||||
const n = parseInt(header.yearLevel, 10);
|
||||
if (Number.isFinite(n) && n > 1) header.yearLevel = String(n - 1);
|
||||
}
|
||||
const res = await browser.runtime.sendMessage({
|
||||
type: 'generate-pdf',
|
||||
data: { header, courses: included, totalUnits, truncateDescription: settings.truncateDescription !== false }
|
||||
@@ -199,6 +249,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -209,6 +291,7 @@
|
||||
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;
|
||||
@@ -217,8 +300,21 @@
|
||||
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(btn);
|
||||
wrapper.appendChild(btnGroup);
|
||||
wrapper.appendChild(unitsP);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user