Index: openacs-4/packages/xowf/lib/online-exam.wf =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/lib/online-exam.wf,v diff -u -r1.6 -r1.7 --- openacs-4/packages/xowf/lib/online-exam.wf 6 Jun 2018 12:42:44 -0000 1.6 +++ openacs-4/packages/xowf/lib/online-exam.wf 3 Sep 2024 15:37:54 -0000 1.7 @@ -1,57 +1,113 @@ # -*- Tcl -*- ######################################################################## -# Online-Exam workflow, designed similar to mobile-clicker +# Online-Exam workflow +# ==================== # -# This workflow lets a teacher choose from a predefined set of exam -# questions, which are typically open text questions. The user -# selects one ore several exam question via drag and drop The teacher -# can test the exam by entering test answers. The results are provided -# in form of a table. +# Defining exams: This workflow lets a teacher choose from a +# predefined set of exam questions, which are typically open text, +# short text, single or multiple choice questions. The teacher +# selects test questions via drag and drop. The teacher can perform a +# test run of the created exam, and can get the results via a result +# table. # -# When the teacher has finished testing of 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 teacher can -# see the incoming answers in the report by refreshing the page. When -# the exam is done, it is unpublished. The workflow offers the teacher -# to see a summary of the results in form of a table (an to download -# the results via csv), or he can produce a printer friendly version -# of the answers. +# Publishing and closing exams: When a teacher 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 teacher can see the incoming answers in the report by refreshing +# the page. When the exam is done, it is unpublished. The workflow +# offers the teacher to see a summary of the results in form of a +# table (an to download the results via csv), or the teacher 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 "Online Exam" -form en:online-exam.wf} +# +# The policy has to allow the following methods on FormPages: +# +# - "answer" (for students), +# - "edit" (for students), +# - "poll" (for teachers), +# - "print-answers" (for teachers), +# - "print-answer-table" (for teachers), +# - "delete" (for teachers), +# # Gustaf Neumann, Feb 2012 ######################################################################## -my set autoname 1 -my set debug 1 -my set masterWorkflow //xowf/de:workflow.wf +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 {Erstelle Prüfung} -Action publish -next_state published -label {Schalte Prüfung frei} -Action unpublish -next_state done -label {Schließe Prüfung} -Action republish -next_state published -label {Schalte Prüfung nochmals frei} -Action restart -next_state initial +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#} + {allow_translation:boolean,horizontal=true,default=f,label=#xowf.Allow_translation#,help_text=#xowf.Allow_translation_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#} + {grading:grading_scheme,required,default=none,label=#xowf.Grading_scheme#,help_text=#xowf.Grading_scheme_help_text#} +} -State initial -actions {select} -form en:select_question.form -view_method edit + +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# + +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 "Prüfungsentwurf (Prüfung nicht freigeschaltet)" + -form "#xowf.online-exam-draft_exam#" State published -actions {unpublish} -form_loader load_form -view_method edit \ - -form "Prüfung ist freigeschaltet" + -form "#xowf.online-exam-open#" State done -actions {republish restart} -form_loader load_form -view_method edit \ - -form "Die Prüfung ist geschlossen." + -form "#xowf.online-exam-closed#" ######################################################################## # Activate action select: After the teacher has selected the # exercises, the answer workflow is created. # select proc activate {obj} { - [[:wf_context] wf_container] create_answer_workflow $obj + xowf::test_item::answer_manager create_workflow \ + -answer_workflow /packages/xowf/lib/online-exam-answer.wf \ + $obj } ######################################################################## # Activate action publish: delete all responses for the workflow and # publish user participation link. # publish proc activate {obj} { - [[:wf_context] wf_container] delete_all_answer_data $obj + xowf::test_item::answer_manager delete_all_answer_data $obj :publish_link $obj } @@ -71,292 +127,301 @@ } ######################################################################## -# create_answer_workflow: create a workflow based on the template -# provided in this method for answering the question for the -# students. The name of the workflow is derived from the wokflow -# instance and recorded in the formfield "wfName". +# publish_link: make the user participation link available for the +# target group # -my proc create_answer_workflow {obj} { - my log "create_answer_workflow $obj" +Action instproc publish_link {obj} { + set aLink [$obj pretty_link -query m=answer] + util_user_message -html \ + -message "[$obj name] is available as [ns_quotehtml $aLink]" + # TODO: make it happen in the LMS +} - # first delete workflow and data, when it exists - if {[$obj property wfName] ne ""} { - set wf [my delete_all_answer_data $obj] - if {$wf ne ""} {$wf delete} - } +######################################################################## +# unpublish_link: remove the user participation link for the target +# group +# +Action instproc unpublish_link {obj} { + util_user_message -html -message "[$obj name] is closed" + # TODO: make it happen in the LMS +} - # create a fresh workflow - set wfName [$obj name].wf - $obj set_property -new 1 wfName $wfName +######################################################################## +# 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 wfMaster [my set masterWorkflow] - set wfTitle [$obj property _title] - set questionObjs [[:wf_context] get_questions] - set wfQuestionNames {} - set wfQuestionTitles {} - set attributeNames {} - foreach q $questionObjs { - set counter 0 - set prefix [lindex [split [$q name] :] end]-a - dom parse -simple -html [$q property form] doc - $doc documentElement root - if {$root ne ""} { - foreach node [$root selectNodes "//textarea|//input"] { - set newName $prefix[incr counter] - lappend attributeNames $newName + 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 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 testrun 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 there are answers, include the full menu. + # + set answers [xowf::test_item::answer_manager get_answer_attributes $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 menu "\[" + if {[acs_user::site_wide_admin_p -user_id [::xo::cc user_id]]} { + append menu "#xowf.online-exam-exam_instances#, " } + append menu \ + "#xowf.online-exam-protocol#, " \ + "#xowf.online-exam-results-table#\]" } - lappend wfQuestionNames ../[$q name] - lappend wfQuestionTitles [$q title] } - set wfID [$obj item_id] - set wfDef [subst -nocommands { - set wfID $wfID - set wfTitle "$wfTitle" - set wfQuestionNames [list $wfQuestionNames] - set wfQuestionTitles [list $wfQuestionTitles] - xowf::include /packages/xowf/lib/online-exam-answer.wf [list wfID wfTitle wfQuestionNames wfQuestionTitles] - }] - set attributeNames [join $attributeNames ,] + set extraAction "" + switch $state { + "created" { + append extraAction "
" \ + "#xowf.online-exam-try_out# " \ + "#xowf.testrun#" + } + "published" { + append extraAction "
" \ + "#xowf.online-exam-can_answer# " \ + "$aLink" + } + } - set WF [::xowiki::Weblog instantiate_forms \ - -parent_id [$obj parent_id] -package_id [$obj package_id] \ - -default_lang [$obj lang] \ - -forms $wfMaster] - set f [$WF create_form_page_instance \ - -name $wfName \ - -nls_language [$obj nls_language] \ - -publish_status ready \ - -parent_id [$obj item_id] \ - -package_id [$obj package_id] \ - -default_variables [list title $wfTitle] \ - -instance_attributes [list workflow_definition $wfDef \ - form_constraints "@table:_name,_state,$attributeNames @cr_fields:hidden"]] - $f save_new - my log "create_answer_workflow $obj DONE [$f pretty_link]" -} + if {$state in {published done}} { + if {$state eq "done"} { + set marked [xowf::test_item::answer_manager marked_results -obj $obj -wf $wf $combined_form_info] + } + set answerStats [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] + } else { + set answerStats "" + } -######################################################################## -# get_answer_wf: return the workflow denoted by the property wfName in obj -# -my proc get_answer_wf {obj} { - return [::xowiki::Weblog instantiate_forms \ - -parent_id [$obj item_id] -package_id [$obj package_id] \ - -default_lang [$obj lang] \ - -forms [$obj property wfName]] -} + append text "$answerStats\n" + append report "$menu $extraAction" -######################################################################## -# get_wf_instances: return the workflow instances -# -my proc get_wf_instances {{-initialize false} wf} { - return [::xowiki::FormPage get_form_entries \ - -base_item_ids [$wf item_id] -form_fields "" \ - -always_queried_attributes "*" -initialize $initialize \ - -publish_status all -package_id [$wf package_id]] -} + # Remove wrapping forms + regsub -all {]*>} $fullQuestionForm {} fullQuestionForm -######################################################################## -# delete_all_answer_data: delete all instances of the answer workflow -# -my proc delete_all_answer_data {obj} { - set wf [my get_answer_wf $obj] - if {$wf ne ""} { - set items [my get_wf_instances -initialize false $wf] - foreach i [$items children] { $i delete } - } - return $wf + set f [::xowiki::Form new \ + -destroy_on_cleanup \ + -set name en:question \ + -form [subst {
$text
$fullQuestionForm
$report
text/html}] \ + -text {} \ + -anon_instances t \ + -form_constraints $full_fc \ + ] } ######################################################################## -# publish_link: make the user participation link available for the -# target group # -Action instproc publish_link {obj} { - set aLink "[$obj pretty_link]?m=answer" - util_user_message -html -message "[$obj name] is available as [ns_quotehtml $aLink]" - # TODO: make it happen -} - -######################################################################## -# unpublish_link: remove the user participation link for the target -# group +# Object specific operations # -Action instproc unpublish_link {obj} { - util_user_message -html -message "[$obj name] is closed" - # TODO: make it happen -} +######################################################################## :object-specific { - ######################################################################## - # Extern callable methods - ######################################################################## + 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 + ######################################################################## - # delete: delete the workflow instance and all its associated data + # web-callable method "delete" # + # Delete the workflow instance and all its associated data. + # :proc www-delete {} { - set ctx [::xowf::Context require [self]] - [$ctx wf_container] delete_all_answer_data [self] + ::xo::cc set_query_parameter return_url [:property return_url] + xowf::test_item::answer_manager delete_all_answer_data [self] next } - :proc -deprecated delete {} { - :www-delete - } - ######################################################################## - # print-answers: print the answers in a somewhat printer friendly way + # web-callable method "print-answer-table" # - :proc www-print-answers {} { + # Print the answers in a somewhat printer friendly way. + # + :proc www-print-answer-table {} { set HTML "" set ctx [::xowf::Context require [self]] - set wf [[$ctx wf_container] get_answer_wf [self]] + set wf [xowf::test_item::answer_manager get_answer_wf [self]] if {$wf ne ""} { - set items [[$ctx wf_container] get_wf_instances $wf] - foreach i [$items children] { - set time [::xo::db::tcl_date [$i property _last_modified] tz_var] - set pretty_date [clock format [clock scan $time] -format "%Y-%m-%d %T"] - set uid [$i property _creation_user] - set text "

[acs_user::get_element -user_id $uid -element username] / [::xo::get_user_name $uid] / $pretty_date

" - set question_form [$i render_content] - set answer $question_form - set title [$i title] - array set ia [$i set instance_attributes] - regsub -all {
|} $answer {

} answer - regsub -all {
} $answer {} answer - regsub -all {IP\: (.*)} $title "$ia(ip)" title - append HTML "

$title

$text$answer
" + 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 \ + [self]] + $items2 destroy } - if {$HTML ne ""} { - ns_return 200 text/html " - - - - - $HTML - - " + if {$HTML eq ""} { + set HTML "#xowiki.no_data#" } else { - util_user_message -html -message "No answer data available" - ad_returnredirect [::xo::cc url] + 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" - :proc -deprecated print-answers {} { - :www-print-answers + xo::Page requireCSS /resources/xowf/test-item.css + :www-view $HTML } ######################################################################## - # answer: answer the exam; this is a convenience routine to shorten - # the published URL; make sure, that no-one trys to start the answer - # workflow in a state different to "published" + # web-callable method "print-answers" # - :proc www-answer {} { - if {[my property _state] ne "published"} { - util_user_message -html -message "Cannot start answer workflow in this state" - } else { - set ctx [::xowf::Context require [self]] - set wf [[$ctx wf_container] get_answer_wf [self]] - $wf www-create-or-use -parent_id [my item_id] - } - } - - :proc -deprecated answer {args} { - ad_log warning "????? who is calling me?" - :www-answer {*}$args - } - - ######################################################################## + # Print the answers in a somewhat printer friendly way. # - # Helper methods for the workflow context - # - ######################################################################## + :proc www-print-answers {} { + set HTML "" + 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 $wf] + set withSignature [expr {[dict exists ${:instance_attributes} signature] + ? [dict get ${:instance_attributes} signature] + : 0 }] + set examTitle ${:title} + set filter_submission_id [[$wf package_id] query_parameter id:integer ""] - set ctx [:wf_context] + 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] - ######################################################################## - # form loader: create dynamically a form containing the disabled - # questions and the survey results (the results can be refreshed) - # - $ctx proc load_form {title} { - set state [my property _state] + if {[$i state] ne "done"} { + ns_log notice "online-exam: submission of $userName is not finished (state [$i state])" + continue + } + if {$filter_submission_id ne "" && [$i item_id] ne $filter_submission_id} { + continue + } - set questions [my get_questions] - set counter 0 - set fullQuestionForm "" - foreach q $questions { - append fullQuestionForm \ - "

Frage [incr counter]

\n" \ - [$q property form] - } + # + # The call to "render_content" calls actually the + # "summary_form" of online-exam-answer.wf when the submit + # instance is in state "done". We set the __feedback_mode to + # get the auto-correction included. + # + $i set __feedback_mode 2 + set question_form [$i render_content] - # disable fields, remove wrapping form - regsub -all {]*>} $fullQuestionForm {} fullQuestionForm + if {$withSignature} { + set answerAttributes [xowf::test_item::renaming_form_loader \ + answer_attributes [$i instance_attributes]] + 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 text "

$title

" - set obj ${:object} + set time [::xo::db::tcl_date [$i property _last_modified] tz_var] + set pretty_date [clock format [clock scan $time] -format "%Y-%m-%d %T"] - set wf [[:wf_container] get_answer_wf $obj] - if {$wf eq ""} { - my msg "cannot get current workflow for [$obj name]" - set lLink "." - set tLink "." - set aLink "." - set pLink "." - set menu "" - } else { - set wf_pretty_link [$wf pretty_link] - set tLink "$wf_pretty_link?m=create-new&p.return_url=[::xo::cc url]&p.try_out_mode=1" - set lLink "$wf_pretty_link?m=list" - set aLink "[$obj pretty_link]?m=answer" - set pLink "[$obj pretty_link]?m=print-answers" - #util_user_message -html -message "$survey is available as $pLink" - set menu "\[refresh,\ - listing,\ - print\]" + append HTML "\n
" \ + "

$userName · $fullName · $pretty_date · IP [$i property ip]

" \ + $signatureString \ + $question_form \ + "
\n" + } } - set extraAction "" - switch [my property _state] { - "created" {set extraAction "
Do you want to try out the exam?"} - "published" {set extraAction "
Students can now answer via $aLink"} + if {$HTML eq ""} { + set HTML "#xowiki.no_data#" + } else { + set HTML "

#xowf.online-exam-protocol#

$HTML" } - append text "$menu $extraAction\n" + set return_url [[$wf package_id] 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 + :www-view $HTML + } - set style "background: #cccccc; padding: 10px; margin:10px;" - set report "" - set wfName [my property wfName] - if {$wfName ne ""} {set report "{{form-stats -parent_id [$obj item_id] -form $wfName}}\n"} - append report "
$menu" - - set f [::xowiki::Form new \ - -set name en:quesiton \ - -form [subst {
$text
$fullQuestionForm
$report
text/html}] \ - -text {} \ - -anon_instances t \ - -form_constraints {@cr_fields:hidden} \ - ] + ######################################################################## + # web-callable method "answer" + # + # Create or use an answering workflow for the current exam. This is + # a convenience routine to shorten the published URL. + # + :proc www-answer {} { + # + # 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]] + $wf www-create-or-use -parent_id [:item_id] + } } ######################################################################## - # get_question: load and initialize the interaction forms + # AJAX call "poll" # - $ctx proc get_questions {} { - set questionNames [join [my property question] |] - set questionForms [::xowiki::Weblog instantiate_forms \ - -parent_id [${:object} parent_id] -package_id [${:object} package_id] \ - -default_lang [${:object} lang] \ - -forms $questionNames] - if {[llength $questionForms] < 1} {error "unknown form $questionNames"} - #my msg "questionNames '$questionNames', questionForms 'questionForms'" - return $questionForms + # Return statistics about working and finished exams. + # + :proc www-poll {} { + set wf [xowf::test_item::answer_manager get_answer_wf [self]] + set answers [xowf::test_item::answer_manager get_answer_attributes $wf] + set answered [xowf::test_item::answer_manager 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 } }