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 ( / (?: ^ | ; ) c s r f t o k e n = ( [ ^ ; ] + ) / ) ;
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