# -*- Tcl -*- ######################################################################## # Inclass-Exam workflow, designed similar to online-exam # ====================================================== # # Defining exams: This workflow lets a lecturer choose from a # predefined set of exam questions, which are typically open text, # short text, single or multiple choice questions. The lecturer # selects test questions via drag and drop. The lecturer can perform a # test run of the created exam, and can get the results via a result # table. # # Publishing and closing exams: When a lecturer is satisfied with the # exam, the exam can be published. In this step, all answers of the # testing phase are deleted. In the process of publishing, the link to # start the exam is offered to the user. When the exam is published, # the lecturer can see the incoming answers in the report by refreshing # the page. When the exam is done, it is unpublished. The workflow # offers the lecturer to see a summary of the results in form of a # table (an to download the results via csv), or the lecturer can # produce a printer friendly version of the answers. # # An admin might wish to add the following entries to the folder to ease # creation of exercises and exams # # {clear_menu -menu New} # # {entry -name New.Item.TextInteraction -form en:edit-interaction.wf -query p.item_type=Text} # {entry -name New.Item.ShortTextInteraction -form en:edit-interaction.wf -query p.item_type=ShortText} # {entry -name New.Item.SCInteraction -form en:edit-interaction.wf -query p.item_type=SC} # {entry -name New.Item.MCInteraction -form en:edit-interaction.wf -query p.item_type=MC} # {entry -name New.Item.ReorderInteraction -form en:edit-interaction.wf -query p.item_type=Reorder} # {entry -name New.Item.UploadInteraction -form en:edit-interaction.wf -query p.item_type=Upload} # # {entry -name New.App.Exam -label "Inclass Exam" -form en:inclass-exam.wf} # # Alternatively, one can use the programmatic setup for the menubar # via config=test-items in case a site wants to change all setups in # all instances for menubars by updating a single file. # # {config -use test-items} # # The policy has to allow the following methods on FormPages: # # - "answer" (for students), # - "delete" (for lecturers), # - "edit" (for students), # - "exam-results" (for lecturers), # - "poll" (for lecturers), # - "poll-open" (for students), # - "print-answers" (for lecturers), # - "print-participants" (for lecturers), # - "proctor" (for students), # - "qrcode" (for lecturers) # - "question-summary" (for lecturers), # - "toggle-publish-status" (for lecturers), # - "view-my-exam" (for students), # # Gustaf Neumann, Feb 2012-2021 ######################################################################## set :autoname 1 ;# to avoid editable name field set :policy ::xowf::test_item::test-item-policy-publish set :debug 0 set :live_updates 1 :forward QM ::xowf::test_item::question_manager :forward AM ::xowf::test_item::answer_manager set :fc_repository { {countdown_audio_alarm:boolean,horizontal=true,default=t,label=#xowf.Countdown_audio_alarm#,help_text=#xowf.Countdown_audio_alarm_help_text#} {shuffle_items:boolean,horizontal=true,label=#xowf.randomized_items#,help_text=#xowf.randomized_items_help_text#} {max_items:number,min=1,label=#xowf.Max_items#,help_text=#xowf.Max_items_help_text#} {allow_paste:boolean,horizontal=true,default=t,label=#xowf.Allow_paste#,help_text=#xowf.Allow_paste_help_text#} {allow_spellcheck:boolean,horizontal=true,default=t,label=#xowf.Allow_spellcheck#,help_text=#xowf.Allow_spellcheck_help_text#} {show_minutes:boolean,horizontal=true,default=t,label=#xowf.Show_minutes#,help_text=#xowf.Show_minutes_help_text#} {show_points:boolean,horizontal=true,default=t,label=#xowf.Show_points#,help_text=#xowf.Show_points_help_text#} {show_ip:boolean,horizontal=true,default=t,label=#xowf.Show_IP#,help_text=#xowf.Show_IP_help_text#} {time_budget:range,default=100,min=100,max=300,step=5,with_output=t,form_item_wrapper_CSSclass=form-inline,output_suffix=%,label=#xowf.Time_budget#,help_text=#xowf.Time_budget_help_text#} {synchronized:boolean,horizontal=true,default=f,label=#xowf.Synchronized#,help_text=#xowf.Synchronized_help_text#} {time_window:time_span,label=#xowf.Exam_time_window#,help_text=#xowf.Exam_time_window_help_text#} {proctoring:boolean,horizontal=true,default=f,label=#xowf.Proctoring#,help_text=#xowf.Proctoring_help_text#} {proctoring_options:checkbox,horizontal=true,options={Desktop d} {Camera c} {Audio a} {Statement s},default=d c a s,label=#xowf.Proctoring_options#,help_text=#xowf.Proctoring_options_help_text#,swa?:disabled=1} {proctoring_record:boolean,horizontal=true,default=t,label=#xowf.Proctoring_record#,help_text=#xowf.Proctoring_record_help_text#} {signature:boolean,horizontal=true,default=f,label=#xowf.Signature#,help_text=#xowf.Signature_help_text#} {show_pagination_actions:boolean,horizontal=true,default=t,label=#xowf.Show_pagination_actions#,help_text=#xowf.Show_pagination_actions_help_text#} {grading:grading_scheme,required,default=none,label=#xowf.Grading_scheme#,help_text=#xowf.Grading_scheme_help_text#} {iprange:iprange,required,default=all,label=#xowf.IPrange#,help_text=#xowf.IPrange_help_text#} } Property realexam -default 1 -allow_query_parameter true ######################################################################## # Define actions: # Action select -next_state created -label #xowf.online-exam-select# \ -title #xowf.online-exam-title-select# Action publish -next_state published -state_safe true -label #xowf.online-exam-publish# \ -title #xowf.online-exam-title-publish# Action unpublish -next_state done -state_safe true -label #xowf.online-exam-unpublish# Action republish -next_state published -label #xowf.online-exam-republish# \ -title #xowf.online-exam-title-republish# Action restart -next_state initial -label #xowf.restart# \ -title #xowf.online-exam-title-restart# Action open_submission_review -next_state submission_review -label #xowf.open_submission_review# \ -title #xowf.open_submission_review_title# Action close_submission_review -next_state done -label #xowf.close_submission_review# \ -title #xowf.close_submission_review_title# ######################################################################## # Define states: # State parameter { {extra_css {/resources/xowf/test-item.css}} } State initial -actions {select} -form en:select_question.form -view_method edit State created -actions {publish restart} -form_loader load_form -view_method edit \ -form "#xowf.inclass-exam-draft_exam#" State published -actions {unpublish} -form_loader load_form -view_method edit \ -form "#xowf.inclass-exam-open#" State done -actions {republish open_submission_review} \ -in_role swa { -actions {republish open_submission_review restart} } \ -form_loader load_form -view_method edit \ -form "#xowf.inclass-exam-closed#" State submission_review -actions {close_submission_review} -form_loader load_form -view_method edit \ -form "#xowf.inclass-exam-review#" ######################################################################## # Activate action select: After the lecturer has selected the # exercises, the answer workflow is created. # select proc activate {obj} { $obj AM create_workflow -answer_workflow /packages/xowf/lib/inclass-exam-answer.wf \ $obj } ######################################################################## # Activate action publish: delete all responses for the workflow and # publish user participation link. # publish proc activate {obj} { # # On the first activation of "publish", older data (e.g. from # testruns) is removed. When the exam was already published before - # e.g. a manual publish operation before an automatic one - NO # cleanup is performed to avoid potential loss of submission data. # set revision_sets [$obj get_revision_sets] set published_periods [$obj AM state_periods $revision_sets -state published] if {[llength $published_periods] == 0} { $obj AM delete_all_answer_data $obj } :publish_link $obj } ######################################################################## # Activate action republish: publish user participation link. # republish proc activate {obj} { :publish_link $obj } ######################################################################## # When the user un-publishes an exam, just the user participation # link should be removed for the users # unpublish proc activate {obj} { :unpublish_link $obj } ######################################################################## # When the user restarts an exam, make sure that already scheduled # atjobs are removed. # restart proc activate {obj} { $obj AM delete_scheduled_atjobs $obj } ######################################################################## # When the user opens the submission review, offer a link. # open_submission_review proc activate {obj} { set aLink [$obj pretty_link -query m=view-my-exam] $obj util_user_message -html -message \ "[$obj name] exam review is available as [ns_quotehtml $aLink]" } ######################################################################## # publish_link: make the user participation link available for the # target group # Action instproc publish_link {obj} { set aLink [$obj pretty_link -query m=answer] $obj util_user_message -html \ -message "[ns_quotehtml [$obj name]] is available as [ns_quotehtml $aLink]" # TODO: make it happen in the LMS } ######################################################################## # unpublish_link: remove the user participation link for the target # group # Action instproc unpublish_link {obj} { $obj util_user_message -html -message "[$obj name] is closed" # TODO: make it happen in the LMS } ######################################################################## # form loader: create dynamically a form containing the overview exam # page, containing the state of the exam and information about # students in submission or working state. # :proc load_form {ctx title} { set obj [$ctx object] set state [$obj property _state] set proctoring [$obj property proctoring 0] set combined_form_info [:QM combined_question_form -with_numbers $obj] set text "

[ns_quotehtml $title]

" set menu "" # append text \ "

" \ [::xowiki::bootstrap::card \ -title "#xowf.exam_summary# [:QM exam_configuration_popup $obj]" \ -body [:QM exam_info_block -combined_form_info $combined_form_info $obj]] \ "
" set detail_link [$obj pretty_link -query m=question-summary] append text [subst {

#xowf.question_summary#

}] set wf [$obj AM get_answer_wf $obj] if {$wf eq ""} { :msg "cannot get current workflow for [$obj name]" set tLink "." set aLink "." } else { # # Always compute the test-run and answer link. # set wf_pretty_link [$wf pretty_link] set tLink [export_vars -base $wf_pretty_link { {m create-new} {p.return_url "[::xo::cc url]"} {p.try_out_mode 1} {title "[$obj title]"} }] set aLink [$obj pretty_link -query m=answer] if {$proctoring} { # # send link via "m=proctor" # set tLink [export_vars -base [$obj pretty_link] { {m proctor} {link "$tLink&p.proctor=1"} }] # # We could send answer link ("aLink") as well this way, but we # want to keep the link short, therefore, we handle the proctor # link inside the www-answer method. # } set answers [:AM get_answer_attributes $wf] set results [:AM get_exam_results -obj $obj results] set autograded [dict get $combined_form_info autograde] set grading_scheme_name [$obj property grading] # # Per default, the entries are disabled. When there are answers, # these will be enabled. # set link_disabled [expr {[llength $answers] == 0 ? "link-disabled" : ""}] set md [subst { listing {obj $wf m list label #xowf.online-exam-exam_instances# icon list} participants {obj $obj m print-participants label #xowf.Participants# icon user} protocol {obj $obj m print-answers label #xowf.online-exam-protocol# icon list-alt} grades {obj $obj m exam-results label #xowf.Points_and_grades# icon graph-up-arrow} }] if {![acs_user::site_wide_admin_p -user_id [::xo::cc user_id]]} { dict unset md listing } ns_log notice "ANSWERS grading_scheme_name $grading_scheme_name $answers resultsNr [llength $results], autograde [dict get $combined_form_info autograde]" # # We assume here that when we have results, we have also some points. # if {[llength $results] == 0} { dict unset md grades } elseif {$grading_scheme_name in {"" "none"}} { dict set md grades label "#xowf.Points#" } #ns_log notice ALL=$text set menu "\n" } set extraAction "" switch $state { "created" { append extraAction "
" \ "#xowf.online-exam-try_out# " \ "#xowf.testrun#" } "published" { append extraAction "
" \ "#xowf.online-exam-can_answer# " \ "[ns_quotehtml $aLink]" } } set www_method [xo::cc query_parameter m:token] if {$www_method ni {edit view}} { set marked "" } else { set answerStatus "" set marked "" if {$state in {published done submission_review} || [llength $answers] > 0 } { if {$state eq "done"} { set marked [$obj AM marked_results -obj $obj -wf $wf $combined_form_info] set marked "" ;# not needed right now } set answerStatus [$obj AM answers_panel \ -heading "#xowf.online-exam-submitted_exams_heading#" \ -submission_msg "#xowf.online-exam-submitted_exams_msg#" \ -polling=[expr {${:live_updates} && $state ni {initial created done}}] \ -manager_obj $obj \ -target_state done \ -extra_text $menu \ -wf $wf] } set qrCode "" set countdownHTML "" if {$state eq "published"} { set src [$obj pretty_link -query m=qrcode] set qrCode [subst {
}] set total_minutes [:QM total_minutes_for_exam -manager $obj] if {$total_minutes > 1} { set target_time [:QM exam_target_time \ -manager $obj -base_time [$obj last_modified]] set countdownHTML [$obj AM countdown_timer -target_time $target_time -id "countdown"] } } append text [subst {
$answerStatus
$qrCode
$countdownHTML
}] #ns_log notice ALL=$text } set f [::xowiki::Form new \ -destroy_on_cleanup \ -set name en:question \ -form [subst {{
$text$marked $extraAction
} text/html}] \ -text {} \ -anon_instances t \ -form_constraints {@categories:off @cr_fields:hidden} \ ] } ######################################################################## # # Object specific operations # ######################################################################## :object-specific { set t0 [clock clicks -milliseconds] #ns_log notice "==== object-specific inclass-exam [self] state ${:state}" set ctx [:wf_context] set container [$ctx wf_container] if {$ctx ne $container} { $ctx forward load_form $container %proc $ctx } :forward QM ::xowf::test_item::question_manager :forward AM ::xowf::test_item::answer_manager ${container}::Property return_url -default "" -allow_query_parameter true if {${:state} eq "done"} { set done_actions republish set combined_form_info [:QM combined_question_form [self]] # # We could allow open_submission_review only when autograde is # possible, but apparently, it makes as well sense for other # open text answers # lappend done_actions open_submission_review #if {[dict get $combined_form_info autograde]} { # lappend done_actions open_submission_review #} set swa_done_actions [concat $done_actions restart] ${container}::done actions $done_actions ${container}::done in_role swa [subst { -actions {$swa_done_actions} }] } elseif {${:state} eq "created" && [:property realexam 0]} { # # Check, if randomization is OK. If not, remove the "publish" # button from the workflow. # # Note: this initialization code is always called when the # workflow is initialized, which might not be wanted, when this # happen during e.g. a test-run of an instance. so, maybe put this # to some "render" method? # #ns_log notice "==== check for randomization" set combined_form_info [:QM combined_question_form [self]] set randomizationOk [dict get $combined_form_info randomization_for_exam] #ns_log notice "==== check for randomization DONE" ${container}::${:state} actions \ [expr {$randomizationOk ? {publish restart} : {restart}}] } # # Unset the actual query return_url, since we want to use it via # property. In some cases, we have to set it explicitly from the # property (like in www-delete), or we have to use the actual # passed-in values (like in toggle_publish_status) # set ::__passed_in_return_url [:query_parameter return_url:localurl ""] ::xo::cc unset_query_parameter return_url ######################################################################## # web-callable method "delete" # # Delete the workflow instance and all its associated data. # :proc www-delete {} { ::xo::cc set_query_parameter return_url [:property return_url] :AM delete_all_answer_data [self] next } ######################################################################## # web-callable method "toggle-publish-status" # # Toggle the status of the workflow. This is needed to avoid a bad # interaction with [ad_return_url] as it is used in # www-toggle-publish-status in xowiki, since the workflow definition # unsets the actual return_url, which causes ad_return_url to use # the URL leading to this call (m=toggle-publish-status), causing a # redirection loop. # :proc www-toggle-publish-status {} { next -return_url $::__passed_in_return_url } ######################################################################## # web-callable method "print-participants" # # Print participants in a tabular form. # :proc www-print-participants {} { set HTML "" set wf [:AM get_answer_wf [self]] if {$wf ne ""} { set items [:AM get_wf_instances $wf] set items2 [$items deep_copy] foreach i [$items2 children] { $i set online-exam-userName [acs_user::get_element -user_id [$i creation_user] -element username] $i set online-exam-fullName [::xo::get_user_name [$i creation_user]] } set HTML [:AM participants_table \ -package_id ${:package_id} \ -items $items2 \ -state * \ [self]] $items2 destroy } if {$HTML eq ""} { set HTML "#xowiki.no_data#" } else { set HTML "

#xowf.Participants#

$HTML" } set return_url [[$wf package_id] query_parameter local_return_url:localurl [:pretty_link]] append HTML "

#xowiki.back#

\n" :www-view $HTML } ######################################################################## # web-callable method "view-my-exam " # # Provide feedback to the student about the results. # :proc www-view-my-exam {} { if {${:state} eq "submission_review"} { ::xo::cc set_query_parameter creation_user [ad_conn user_id] ::xo::cc set_query_parameter as_student 1 :www-print-answers } else { :www-view "#xowf.Exam-review-not-open#" } } ######################################################################## # web-callable method "question-summary" # # Print a summary of the exam-questions. # :proc www-question-summary {} { :www-view [:QM question_summary [self]] } ######################################################################## # web-callable method "exam-results" # # Print results (grades, statistics) for a lecturer. # :proc www-exam-results {} { set form_info [:QM combined_question_form [self]] set autograde [dict get $form_info autograde] set orderby [:query_parameter orderby:token "participant,desc"] set format [:query_parameter format:alpha ""] set perQuestion [:query_parameter per-question:boolean 0] set grading_scheme_name [:property grading] if {$grading_scheme_name eq ""} { set grading_scheme_name none } set gradingScheme [:AM grading_scheme -examWf [self] -grading $grading_scheme_name] set withGrades [expr {$gradingScheme ne "::xowf::test_item::grading::none"}] set return_url [:query_parameter local_return_url:localurl [:pretty_link]] set backButtonHTML "

#xowiki.back#

\n" set results [:AM get_exam_results -obj [self] results] if {[llength $results] eq 0} { # # Actually, the link leading to this result should not be # active. The message is just for cases of URL hacking. # return [:www-view "

No submission results available.$backButtonHTML"] } set manual_gradings [:AM get_exam_results -obj [self] manual_gradings] if {$format eq "csv"} { return [:AM exam_results \ -format csv \ -reply \ -orderby $orderby \ -only_grades [:query_parameter onlygrades:boolean true] \ -gradingScheme [expr {$perQuestion ? "" : $gradingScheme}] \ -manual_gradings $manual_gradings \ $results] #:AM exam_results -reply -manual_gradings $manual_gradings $results } set statistics [:AM get_exam_results -obj [self] statistics] # # We have to following options: # - perParticipant with grades # - perParticipant without grades # - perQuestion (always without grades, since this includes as well the statistics per alternative) # set HTML "" if {0} { set HTML [subst {

        autograde $autograde
        manual_gradings $manual_gradings
        results $results
        statistics $statistics
      }]
      append HTML \n\nper_question\n[:AM exam_results -manual_gradings $manual_gradings $results]
      append HTML \nper_student\n[:AM exam_results -gradingScheme $gradingScheme \
                                      -manual_gradings $manual_gradings $results]
      append HTML 
} if {$perQuestion} { append HTML \ "

#xowf.exam_statistics_question#

\n" \ [:AM exam_results -format html \ -manual_gradings $manual_gradings \ $results] set exportQuestionResultsURL [ad_return_url {{format csv} {onlygrades 0}}] set perParticipantURL [ns_conn url]?[::xo::update_query [ns_conn query] per-question 0] set buttonsHTML [subst {


#xowf.export_results_title# #xowf.exam_statistics_participant#

}] } else { append HTML \ "

#xowf.exam_statistics_participant#

" \ "

#xowf.Grading-Scheme#: [$gradingScheme cget -title]\n" \ [:AM exam_results -format html \ -orderby $orderby \ -gradingScheme $gradingScheme \ -manual_gradings $manual_gradings \ $results] set exportPointGradesURL [ad_return_url {{format csv} {onlygrades 0}}] if {$withGrades} { # # Only grades as table # #append HTML [:AM exam_results -format html \ # -orderby $orderby \ # -only_grades true \ # -gradingScheme $gradingScheme \ # -manual_gradings $manual_gradings \ # $results] append HTML [:AM exam_results -format chart \ -orderby $orderby \ -only_grades true \ -gradingScheme $gradingScheme \ -manual_gradings $manual_gradings \ $results] set exportGradesURL [ad_return_url {{format csv}}] set buttonsHTML [subst {


#xowf.export_grades_title# #xowf.export_points_and_grades_title# }] } else { set buttonsHTML [subst { #xowf.export_points_title# }] } set perQuestionURL [ns_conn url]?[::xo::update_query [ns_conn query] per-question 1] append buttonsHTML [subst { #xowf.exam_statistics_question#

}] } if {$HTML eq ""} { set HTML "#xowiki.no_data#" } else { append HTML $buttonsHTML } append HTML $backButtonHTML :www-view $HTML } ######################################################################## # web-callable method "print-answers" # # Print the answers in a somewhat printer friendly way. # :proc www-print-answers {} { template::head::add_link -rel stylesheet -href /resources/xowf/test-item.css set as_student [:query_parameter as_student:boolean false] set fos [:query_parameter fos:int32 ""] set d [:AM render_answers \ -as_student $as_student \ -creation_user [:query_parameter creation_user:int32 ""] \ -revision_id [:query_parameter rid:int32 ""] \ -filter_submission_id [:query_parameter id:int32 ""] \ -filter_form_ids $fos \ -export [:query_parameter export:boolean 0] \ -orderby [:query_parameter orderby:token "online-exam-userName"] \ -grading [:query_parameter grading:token [:property grading]] \ -with_grading_table [expr {!$as_student && $fos eq ""}] \ [self]] set do_stream [dict get $d do_stream] set HTML [dict get $d HTML] if {$do_stream == 0 && $HTML eq ""} { set HTML "#xowiki.no_data#" } if {!$as_student} { set return_url [:query_parameter local_return_url:localurl [:pretty_link]] append HTML "

#xowiki.back#

\n" } if {$do_stream} { ns_write [lang::util::localize $HTML] set HTML "" ${:package_id} set __continuation ad_progress_bar_end return "" } else { :www-view $HTML } } ######################################################################## # web-callable method "answer" # :proc www-answer {} { # # Create or use an answering workflow for the current exam. This # is a convenience routine to shorten the published URL. # # Make sure that no-one tries to start the answer workflow in a # state different to "published". # if {${:state} ne "published"} { switch ${:state} { done { set message "

#xowf.inclass-exam-closed#" } created { set message [:AM waiting_room_message [self]] } default { set message "

#The Exam has not been published#" } } return [:www-view $message] } elseif {![:AM allow_answering -examwf [self] -ip [ns_conn peeraddr]]} { return [:www-view "

[_ xowf.IPrange_not_allowed [list ip [ns_conn peeraddr]]]"] } else { set wf [:AM get_answer_wf [self]] set proctoring [:property proctoring] if {$proctoring ne "" && $proctoring} { set po [:property proctoring_options] set record_p [:property proctoring_record true] set cLink [export_vars -base [:pretty_link] { {m proctor} {link "[:pretty_link -query m=proctor-answer&proctoring_options=$po&record_p=$record_p]"} }] ::${:package_id} returnredirect $cLink } else { $wf www-create-or-use -parent_id [:item_id] } } } ######################################################################## # web-callable function "qrcode", acts as responder # :proc www-qrcode {} { # # Produce a QR code with an answer link # set aLink [:pretty_link -absolute true -query m=answer] set fn /tmp/qr-${:item_id}.png exec qrencode -o $fn -l h $aLink ns_returnfile 200 image/png $fn ad_script_abort } ######################################################################## # web-callable method "proctoring-display" # :proc www-proctoring-display {} { # # Display the proctoring files collected for this exam using the # UI by the proctoring-support package. # # By this is also possible to delete the proctoring files, either # for the whole exam or for the single participant. # ${:package_id} return_page -adp /packages/proctoring-support/lib/proctoring-display -variables { {object_id ${:item_id}} } } ######################################################################## # web-callable function "proctor-answer" # :proc www-proctor-answer {} { # # Start answering an exam in proctored mode # if {[:property _state] ne "published"} { :util_user_message -html -message "Cannot start answer workflow in this state" } else { set wf [:AM get_answer_wf [self]] $wf www-create-or-use -parent_id [:item_id] } } ######################################################################## # web-callable function "proctor" # :proc www-proctor {} { # # Redirect the exam to an iframe for implementing proctoring. The # basic idea is that the web application turns on the camera and # keeps the iframe while the user is iterating through the exam. # The line "

You are being proctored!

" is just a # placeholder and has to be replaced with real code. # set link [:query_parameter link:localurl ""] ::xo::cc set_parameter template_file view-plain-master ::xo::cc set_parameter MenuBar 0 set proctoring_template /packages/proctoring-support/lib/proctored-page if {[file exists [acs_root_dir]${proctoring_template}.adp]} { set object_id ${:item_id} set object_url $link set examination_statement_p [expr {![string match *p.try_out_mode=1* $link]}] set record_p [:property proctoring_record true] set proctoring_options [:property proctoring_options "d c a s"] foreach \ proctoring_parm {d c a s} \ flag {desktop_p camera_p audio_p examination_statement_p} { set $flag [expr {$proctoring_parm in $proctoring_options}] } set preview_p [expr {$desktop_p || $camera_p || $audio_p}] # # Set the max interval between screen captures to 30s (default is 60s) # set max_ms_interval 30000 ${:package_id} return_page -adp $proctoring_template -variables { object_id object_url preview_p desktop_p camera_p audio_p record_p max_ms_interval examination_statement_p {check_active_p false} } } else { # # Minimal fallback in case the proctoring-support is not installed # return [:www-view [subst {

You ([xo::cc user_id]) are being proctored in exam ${:object_id}!

}]] } } ######################################################################## # web-callable function "proctor-image", acts as responder # :proc www-proctor-image {} { # # View a proctored image # set type [${:package_id} query_parameter type:ascii ""] set ts [${:package_id} query_parameter ts:integer ""] set ext [${:package_id} query_parameter e:wordchar ""] set user_id [${:package_id} query_parameter user_id:int32 ""] set proctoring_dir [proctoring::folder \ -object_id ${:item_id} \ -user_id $user_id] set png_path $proctoring_dir/$type-$ts.$ext #ns_log notice "image: $png_path ... [ad_file exists $$png_path]" ns_returnfile 200 [ns_guesstype $ts.$ext] $png_path ad_script_abort } ######################################################################## # web-callable function "blank-inputs" # :proc www-blank-inputs {} { # # Analyze the student submissions an find situations, where input # is "cleared" between revisions. This method is primarily for # debugging purposes. # template::head::add_link -rel stylesheet -href /resources/xowf/test-item.css set HTML [:AM render_answers_with_edit_history [self]] if {$HTML eq ""} { set HTML "#xowiki.no_data#" } set return_url [:query_parameter local_return_url:localurl [:pretty_link]] append HTML "

#xowiki.back#

\n" :www-view $HTML } ######################################################################## # AJAX call "poll", acts as responder # :proc www-poll {} { # # Return statistics about working and finished exams. # set wf [:AM get_answer_wf [self]] set answers [:AM get_answer_attributes $wf] set answered [:AM get_answer_attributes -state done $wf] ns_return 200 text/plain [llength $answered]/[llength $answers] #ns_log notice "MASTER POLL [self] ${:name}, returned [llength $answered]/[llength $answers]" ad_script_abort } :proc www-poll-open {} { if {${:state} eq "created"} { set time [lc_time_fmt [clock format [clock seconds] -format "%Y-%m-%d %H:%M:%S"] "%Q %T"] ns_return 200 text/plain [subst {{"action": "msg", "msg": "$time"} }] } else { set URL [:pretty_link -query m=answer] ns_return 200 text/plain [subst {{"action": "redirect", "url": "$URL"} }] } ad_script_abort } ######################################################################## # AJAX call "send-participant-message", acts as responder # :proc www-send-participant-message {} { # # Send a message to a participant # ::xowiki::includelet::personal-notification-messages message_add \ -notification_id ${:item_id} \ -to_user_id [${:package_id} form_parameter to_user_id ""] \ -payload [list msg [${:package_id} form_parameter msg] \ from [xo::cc user_id] \ urgency [${:package_id} form_parameter urgency]] ns_return 200 text/plain ok ad_script_abort } ######################################################################## # AJAX call "grade-single-item", acts as responder # :proc www-grade-single-item {} { set formDict [ns_set array [ns_getform]] # # Update property "manual_gradings" of the exam in the results page # set manual_gradings [:AM get_exam_results -obj [self] manual_gradings] set user_id [dict get $formDict user_id] set qn [dict get $formDict question_name] dict set manual_gradings $user_id $qn achieved [dict get $formDict achieved] dict set manual_gradings $user_id $qn comment [dict get $formDict comment] :AM set_exam_results -obj [self] manual_gradings $manual_gradings # # Recompute the achieved points percentage for the full exam # submission of the student... based on the grading scheme and # manual grading info. # set grading_scheme [dict get $formDict grading_scheme] set achieved_points [dict get $formDict achieved_points] set details [dict get $achieved_points details] set newDetails {} foreach detail $details { if {[dict get $detail attributeName] eq $qn} { dict set detail achieved [dict get $formDict achieved] } lappend newDetails $detail } dict set achieved_points details $newDetails dict set achieved_points achievedPoints "" set gradingInfo [::xowf::test_item::grading::$grading_scheme print -achieved_points $achieved_points] # # Return the line for the panel as result of the AJAX call # ns_return 200 text/plain [dict get $gradingInfo panel] ad_script_abort } ######################################################################## # AJAX call "update-config", acts as responder # :proc www-update-config {} { # # Received updates for form # set field_names [:QM exam_configuration_modifiable_field_names [self]] set form_fields [:create_form_fields_from_names -lookup \ -form_constraints [:get_fc_repository] \ $field_names] #ns_log notice "UPDATE CONFIG <[::xo::cc array names form_parameter]> [ns_set array [ns_conn form]] // $form_fields" set last_instance_attributes ${:instance_attributes} set field_names_with_child_components [lmap f [::xowiki::formfield::child_components $form_fields] {$f name}] #ns_log notice "\nORIG $field_names \NEW $field_names_with_child_components" lassign [:get_form_data -field_names $field_names_with_child_components $form_fields] validation_errors category_ids if {$validation_errors == 0} { if {$last_instance_attributes eq ${:instance_attributes}} { ns_log notice "UPDATE CONFIG ... nothing has changed" } else { ns_log notice "UPDATE CONFIG ... no validation_errors -> SAVE" :update_attribute_from_slot [:find_slot instance_attributes] ${:instance_attributes} if {[dict exists $last_instance_attributes time_window]} { set old_time_window [dict get $last_instance_attributes time_window] set new_time_window [dict get ${:instance_attributes} time_window] if {$old_time_window ne $new_time_window} { ns_log notice "UPDATE CONFIG time_window $new_time_window" :AM time_window_setup [self] -time_window $new_time_window ns_log notice "UPDATE CONFIG time_window $new_time_window DONE" } } } ns_return 200 text/plain OK } else { ns_return 200 text/plain validation_errors } ad_script_abort } #ns_log notice "==== object-specific inclass-exam [self] state ${:state} DONE (took [expr {[clock clicks -milliseconds]-$t0}]ms)" } # # Local variables: # mode: tcl # tcl-indent-level: 2 # indent-tabs-mode: nil # End: