Skip to content

Make suggested Changes to exam level dashboard for exam board#776

Open
Karthikeyantestpress wants to merge 7 commits intomainfrom
excel-changes
Open

Make suggested Changes to exam level dashboard for exam board#776
Karthikeyantestpress wants to merge 7 commits intomainfrom
excel-changes

Conversation

@Karthikeyantestpress
Copy link
Copy Markdown
Contributor

Add appeared data in overall stats and exam center (appeared is for students actually submitted the test from present students)
Add filter by location (should filter by state and city)
Add dummy excel export (sample data)
Distinguishing Overall stats panel and exam center table panel with distinct colors

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the exam level dashboard by introducing a new 'Appeared' metric for student test submissions, alongside robust location-based filtering for exam centers. It also adds a foundational Excel export feature for comprehensive data analysis and makes minor UI adjustments to integrate these new functionalities seamlessly.

Highlights

  • New 'Appeared' Metric: Introduced a new 'Appeared' metric in the exam center dashboard, reflecting students who submitted the test. This includes adding it to the data model, table display, and overall statistics.
  • Location-Based Filtering: Implemented filtering capabilities for exam centers by state and city, enhancing data navigation.
  • Excel Export Functionality: Added a basic Excel export feature for dashboard data, including both summary statistics and detailed exam center information.
  • Dashboard UI Enhancements: Adjusted the layout of the overall stats panel to accommodate the new 'Appeared' metric and improved the filter section's responsiveness.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/testpress/exam_board/exam_dashboard/centers_table.html
    • Updated dummy exam center data to include 'city' and 'submitted' fields.
    • Added logic to filter exam center rows by 'state' and 'city' based on an Alpine store.
    • Extended the numeric sorting keys to include the new 'submitted' field.
    • Integrated Alpine.js x-init to synchronize 'sortedRows' with the 'examExport' store for export functionality.
    • Modified the filter section's flex container to allow 'flex-wrap' for better responsiveness.
    • Introduced a new sortable table header column for 'Appeared' (submitted count).
    • Added a table data cell to display the 'submitted' count for each exam center.
  • src/testpress/exam_board/exam_dashboard/header.html
    • Implemented a new dropdown filter for location, allowing selection by state and city, and storing selections in an Alpine store.
    • Assigned an id="exam-export-excel" to the export button.
    • Added JavaScript to handle the click event of the export button, generating an Excel file using XLSX.utils, including hardcoded pipeline and exam center data for the export.
    • Introduced a new 'Appeared' section in the overall pipeline statistics, complete with a doughnut chart placeholder and percentage/count display.
    • Adjusted the grid layout for overall stats from xl:grid-cols-6 to xl:grid-cols-7 to accommodate the new 'Appeared' stat.
    • Stored the 'pipeline' data in the 'examExport' Alpine store.
  • src/testpress/exam_board/exam_dashboard/index.html
    • Included the xlsx.full.min.js library from a CDN for Excel export capabilities.
    • Initialized two Alpine.js stores: 'examLocationFilter' to manage state and city filter selections, and 'examExport' to hold data ('rows' and 'pipeline') intended for Excel export.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several new features to the exam dashboard, including filtering by location, an 'appeared' status, and a dummy Excel export. The changes are functional but there are several areas for improvement regarding code quality, maintainability, and security. Specifically, there's a security concern with using a CDN for a third-party script. There are also multiple instances of code duplication and verbose logic, particularly in the Alpine.js components and the new export script. Refactoring these areas to use Alpine's $store magic property, extracting repeated logic into helper functions, and sourcing data from a single source of truth will greatly improve the codebase.

Comment on lines +850 to +1010
exportBtn.addEventListener('click', function () {
if (typeof XLSX === 'undefined') return;

// Hard-coded data for design phase (not from DOM/Alpine)
var pipeline = [
{ label: 'Scheduled', completed: 500 },
{ label: 'Registered', completed: 450 },
{ label: 'Biometrics', completed: 380 },
{ label: 'Photo', completed: 350 },
{ label: 'Present', completed: 320 },
{ label: 'Appeared', completed: 300 },
{ label: 'Absent', completed: 100 }
];

var rows = [
{
name: 'Downtown Test Center',
state: 'Tamil Nadu',
city: 'Chennai',
venue: 'Chennai Central Hall',
address: '123 Main St, Chennai',
contactName: 'Raj Kumar',
contactPhone: '+91 98765 43210',
sessionCount: 4,
capacity: 900,
allocated: 832,
registered: 800,
biometrics: 780,
photos: 765,
present: 768,
submitted: 750,
absent: 64,
online: true,
clientNodes: '10/10',
battery: 85
},
{
name: 'North Campus Hub',
state: 'Karnataka',
city: 'Bangalore',
venue: 'Bangalore North Campus',
address: '45 College Rd, Bangalore',
contactName: 'Priya S.',
contactPhone: '+91 87654 32109',
sessionCount: 3,
capacity: 700,
allocated: 650,
registered: 620,
biometrics: 610,
photos: 600,
present: 598,
submitted: 590,
absent: 52,
online: true,
clientNodes: '10/10',
battery: 72
},
{
name: 'South Zone Centre',
state: 'Telangana',
city: 'Hyderabad',
venue: 'Hyderabad South Block',
address: '78 MG Rd, Hyderabad',
contactName: 'Anil M.',
contactPhone: '+91 76543 21098',
sessionCount: 2,
capacity: 500,
allocated: 450,
registered: 0,
biometrics: 0,
photos: 0,
present: 0,
submitted: 0,
absent: 0,
online: false,
clientNodes: '0/10',
battery: 5
}
];

var now = new Date();
var tsLabel = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
var tsFile = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + '-' + String(now.getHours()).padStart(2, '0') + '-' + String(now.getMinutes()).padStart(2, '0');
var totalCandidates = 600;
var centersHeaders = ['Center Name', 'State', 'City', 'Venue', 'Address', 'Contact Name', 'Contact Phone', 'Sessions', 'Capacity', 'Scheduled', 'Registered', 'Biometrics', 'Photos', 'Present', 'Appeared', 'Absent', 'Health', 'Client Nodes', 'Battery'];
var summaryAoa = [
['Exam', 'ACCP Professional Certification Exam'],
['Date & Time', 'Feb 12, 2026 · 9:00 AM to 12:00 PM'],
['Code', 'EX-2026-001'],
['Exported at', tsLabel],
[],
['Overall stats', 'Value']
];
pipeline.forEach(function (item) {
var completed = item.completed != null ? item.completed : 0;
var percent = totalCandidates ? Math.round((completed / totalCandidates) * 100) : 0;
var valueLabel = percent + '% (' + completed + '/' + totalCandidates + ')';
summaryAoa.push([item.label, valueLabel]);
});
summaryAoa.push([]);
summaryAoa.push(['Exam center details']);
summaryAoa.push(centersHeaders);
rows.forEach(function (row) {
summaryAoa.push([
row.name || '',
row.state || '',
row.city || '',
row.venue || '',
row.address || '',
row.contactName || '',
row.contactPhone || '',
row.sessionCount != null ? row.sessionCount : '',
row.capacity != null ? row.capacity : '',
row.allocated != null ? row.allocated : '',
row.registered != null ? row.registered : '',
row.biometrics != null ? row.biometrics : '',
row.photos != null ? row.photos : '',
row.present != null ? row.present : '',
row.submitted != null ? row.submitted : '',
row.absent != null ? row.absent : '',
row.online === true ? 'Online' : (row.online === false ? 'Offline' : ''),
row.clientNodes || '',
row.battery != null ? row.battery + '%' : ''
]);
});
var summarySheet = XLSX.utils.aoa_to_sheet(summaryAoa);
var centersAoa = [
['Exam center details'],
['Exported at', tsLabel],
[],
centersHeaders
];
rows.forEach(function (row) {
centersAoa.push([
row.name || '',
row.state || '',
row.city || '',
row.venue || '',
row.address || '',
row.contactName || '',
row.contactPhone || '',
row.sessionCount != null ? row.sessionCount : '',
row.capacity != null ? row.capacity : '',
row.allocated != null ? row.allocated : '',
row.registered != null ? row.registered : '',
row.biometrics != null ? row.biometrics : '',
row.photos != null ? row.photos : '',
row.present != null ? row.present : '',
row.submitted != null ? row.submitted : '',
row.absent != null ? row.absent : '',
row.online === true ? 'Online' : (row.online === false ? 'Offline' : ''),
row.clientNodes || '',
row.battery != null ? row.battery + '%' : ''
]);
});
var centersSheet = XLSX.utils.aoa_to_sheet(centersAoa);
var wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, summarySheet, 'Summary');
XLSX.utils.book_append_sheet(wb, centersSheet, 'Centers');
XLSX.writeFile(wb, 'exam-dashboard-export-' + tsFile + '.xlsx');
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Excel export functionality has a few issues:

  1. Hardcoded Data: It uses hardcoded pipeline and rows data instead of sourcing it from the examExport Alpine store. This means the exported file will not reflect any filtering or sorting applied by the user.
  2. Code Duplication: The logic to map a row object to an array for the Excel sheet is duplicated for both the 'Summary' and 'Centers' sheets. This violates the DRY (Don't Repeat Yourself) principle.

This entire block should be refactored to use the data from Alpine.store('examExport') and to extract the duplicated row processing logic into a helper function.

{% block body_class %}overflow-y-scroll min-h-screen bg-gray-100 dark:bg-neutral-900{% endblock body_class %}

{% block content %}
<script src="https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js"></script>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Loading external scripts from a CDN can introduce security vulnerabilities (if the CDN is compromised) and reliability issues (if the CDN is unavailable). It is recommended to manage third-party libraries like SheetJS (xlsx) as part of your project's dependencies using a package manager (like npm or yarn) and bundle them with your application's assets.

Comment on lines +39 to +43
const loc = typeof Alpine !== 'undefined' && Alpine.store && Alpine.store('examLocationFilter')
if (loc) {
if (loc.state && loc.state !== 'All') list = list.filter(r => r.state === loc.state)
if (loc.city && loc.city !== 'All') list = list.filter(r => r.city === loc.city)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The filteredRows getter uses a verbose check typeof Alpine !== 'undefined' && Alpine.store && ... to access the Alpine store. This can be simplified by using the $store magic property within the component, which improves readability and conciseness.

Suggested change
const loc = typeof Alpine !== 'undefined' && Alpine.store && Alpine.store('examLocationFilter')
if (loc) {
if (loc.state && loc.state !== 'All') list = list.filter(r => r.state === loc.state)
if (loc.city && loc.city !== 'All') list = list.filter(r => r.city === loc.city)
}
const loc = this.$store.examLocationFilter;
if (loc) {
if (loc.state && loc.state !== 'All') list = list.filter(r => r.state === loc.state)
if (loc.city && loc.city !== 'All') list = list.filter(r => r.city === loc.city)
}

})
}
}"
x-init="$watch('sortedRows', function(v) { var s = typeof Alpine !== 'undefined' && Alpine.store && Alpine.store('examExport'); if (s) s.rows = v || []; }); $nextTick(function() { var s = typeof Alpine !== 'undefined' && Alpine.store && Alpine.store('examExport'); if (s) s.rows = $data.sortedRows || []; })"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The x-init attribute contains a long and hard-to-read line of JavaScript. To improve readability and maintainability, this logic should be extracted into a method within the x-data object.

