Skip to content

Commit 000cf5d

Browse files
reppuli92ihalaij1
authored andcommitted
Add a UI for batch assessment in the participants page
Fixes #1513
1 parent 53947a3 commit 000cf5d

9 files changed

Lines changed: 650 additions & 106 deletions

File tree

assets/js-translations/utils.fi.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@
1212
"Lataa lisää",
1313

1414
"Reset filters":
15-
"Nollaa suodattimet"
15+
"Nollaa suodattimet",
16+
17+
"Copied!":
18+
"Kopioitu!"
1619
}

assets/js/copy-to-clipboard.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,20 @@ const copyToClipboard = (btnClassSelector, targetIdSelector) => {
2626
});
2727
const el = $(btnClassSelector);
2828

29-
const elOriginalText = el.attr("data-original-title")
30-
? el.attr("data-original-title")
29+
const elOriginalText = el.attr("data-bs-original-title")
30+
? el.attr("data-bs-original-title")
3131
: "";
3232

3333
clipboard.on("success", function (e) {
3434
var msg = _("Copied!");
35-
el.attr("data-original-title", msg).tooltip("show");
35+
el.attr("data-bs-original-title", msg).tooltip("show");
3636
e.clearSelection();
37-
el.attr("data-original-title", elOriginalText);
37+
el.attr("data-bs-original-title", elOriginalText);
3838
});
3939

4040
clipboard.on("error", function (e) {
41-
var msg = _("Copying failed!");
42-
el.attr("data-original-title", msg).tooltip("show");
43-
el.attr("data-original-title", elOriginalText);
41+
var msg = _("Copying failed! Use Ctrl+C to copy");
42+
el.attr("data-bs-original-title", msg).tooltip("show");
43+
el.attr("data-bs-original-title", elOriginalText);
4444
});
4545
};

assets/js/participants.js

Lines changed: 237 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
function participants_list(participants, api_url, is_teacher, enrollment_statuses) {
1+
function participants_list(participants, api_url, is_teacher, enrollment_statuses, exercises_api_url, batch_assess_url, current_user_name) {
22
// Ensure predictable order by student id
33
participants.sort(function(a, b) { return a.id.localeCompare(b.id); });
44

@@ -720,7 +720,7 @@ function participants_list(participants, api_url, is_teacher, enrollment_statuse
720720
const $all_box = $('#students-select-all');
721721
$all_box.prop('checked', allChecked).prop('indeterminate', atLeastOne);
722722
$('#selected-number').text(selectedFiltered);
723-
$('#add-tag-selected, #remove-tag-selected').prop('disabled', selectedFiltered === 0);
723+
$('#add-tag-selected, #remove-tag-selected, #batch-assess-selected').prop('disabled', selectedFiltered === 0);
724724
}
725725
// Toggle per-row checkbox updates selection store
726726
$table.on('change', 'input[type="checkbox"][name="students"]', function(){
@@ -821,6 +821,111 @@ function participants_list(participants, api_url, is_teacher, enrollment_statuse
821821
}
822822
if (ids.length) openRemoveTagModal(ids);
823823
});
824+
// Global "Batch assess to selected" button
825+
$('#batch-assess-selected').off('click.aplusBatchSubmit').on('click.aplusBatchSubmit', function(){
826+
const ids = [];
827+
const filtered = dt.rows({ search: 'applied', page: 'all' }).data();
828+
for (let i = 0; i < filtered.length; i++) {
829+
const uid = filtered[i].user_id;
830+
if (selectedIds.has(uid)) ids.push(uid);
831+
}
832+
if (ids.length) {
833+
$('#batch-assess-confirm').data('targetUserIds', ids);
834+
const bsModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('batch-assess-modal'));
835+
bsModal.show();
836+
}
837+
});
838+
839+
// Fetch exercises and populate dropdown when batch assess modal is shown
840+
const batchSubmitModalEl = document.getElementById('batch-assess-modal');
841+
batchSubmitModalEl.addEventListener('show.bs.modal', function() {
842+
const $select = $('#batch-assess-category');
843+
// Disable points controls initially
844+
$('#batch-assess-value').prop('disabled', true);
845+
$('#batch-assess-value-display').prop('disabled', true);
846+
$('#batch-assess-max').text('0');
847+
// Only fetch if we haven't already populated the exercises
848+
if ($select.find('option').length <= 1) {
849+
fetch(exercises_api_url)
850+
.then(response => {
851+
if (!response.ok) throw new Error(`Failed to fetch exercises (HTTP ${response.status})`);
852+
return response.json();
853+
})
854+
.then(data => {
855+
// Recursively extract all exercises from modules
856+
function extractExercises(modules) {
857+
const exercises = [];
858+
if (Array.isArray(modules)) {
859+
modules.forEach(module => {
860+
if (Array.isArray(module.exercises)) {
861+
module.exercises.forEach(exercise => {
862+
if (exercise.max_points > 0) {
863+
exercises.push({
864+
id: exercise.id,
865+
name: exercise.hierarchical_name || exercise.display_name,
866+
max_points: exercise.max_points
867+
});
868+
}
869+
});
870+
}
871+
});
872+
}
873+
return exercises;
874+
}
875+
const exercises = extractExercises(data.results || []);
876+
exercises.forEach(exercise => {
877+
const option = document.createElement('option');
878+
option.value = exercise.id;
879+
option.textContent = exercise.name;
880+
option.dataset.maxPoints = exercise.max_points;
881+
$select.append(option);
882+
});
883+
})
884+
.catch(error => console.error('Error fetching exercises:', error));
885+
}
886+
});
887+
888+
// Sync slider and textbox values
889+
$('#batch-assess-value').off('input.aplusBatchSlider').on('input.aplusBatchSlider', function(){
890+
$('#batch-assess-value-display').val($(this).val());
891+
});
892+
$('#batch-assess-value-display').off('change.aplusBatchTextbox').on('change.aplusBatchTextbox', function(){
893+
const $this = $(this);
894+
const maxPoints = parseInt($('#batch-assess-value').attr('max') || 0, 10);
895+
let value = parseInt($this.val() || 0, 10);
896+
value = Math.max(0, Math.min(maxPoints, value));
897+
$this.val(value);
898+
$('#batch-assess-value').val(value);
899+
});
900+
901+
// Enable/disable points controls based on exercise selection
902+
$('#batch-assess-category').off('change.aplusBatchExercise').on('change.aplusBatchExercise', function(){
903+
const $this = $(this);
904+
const selectedOption = $this.find('option:selected');
905+
const isSelected = $this.val() !== '';
906+
const maxPoints = isSelected ? parseInt(selectedOption.data('maxPoints') || 0, 10) : 0;
907+
908+
const $slider = $('#batch-assess-value');
909+
const $textbox = $('#batch-assess-value-display');
910+
const $feedback = $('#batch-assess-feedback');
911+
$slider.prop('disabled', !isSelected);
912+
$textbox.prop('disabled', !isSelected);
913+
$feedback.prop('disabled', !isSelected);
914+
$slider.attr('max', maxPoints);
915+
$textbox.attr('max', maxPoints);
916+
917+
// Set value to max_points when exercise is selected
918+
if (isSelected) {
919+
$slider.val(maxPoints);
920+
$textbox.val(maxPoints);
921+
$('#batch-assess-max').text(maxPoints);
922+
} else {
923+
$slider.val(0);
924+
$textbox.val(0);
925+
$feedback.val('');
926+
$('#batch-assess-max').text('0');
927+
}
928+
});
824929
}
825930
});
826931

@@ -830,6 +935,136 @@ function participants_list(participants, api_url, is_teacher, enrollment_statuse
830935
dt._aplusRedrawTimer = setTimeout(function(){ dt.draw(false); }, 0);
831936
});
832937

938+
// Confirm batch assess (DT path)
939+
$('#batch-assess-confirm').off('click.aplusBatchConfirm').on('click.aplusBatchConfirm', function(){
940+
const userIds = $(this).data('targetUserIds') || [];
941+
const exerciseId = parseInt($('#batch-assess-category').val() || 0, 10);
942+
const points = parseInt($('#batch-assess-value-display').val() || 0, 10);
943+
const feedback = $('#batch-assess-feedback').val() || '';
944+
945+
if (!exerciseId || !userIds.length) return;
946+
947+
// Get current timestamp in format "YYYY-MM-DD HH:MM"
948+
const now = new Date();
949+
const year = now.getFullYear();
950+
const month = String(now.getMonth() + 1).padStart(2, '0');
951+
const day = String(now.getDate()).padStart(2, '0');
952+
const hours = String(now.getHours()).padStart(2, '0');
953+
const minutes = String(now.getMinutes()).padStart(2, '0');
954+
const submission_time = `${year}-${month}-${day} ${hours}:${minutes}`;
955+
956+
// Prepare the JSON payload
957+
const submissionData = {
958+
objects: userIds.map(userId => ({
959+
students_by_user_id: [userId],
960+
exercise_id: exerciseId,
961+
points: points,
962+
feedback: feedback,
963+
submission_time: submission_time
964+
}))
965+
};
966+
967+
// Make POST request
968+
const csrfTokenMatch = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/);
969+
const csrfToken = csrfTokenMatch ? decodeURIComponent(csrfTokenMatch[1]) : '';
970+
971+
fetch(batch_assess_url, {
972+
method: 'POST',
973+
credentials: 'same-origin',
974+
headers: {
975+
'Content-Type': 'application/x-www-form-urlencoded',
976+
'Accept': 'application/json',
977+
'X-CSRFToken': csrfToken
978+
},
979+
body: new URLSearchParams({
980+
'submissions_json': JSON.stringify(submissionData)
981+
})
982+
})
983+
.then(response => {
984+
if (response.ok) {
985+
// Get exercise name from selected option
986+
const exerciseOption = $('#batch-assess-category').find('option:selected');
987+
const exerciseName = exerciseOption.text();
988+
989+
// Find participant names for the submitted students
990+
const studentsList = [];
991+
userIds.forEach(userId => {
992+
const participant = participants.find(p => p.user_id === userId);
993+
if (participant) {
994+
studentsList.push(participant);
995+
}
996+
});
997+
998+
// Populate success modal
999+
$('#batch-assess-success-count').text(userIds.length);
1000+
$('#batch-assess-success-exercise').text(exerciseName);
1001+
$('#batch-assess-success-points').text(points);
1002+
$('#batch-assess-success-time').text(submission_time);
1003+
$('#batch-assess-success-grader').text(current_user_name || 'Current user');
1004+
$('#batch-assess-success-feedback').text(feedback || '(none)');
1005+
1006+
// Populate students list
1007+
const $studentsList = $('#batch-assess-success-students').empty();
1008+
studentsList.forEach(student => {
1009+
const fullName = [student.first_name, student.last_name].filter(Boolean).join(' ') || student.email;
1010+
$studentsList.append(
1011+
$('<li>').append(
1012+
escapeHtml(`${fullName} (ID: ${student.student_id || student.user_id})`)
1013+
)
1014+
);
1015+
});
1016+
1017+
// Store submission data for JSON display
1018+
$('#batch-assess-success-modal').data('submissionData', submissionData);
1019+
1020+
// Show success modal
1021+
hideModalSafely('batch-assess-modal');
1022+
const successModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('batch-assess-success-modal'));
1023+
successModal.show();
1024+
} else {
1025+
response.json().then(data => {
1026+
if (data && Array.isArray(data.errors)) {
1027+
alert('Error submitting batch:\n' + data.errors.join('\n'));
1028+
} else {
1029+
alert('Error submitting batch: ' + response.statusText);
1030+
}
1031+
}).catch(() => {
1032+
alert('Error submitting batch: ' + response.statusText);
1033+
});
1034+
}
1035+
})
1036+
.catch(error => {
1037+
console.error('Error:', error);
1038+
alert('Error submitting batch: ' + error.message);
1039+
});
1040+
});
1041+
1042+
// Show JSON button on success modal
1043+
$('#batch-assess-success-show-json').off('click.aplusShowJson').on('click.aplusShowJson', function(){
1044+
const submissionData = $('#batch-assess-success-modal').data('submissionData');
1045+
if (submissionData) {
1046+
const jsonStr = JSON.stringify(submissionData, null, 2);
1047+
$('#batch-assess-json-content').text(jsonStr);
1048+
const jsonModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('batch-assess-json-modal'));
1049+
jsonModal.show();
1050+
}
1051+
});
1052+
1053+
// Copy JSON to clipboard button
1054+
$('#batch-assess-json-copy').off('click.aplusCopyJson').on('click.aplusCopyJson', function(){
1055+
const jsonStr = $('#batch-assess-json-content').text();
1056+
if (jsonStr) {
1057+
const $btn = $(this);
1058+
navigator.clipboard.writeText(jsonStr).then(function() {
1059+
const originalText = $btn.text();
1060+
$btn.text(_('Copied!'));
1061+
setTimeout(function(){ $btn.text(originalText); }, 2000);
1062+
}).catch(function(err) {
1063+
console.error('Failed to copy:', err);
1064+
});
1065+
}
1066+
});
1067+
8331068
// Confirm add/remove tag modals (DT path)
8341069
$('#tag-add-confirm').off('click.aplusConfirm').on('click.aplusConfirm', function(){
8351070
const userIds = $(this).data('targetUserIds') || [];

0 commit comments

Comments
 (0)