7878 */
7979class questions_json_generator {
8080 private $ context ;
81+ private $ usagemap ;
8182
8283 public function __construct ($ context ) {
8384 $ this ->context = $ context ;
@@ -88,6 +89,10 @@ public function __construct($context) {
8889 */
8990 public function generate_questions_data () {
9091 $ questions = bulk_tester::get_all_coderunner_questions_in_context ($ this ->context ->id , false );
92+
93+ // Fetch quiz usage for all questions in one bulk query.
94+ $ this ->usagemap = $ this ->fetch_quiz_usage_bulk ($ questions );
95+
9196 $ enhancedquestions = [];
9297
9398 foreach ($ questions as $ question ) {
@@ -98,13 +103,61 @@ public function generate_questions_data() {
98103 return $ enhancedquestions ;
99104 }
100105
106+ /**
107+ * Fetch quiz usage for all questions in a single query.
108+ */
109+ private function fetch_quiz_usage_bulk ($ questions ) {
110+ global $ DB ;
111+
112+ if (empty ($ questions )) {
113+ return [];
114+ }
115+
116+ $ questionids = array_column ($ questions , 'id ' );
117+
118+ if (empty ($ questionids )) {
119+ return [];
120+ }
121+
122+ // Build the query to get quiz usage for all questions.
123+ // This combines question_references (direct usage) and question_attempts (random question usage).
124+ [$ insql , $ params ] = $ DB ->get_in_or_equal ($ questionids , SQL_PARAMS_NAMED );
125+
126+ $ sql = "SELECT CONCAT(qv.questionid, '-', qz.id) as uniqueid,
127+ qv.questionid, qz.id as quizid, qz.name as quizname
128+ FROM {question_versions} qv
129+ JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
130+ JOIN {question_references} qr ON qr.questionbankentryid = qbe.id
131+ JOIN {quiz_slots} slot ON slot.id = qr.itemid
132+ JOIN {quiz} qz ON qz.id = slot.quizid
133+ WHERE qv.questionid $ insql
134+ AND qr.component = 'mod_quiz'
135+ AND qr.questionarea = 'slot'
136+ GROUP BY qv.questionid, qz.id, qz.name
137+ ORDER BY qv.questionid, qz.name " ;
138+
139+ $ usages = $ DB ->get_records_sql ($ sql , $ params );
140+
141+ // Build lookup map: questionid => array of quiz names.
142+ $ usagemap = [];
143+ foreach ($ usages as $ usage ) {
144+ if (!isset ($ usagemap [$ usage ->questionid ])) {
145+ $ usagemap [$ usage ->questionid ] = [];
146+ }
147+ $ usagemap [$ usage ->questionid ][] = $ usage ->quizname ;
148+ }
149+
150+ return $ usagemap ;
151+ }
152+
101153 /**
102154 * Enhance a single question with metadata analysis.
103155 */
104156 private function enhance_question_metadata ($ question ) {
105157 $ courseid = $ this ->get_course_id_from_context ();
106158 $ answer = $ this ->extract_answer ($ question ->answer ?? '' );
107159 $ tags = $ this ->get_question_tags ($ question ->id );
160+ $ usedin = $ this ->usagemap [$ question ->id ] ?? [];
108161
109162 $ enhanced = [
110163 'type ' => 'coderunner ' ,
@@ -118,6 +171,7 @@ private function enhance_question_metadata($question) {
118171 'version ' => (int )$ question ->version ,
119172 'courseid ' => (string )$ courseid ,
120173 'tags ' => $ tags ,
174+ 'usedin ' => $ usedin ,
121175 ];
122176
123177 $ enhanced ['lines_of_code ' ] = $ this ->count_lines_of_code ($ answer );
@@ -580,7 +634,8 @@ private function get_question_tags($questionid) {
580634 name: null,
581635 actions: 280,
582636 category: null,
583- tags: 200
637+ tags: 200,
638+ usedin: 200
584639 };
585640
586641 // Elements.
@@ -619,13 +674,15 @@ function buildHeader() {
619674 const actionsColDef = document.createElement('col');
620675 const categoryColDef = document.createElement('col');
621676 const tagsColDef = document.createElement('col');
622-
677+ const usedinColDef = document.createElement('col');
678+
623679 if (columnWidths.name) nameColDef.style.width = columnWidths.name + 'px';
624680 actionsColDef.style.width = columnWidths.actions + 'px';
625681 if (columnWidths.category) categoryColDef.style.width = columnWidths.category + 'px';
626682 if (columnWidths.tags) tagsColDef.style.width = columnWidths.tags + 'px';
627-
628- colgroup.append(nameColDef, actionsColDef, categoryColDef, tagsColDef);
683+ if (columnWidths.usedin) usedinColDef.style.width = columnWidths.usedin + 'px';
684+
685+ colgroup.append(nameColDef, actionsColDef, categoryColDef, tagsColDef, usedinColDef);
629686 table.appendChild(colgroup);
630687
631688 const thead = document.createElement('thead');
@@ -660,28 +717,39 @@ function buildHeader() {
660717 tagsCol.id = 'sortTags';
661718 tagsCol.style.cursor = 'pointer';
662719 tagsCol.className = 'user-select-none';
720+ tagsCol.style.position = 'relative';
663721 const tagsText = document.createElement('span');
664722 tagsText.textContent = 'Tags ↕';
665723 tagsCol.appendChild(tagsText);
666724
725+ const usedinCol = document.createElement('th');
726+ usedinCol.id = 'sortUsedIn';
727+ usedinCol.style.cursor = 'pointer';
728+ usedinCol.className = 'user-select-none';
729+ usedinCol.style.position = 'relative';
730+ const usedinText = document.createElement('span');
731+ usedinText.textContent = 'Used In ↕';
732+ usedinCol.appendChild(usedinText);
733+
667734 // Add resizers to all columns except the last
668- [nameCol, actionsCol, categoryCol].forEach((col, idx) => {
735+ [nameCol, actionsCol, categoryCol, tagsCol ].forEach((col, idx) => {
669736 const resizer = document.createElement('div');
670737 resizer.className = 'column-resizer';
671738 resizer.dataset.columnIndex = idx;
672-
673- // Stop clicks from bubbling to prevent triggering sort
739+
740+ // Stop propagation to prevent triggering sort on parent <th>
674741 resizer.addEventListener('click', (e) => {
675742 e.stopPropagation();
676743 });
677-
744+
678745 col.appendChild(resizer);
679746 });
680747
681748 headerRow.appendChild(nameCol);
682749 headerRow.appendChild(actionsCol);
683750 headerRow.appendChild(categoryCol);
684751 headerRow.appendChild(tagsCol);
752+ headerRow.appendChild(usedinCol);
685753
686754 thead.appendChild(headerRow);
687755 table.appendChild(thead);
@@ -767,10 +835,31 @@ function summarizeRow(q, idx, tbody){
767835 tagsCell.style.maxWidth = '200px';
768836 tagsCell.title = tagText;
769837
838+ const usedinCell = document.createElement('td');
839+ const usedinArray = Array.isArray(q.usedin) ? q.usedin : [];
840+ usedinCell.className = 'text-muted small';
841+ usedinCell.style.maxWidth = '200px';
842+
843+ // Create one div per quiz name, each with its own truncation
844+ if (usedinArray.length > 0) {
845+ usedinArray.forEach(quizname => {
846+ const quizDiv = document.createElement('div');
847+ quizDiv.textContent = quizname;
848+ quizDiv.className = 'text-truncate';
849+ quizDiv.title = quizname;
850+ usedinCell.appendChild(quizDiv);
851+ });
852+ }
853+
854+ // Tooltip shows full list
855+ const usedinText = usedinArray.join('\n');
856+ usedinCell.title = usedinText;
857+
770858 row.appendChild(nameCell);
771859 row.appendChild(actionsCell);
772860 row.appendChild(categoryCell);
773861 row.appendChild(tagsCell);
862+ row.appendChild(usedinCell);
774863
775864 let openType = null;
776865 let detailRow = null;
@@ -819,7 +908,7 @@ function toggleDisplay(type, content, isHTML = false) {
819908
820909 detailRow = document.createElement('tr');
821910 const detailCell = document.createElement('td');
822- detailCell.colSpan = 4 ;
911+ detailCell.colSpan = 5 ;
823912
824913 const detail = document.createElement('div');
825914 detail.className = isHTML ? 'qbrowser-detail html-content' : 'qbrowser-detail code-content';
@@ -877,7 +966,7 @@ function sortBy(field) {
877966
878967 viewData.sort((a, b) => {
879968 let aVal, bVal;
880- if (field === 'tags') {
969+ if (field === 'tags' || field === 'usedin' ) {
881970 aVal = (Array.isArray(a[field]) ? a[field].join(', ') : '').toLowerCase();
882971 bVal = (Array.isArray(b[field]) ? b[field].join(', ') : '').toLowerCase();
883972 } else {
@@ -900,19 +989,25 @@ function updateHeaderSortIndicators() {
900989 const sortName = document.getElementById('sortName');
901990 const sortCategory = document.getElementById('sortCategory');
902991 const sortTags = document.getElementById('sortTags');
992+ const sortUsedIn = document.getElementById('sortUsedIn');
903993
904- [sortName, sortCategory, sortTags].forEach(header => {
994+ [sortName, sortCategory, sortTags, sortUsedIn ].forEach(header => {
905995 if (!header) return;
906996 let field;
907997 if (header.id === 'sortName') field = 'name';
908998 else if (header.id === 'sortCategory') field = 'category';
909999 else if (header.id === 'sortTags') field = 'tags';
1000+ else if (header.id === 'sortUsedIn') field = 'usedin';
1001+
1002+ // Find the span element that contains the text
1003+ const span = header.querySelector('span');
1004+ if (!span) return;
9101005
9111006 if (field === currentSort.field) {
9121007 const arrow = currentSort.direction === 'asc' ? '↑' : '↓';
913- header .textContent = header .textContent.replace(/[↕↑↓]/, arrow);
1008+ span .textContent = span .textContent.replace(/[↕↑↓]/, arrow);
9141009 } else {
915- header .textContent = header .textContent.replace(/[↕↑↓]/, '↕');
1010+ span .textContent = span .textContent.replace(/[↕↑↓]/, '↕');
9161011 }
9171012 });
9181013 }
@@ -942,6 +1037,7 @@ function renderList(data){
9421037 document.getElementById('sortName')?.addEventListener('click', () => sortBy('name'));
9431038 document.getElementById('sortCategory')?.addEventListener('click', () => sortBy('category'));
9441039 document.getElementById('sortTags')?.addEventListener('click', () => sortBy('tags'));
1040+ document.getElementById('sortUsedIn')?.addEventListener('click', () => sortBy('usedin'));
9451041 }
9461042
9471043 function buildFilters(data){
@@ -1474,9 +1570,9 @@ function initializeResizers() {
14741570
14751571 cols[columnIndex].style.width = newWidth + 'px';
14761572 cols[columnIndex + 1].style.width = nextNewWidth + 'px';
1477-
1573+
14781574 // Store widths
1479- const columnNames = ['name', 'actions', 'category', 'tags'];
1575+ const columnNames = ['name', 'actions', 'category', 'tags', 'usedin' ];
14801576 columnWidths[columnNames[columnIndex]] = newWidth;
14811577 columnWidths[columnNames[columnIndex + 1]] = nextNewWidth;
14821578 };
0 commit comments