For example, you could add a method to x-data:

updateExportStore() {
  const store = Alpine.store('examExport');
  if (store) {
    store.rows = this.sortedRows || [];
  }
}

And then simplify x-init:

x-init="$watch('sortedRows', () => updateExportStore()); $nextTick(() => updateExportStore())"

Comment on lines +29 to +42
get locationLabel() {
var s = typeof Alpine !== 'undefined' && Alpine.store && Alpine.store('examLocationFilter');
if (!s || !s.state || s.state === 'All') return 'Filter by location';
return (s.city && s.city !== 'All') ? (s.state + ' – ' + s.city) : s.state;
},
applyLocationFilter() {
var store = typeof Alpine !== 'undefined' && Alpine.store && Alpine.store('examLocationFilter');
var stateEl = document.getElementById('exam-location-state');
var cityEl = document.getElementById('exam-location-city');
if (store && stateEl && cityEl) {
store.state = stateEl.value || '';
store.city = cityEl.value || '';
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The component repeatedly uses verbose checks like typeof Alpine !== 'undefined' && Alpine.store && ... to access the Alpine store. Within an Alpine component, you can access stores more cleanly using the $store magic property. This will make the code more concise and easier to read.

Suggested change
get locationLabel() {
var s = typeof Alpine !== 'undefined' && Alpine.store && Alpine.store('examLocationFilter');
if (!s || !s.state || s.state === 'All') return 'Filter by location';
return (s.city && s.city !== 'All') ? (s.state + ' – ' + s.city) : s.state;
},
applyLocationFilter() {
var store = typeof Alpine !== 'undefined' && Alpine.store && Alpine.store('examLocationFilter');
var stateEl = document.getElementById('exam-location-state');
var cityEl = document.getElementById('exam-location-city');
if (store && stateEl && cityEl) {
store.state = stateEl.value || '';
store.city = cityEl.value || '';
}
}
get locationLabel() {
const s = this.$store.examLocationFilter;
if (!s || !s.state || s.state === 'All') return 'Filter by location';
return (s.city && s.city !== 'All') ? `${s.state} – ${s.city}` : s.state;
},
applyLocationFilter() {
const store = this.$store.examLocationFilter;
const stateEl = document.getElementById('exam-location-state');
const cityEl = document.getElementById('exam-location-city');
if (store && stateEl && cityEl) {
store.state = stateEl.value || '';
store.city = cityEl.value || '';
}
}

Comment on lines +15 to +22
document.addEventListener('alpine:init', function () {
if (typeof Alpine !== 'undefined' && !Alpine.store('examLocationFilter')) {
Alpine.store('examLocationFilter', { state: '', city: '' });
}
if (typeof Alpine !== 'undefined' && !Alpine.store('examExport')) {
Alpine.store('examExport', { rows: [], pipeline: [] });
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The typeof Alpine !== 'undefined' check is redundant inside an alpine:init event listener, as the Alpine object is guaranteed to be available in this context. You can simplify the code for better readability.

document.addEventListener('alpine:init', function () {
  if (!Alpine.store('examLocationFilter')) {
    Alpine.store('examLocationFilter', { state: '', city: '' });
  }
  if (!Alpine.store('examExport')) {
    Alpine.store('examExport', { rows: [], pipeline: [] });
  }
});

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 2, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://testpress.github.io/design/pr-preview/pr-776/

Built to branch gh-pages at 2026-03-02 09:59 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant