# -*- 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 with 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), # - "proctor" (for students), # - "view-my-exam" (for students), # - "edit" (for students), # - "poll" (for lecturers), # - "print-answers" (for lecturers), # - "print-answer-table" (for lecturers), # - "print-participants" (for lecturers), # - "delete" (for lecturers), # - "qrcode" (for lecturers) # # Gustaf Neumann, Feb 2012-2020 ######################################################################## set :autoname 1 ;# to avoid editable name field set :policy ::xowf::test_item::test-item-policy-publish set :debug 0 set :live_updates 1 Action select -next_state created -label #xowf.online-exam-select# \ -title #xowf.online-exam-title-select# Action publish -next_state published -label #xowf.online-exam-publish# \ -title #xowf.online-exam-title-publish# Action unpublish -next_state done -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# 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 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} { xowf::test_item::answer_manager 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} { xowf::test_item::answer_manager 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} { xowf::test_item::answer_manager 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 "[$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 disabled # questions as a preview and the survey results (the results can be # refreshed). # :proc load_form {ctx title} { set obj [$ctx object] set state [$obj property _state] set combined_form_info [::xowf::test_item::question_manager combined_question_form -with_numbers $obj] set fullQuestionForm [dict get $combined_form_info form] set full_fc [dict get $combined_form_info disabled_form_constraints] #:log fullQuestionForm=$fullQuestionForm set text "

$title

" set menu "" set proctoring [$obj property proctoring 0] set synchronized [$obj property synchronized 0] set allow_paste [$obj property allow_paste 1] set max_items [$obj property max_items ""] set time_window [$obj property time_window ""] append text [subst {

[expr {$synchronized ? "" : "Non-"}]Synchronized Exam [expr {$proctoring ? " with Proctoring" : ""}]

}] set question_objs [dict get $combined_form_info question_objs] set nrQuestions [llength $question_objs] set randomizationOk [dict get $combined_form_info randomization_for_exam] set autograde [dict get $combined_form_info autograde] set revision_sets [$obj get_revision_sets] set published_periods [xowf::test_item::answer_manager state_periods $revision_sets -state published] set review_periods [xowf::test_item::answer_manager state_periods $revision_sets -state submission_review] set total_minutes [xowf::test_item::question_manager total_minutes -max_items $max_items $combined_form_info] set total_points [xowf::test_item::question_manager total_points -max_items $max_items $combined_form_info] set max_items_msg "" if {$max_items ne ""} { set all_minutes [lmap t [dict get $combined_form_info title_infos] { dict get $t minutes }] if {[llength [lsort -unique $all_minutes]] != 1} { set max_items_msg [_ xowf.Max_items_not_ok_duration [list n $max_items]] } elseif {$max_items > [llength $all_minutes]} { set max_items_msg [_ xowf.Max_items_not_ok_number [list n $max_items]] } else { set max_items_msg [_ xowf.Max_items_ok [list n $max_items]] } } set time_window_msg "" if {$time_window ne ""} { set dtstart [dict get $time_window time_window.dtstart] if {$dtstart ne ""} { regsub -all T $dtstart " " dtstart set dtend [dict get $time_window time_window.dtend] set time_window_msg
[_ xowf.Automatically_published_from_to [list from $dtstart to $dtend]] set time_window_msg "
Automatische Freischaltung der Prüfung von $dtstart bis $dtend" } } append text [subst {

[expr {$max_items_msg ne "" ? "$max_items_msg" : ""}] $nrQuestions [expr {$nrQuestions == 1 ? "#xowf.question#" : "#xowf.questions#"}], $total_minutes #xowf.Minutes#, $total_points #xowf.Points#
[expr {$autograde ? "#xowf.exam_review_possible#" : "#xowf.exam_review_not_possible#"}]
[expr {$randomizationOk ? "#xowf.randomization_for_exam_ok#" : "#xowf.randomization_for_exam_not_ok#"}]
[expr {$allow_paste ? "#xowf.Cut_and_paste_allowed#" : "#xowf.Cut_and_paste_not_allowed#"}]
$time_window_msg [expr {[llength $published_periods] > 0 ? "
#xowf.inclass-exam-open#: [join $published_periods {, }]
" : ""}] [expr {[llength $review_periods] > 0 ? "#xowf.inclass-exam-review#: [join $review_periods {, }]
" : ""}]

}] set wf [xowf::test_item::answer_manager get_answer_wf $obj] if {$wf eq ""} { :msg "cannot get current workflow for [$obj name]" set lLink "." set tLink "." set aLink "." set pLink "." } 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. # } # # If there are answers, include the full menu. # set answers [xowf::test_item::answer_manager get_answers $wf] if {[llength $answers] > 0} { set lLink "$wf_pretty_link?m=list" set pLink1 [$obj pretty_link -query m=print-answers] set pLink2 [$obj pretty_link -query m=print-answer-table] set pLink3 [$obj pretty_link -query m=print-participants] set menu "\[" if {[acs_user::site_wide_admin_p -user_id [::xo::cc user_id]]} { append menu "#xowf.online-exam-exam_instances#, " } append menu \ "#xowf.Participants#, " \ "#xowf.online-exam-protocol#, " \ "#xowf.online-exam-results-table#\]" } } switch $state { "created" - "done" - "submission_review" - "published" { # # In inclass cases, never show all questions on screen, since # the lecturer might have the screen on the projector. # template::add_script -src urn:ad:js:bootstrap3 set fullQuestionForm [subst {
$fullQuestionForm
}] } } set extraAction "" switch $state { "created" { append extraAction "
" \ "#xowf.online-exam-try_out# " \ "#xowf.testrun#" } "published" { append extraAction "
" \ "#xowf.online-exam-can_answer# " \ "$aLink" } } set www_method [xo::cc query_parameter m] if {$www_method ni {edit view}} { set marked "" } else { set answerStatus "" set marked "" if {$state in {published done submission_review} } { if {$state eq "done"} { [$ctx object] setCSSDefaults set marked [xowf::test_item::answer_manager marked_results -obj $obj -wf $wf $combined_form_info] set marked "" ;# not needed right now } set answerStatus [xowf::test_item::answer_manager 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 \ -wf $wf] } set qrCode "" set countdownHTML "" if {$state eq "published"} { set src [$obj pretty_link -query m=qrcode] set qrCode [subst {
}] set target_time [xowf::test_item::question_manager exam_target_time \ -manager $obj -base_time [$obj last_modified]] set countdownHTML [xowf::test_item::answer_manager countdown_timer \ -target_time $target_time -id "countdown"] } # Remove wrapping forms regsub -all {]*>} $fullQuestionForm {} fullQuestionForm append text [subst {
$answerStatus
$fullQuestionForm
$qrCode
$countdownHTML
}] } set footer "$menu $extraAction" #-form [subst {
$text
$fullQuestionForm
$report
text/html}] set f [::xowiki::Form new \ -destroy_on_cleanup \ -set name en:question \ -form [subst {
$text$marked$footer
text/html}] \ -text {} \ -anon_instances t \ -form_constraints $full_fc \ ] } ######################################################################## # # Object specific operations # ######################################################################## :object-specific { set ctx [:wf_context] set container [$ctx wf_container] if {$ctx ne $container} { $ctx forward load_form $container %proc $ctx } ${container}::Property return_url -default "" -allow_query_parameter true # # 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, e.g. in www-delete. # ::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] xowf::test_item::answer_manager delete_all_answer_data [self] next } ######################################################################## # web-callable method "print-answer-table" # # Print the answers in a tabular form. # :proc www-print-answer-table {} { set HTML "" set withAnswerColumns [${:package_id} query_parameter with_answers:boolean 0] set wf [xowf::test_item::answer_manager get_answer_wf [self]] if {$wf ne ""} { #set form_info [::xowf::test_item::question_manager combined_question_form -with_numbers [self]] set items [xowf::test_item::answer_manager 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 [::xowf::test_item::answer_manager results_table \ -package_id ${:package_id} \ -items $items2 \ -state * \ -with_answers $withAnswerColumns \ [self]] $items2 destroy } if {$HTML eq ""} { set HTML "#xowiki.no_data#" } else { set HTML "

#xowf.online-exam-results-table#

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

#xowiki.back#

\n" xo::Page requireCSS /resources/xowf/test-item.css :www-view $HTML } ######################################################################## # web-callable method "print-participants" # # Print participants in a tabular form. # :proc www-print-participants {} { set HTML "" set wf [xowf::test_item::answer_manager get_answer_wf [self]] if {$wf ne ""} { set items [xowf::test_item::answer_manager 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 [::xowf::test_item::answer_manager 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 "print-answers" # # Print the answers in a somewhat printer friendly way. # :proc www-print-answers {} { set as_student [:query_parameter as_student:boolean 0] set filter_id [:query_parameter id:integer ""] set creation_user [:query_parameter creation_user:integer ""] set revision_id [:query_parameter rid:integer ""] set combined_form_info [::xowf::test_item::question_manager combined_question_form [self]] set autograde [dict get $combined_form_info autograde] set totalPoints [::xowf::test_item::question_manager total_minutes \ -max_items [:property max_items ""] \ $combined_form_info] # # The management of the grading scheme has to be extended. For the # time being, we have a single grading scheme with the option to # round to full points or not. When an exam has less than 40 # points, we do not round, since this rounding could provide more # than 1 percent of the result. This should be made configurable # (also in www-print-answer-table, which is not used right now). # set grading_scheme ::xowf::test_item::grading::wi1 if {$totalPoints < 40} { append grading_scheme _noround } set grade_dict {} set grade_csv "" # # Provide quick mapping from the mangled attribute name to the question obj. # set nameToQuestionObj [xowf::test_item::renaming_form_loader \ name_to_question_obj_dict \ [dict get $combined_form_info question_objs]] set ctx [::xowf::Context require [self]] set wf [xowf::test_item::answer_manager get_answer_wf [self]] if {$wf ne ""} { set items [xowf::test_item::answer_manager get_wf_instances \ {*}[expr {$creation_user ne "" ? "-creation_user $creation_user" : ""}] \ {*}[expr {$filter_id ne "" ? "-item_id $filter_id" : ""}] \ $wf] set withSignature [expr {[dict exists ${:instance_attributes} signature] ? [dict get ${:instance_attributes} signature] : 0 }] set examTitle ${:title} set do_stream [expr {[llength [$items children]] > 100}] ::xo::cc set_parameter template_file view-plain-master ::xo::cc set_parameter MenuBar 0 template::head::add_link -rel stylesheet -href /resources/xowf/test-item.css if {$as_student} { set userName [acs_user::get_element -user_id [ad_conn user_id] -element username] set fullName [::xo::get_user_name [ad_conn user_id]] set heading "$userName - $fullName" set HTML "

#xowf.online-exam-review-protocol# - $heading

\n" } else { set HTML "

#xowf.online-exam-protocol#

\n" } if {$do_stream} { # ns_log notice STREAM-[info level]-$::template::parse_level uplevel #$::template::parse_level [subst {set title "${:title}"; set context .}] ad_return_top_of_page [ad_parse_template \ -params [list context title] \ [template::streaming_template]] ns_write [subst {

[ns_quotehtml ${:title}]

[lang::util::localize $HTML] }] set HTML "" } if {$revision_id ne ""} { set r [::xowiki::FormPage get_instance_from_db -revision_id $revision_id] if {[$r item_id] ni [lmap i [$items children] {$i item_id}]} { error "invalid revision id '$revision_id' provided" } $items destroy set items [::xo::OrderedComposite new -destroy_on_cleanup] $items add $r } foreach i [$items 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]] } $items orderby online-exam-userName foreach i [$items children] { set userName [$i set online-exam-userName] set fullName [$i set online-exam-fullName] set state [$i state] #if {$state ne "done"} { # ns_log notice "online-exam: submission of $userName is not finished (state $state)" # #continue #} set revisions [$i get_revision_sets] if {[llength $revisions] <=1 } { # just an initial revision ns_log notice "online-exam: submission of $userName is empty. Ignoring." continue } # # The call to "render_content" calls actually the # "summary_form" of online/inclass-exam-answer.wf when the submit # instance is in state "done". We set the __feedback_mode to # get the auto-correction included. # foreach f [::xowiki::formfield::FormField info instances -closure] { #ns_log notice "FF could DESTROY $f [$f name]" if {[string match *_ [$f name]]} { #ns_log notice "FF DESTROY $f [$f name]" $f destroy } } $wf form_field_flush_cache set achieved_points {} xo::cc eval_as_user -user_id [$i creation_user] { $i set __feedback_mode 2 set question_form [$i render_content] if {$withSignature || $autograde} { set answerAttributes [xowf::test_item::renaming_form_loader \ answer_attributes [$i instance_attributes]] if {$autograde} { set achieved_points [xowf::test_item::answer_manager achieved_points \ -answer_object $i -answer_attributes $answerAttributes] dict set achieved_points totalPoints $totalPoints #ns_log notice "==== www-print-answers: $userName achieved_points $achieved_points" #foreach detailInfo [dict get $achieved_points details] { # set questionObj [dict get $nameToQuestionObj [dict get $detailInfo attributeName]] # ns_log notice ".... $detailInfo item_id [$questionObj item_id]" #} } } } if {$withSignature} { set sha256 [ns_md string -digest sha256 $answerAttributes] set signatureString "
online-exam-actual_signature: $sha256
\n" set submissionSignature [$i property signature ""] if {$submissionSignature ne ""} { append signatureString "
#xowf.online-exam-submission_signature#: $submissionSignature
\n" } } else { set signatureString "" } set time [::xo::db::tcl_date [$i property _last_modified] tz_var] set pretty_date [clock format [clock scan $time] -format "%Y-%m-%d"] if {$filter_id ne "" && [:property proctoring] eq "t"} { set user_id [$i creation_user] set img_url [:pretty_link -query m=proctor-image&user_id=$user_id] set proctoring_dir [proctoring::folder \ -object_id ${:item_id} \ -user_id $user_id] set files [glob -nocomplain -directory $proctoring_dir *.*] #ns_log notice "proctoring_dir $proctoring_dir files $files" if {$revision_id ne ""} { set filtered_revisions [xowf::test_item::answer_manager revisions_up_to $revisions $revision_id] } else { set filtered_revisions $revisions } set start_date [ns_set get [lindex $filtered_revisions 0] creation_date] set end_date [ns_set get [lindex $filtered_revisions end] creation_date] set start_clock [clock scan [::xo::db::tcl_date $start_date tz_var]] set end_clock [clock scan [::xo::db::tcl_date $end_date tz_var]] set image "" ns_log notice "start date $start_date end_date $end_date / $start_clock $end_clock" foreach f $files { #ns_log notice "check: $f" if {[regexp {/([^/]+)-(\d+)[.](webm|png|jpeg)$} $f . type stamp ext]} { set inWindow [expr {$stamp >= $start_clock && $stamp <= $end_clock}] ns_log notice "parsed $type $stamp $ext $inWindow $stamp \ [clock format $stamp -format {%m-%d %H:%M:%S}] >= \ $start_clock ([expr {$stamp >= $start_clock}]) \ && $stamp <= $end_clock ([expr {$stamp <= $end_clock}])" if {$inWindow} { dict set image $stamp $type $ext } } } set markup "" foreach ts [lsort -integer [dict keys $image]] { #ns_log notice "ts $ts [dict get $image $ts]" append markup [subst {
[clock format $ts -format {%Y-%m-%d %H:%M:%S}]
}] append markup {
} foreach type {camera-image desktop-image} { if {[dict exists $image $ts $type]} { set ext [dict get $image $ts $type] append markup [subst {}] } } if {[dict exists $image $ts camera-audio]} { set ext [dict get $image $ts camera-audio] append markup [subst {}] } append markup
\n } set question_form [subst {
$question_form
$markup
}] } set view [expr {$as_student ? "student" : $filter_id ne "" ? "revision_overview" : "default"}] set gradingInfo [$grading_scheme print -achieved_points $achieved_points] set grandingPanel [expr {[dict exists $gradingInfo panel] ? [dict get $gradingInfo panel] : ""}] set runtime_panel [xowf::test_item::answer_manager runtime_panel \ -revision_id $revision_id \ -view $view \ -grading_info $grandingPanel \ $i] if {$autograde} { set grade [$grading_scheme grade -achieved_points $achieved_points] ns_log notice "CSV $userName\t[dict get $gradingInfo csv]" dict incr grade_dict $grade append grade_csv $userName\t[dict get $gradingInfo csv]\n } set heading "$userName · $fullName · $pretty_date" append HTML [subst {
[expr {$as_student ? "" : "

$heading

"}] $runtime_panel
$signatureString $question_form
}] if {$do_stream} { ns_write [lang::util::localize $HTML] set HTML " " } } } if {$HTML eq ""} { append HTML "#xowiki.no_data#" } if {!$as_student} { if {$autograde} { append HTML

[xowf::test_item::answer_manager grading_table -csv $grade_csv $grade_dict]

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

#xowiki.back#

\n" } #::xo::cc set_parameter template_file view-plain-master #::xo::cc set_parameter MenuBar 0 #xo::Page requireCSS /resources/xowf/test-item.css if {$do_stream} { ns_write [lang::util::localize $HTML] set HTML "" [$wf 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 {[:property _state] ne "published"} { :util_user_message -html -message "Cannot start answer workflow in this state" } else { set wf [xowf::test_item::answer_manager get_answer_wf [self]] set proctoring [:property proctoring] if {$proctoring ne "" && $proctoring} { set cLink [export_vars -base [:pretty_link] { {m proctor} {link "[:pretty_link -query m=proctor-answer]"} }] ::${:package_id} returnredirect $cLink } else { $wf www-create-or-use -parent_id [:item_id] } } } :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 [xowf::test_item::answer_manager get_answer_wf [self]] $wf www-create-or-use -parent_id [:item_id] } } :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 } :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]}] ${:package_id} return_page -adp $proctoring_template -variables { object_id object_url {preview_p true} examination_statement_p {check_active_p false} } } else { return [:www-view [subst {

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

}]] } } :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:integer ""] 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 } ######################################################################## # AJAX call "poll" # :proc www-poll {} { # # Return statistics about working and finished exams. # set wf [xowf::test_item::answer_manager get_answer_wf [self]] set answers [xowf::test_item::answer_manager get_answers $wf] set answered [xowf::test_item::answer_manager get_answers -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 } ######################################################################## # AJAX call "send-participant-message" # :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 [xo::cc user_id] \ -payload [list msg [ns_queryget msg] from [xo::cc user_id] urgency [ns_queryget urgency]] ns_return 200 text/plain ok ad_script_abort } } # # Local variables: # mode: tcl # tcl-indent-level: 2 # indent-tabs-mode: nil # End: