Skip to content

Commit 2d9ba61

Browse files
committed
Add "Used in" column.
1 parent a8142b6 commit 2d9ba61

File tree

1 file changed

+111
-15
lines changed

1 file changed

+111
-15
lines changed

questionbrowser.php

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
*/
7979
class 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

Comments
 (0)