Index: openacs-4/packages/xowf/lib/inclass-exam.wf =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/lib/inclass-exam.wf,v diff -u -r1.1.2.69 -r1.1.2.70 --- openacs-4/packages/xowf/lib/inclass-exam.wf 2 May 2021 11:54:48 -0000 1.1.2.69 +++ openacs-4/packages/xowf/lib/inclass-exam.wf 5 May 2021 12:26:10 -0000 1.1.2.70 @@ -493,336 +493,31 @@ # 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 form_objs [:query_parameter fos:integer ""] - set export [:query_parameter export:boolean 0] - set grading [:query_parameter grading:alnum ""] + template::head::add_link -rel stylesheet -href /resources/xowf/test-item.css + + set as_student [:query_parameter as_student:boolean false] + set d [xowf::test_item::answer_manager render_answers \ + -as_student $as_student \ + -filter_id [:query_parameter id:integer ""] \ + -creation_user [:query_parameter creation_user:integer ""] \ + -revision_id [:query_parameter rid:integer ""] \ + -form_objs [:query_parameter fos:integer ""] \ + -export [:query_parameter export:boolean 0] \ + -grading [:query_parameter grading:alnum ""] \ + -with_grading_table [expr {!$as_student}] \ + [self]] - 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_points \ - -max_items [:property max_items ""] \ - $combined_form_info] - if {$form_objs ne "" && $form_objs ni [dict get $combined_form_info question_objs]} { - ns_log warning "inclass-exam: ignore invalid form_obj '$form_objs';" \ - "valid [dict get $combined_form_info question_objs]" + 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#" } - # - # 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 per default, 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). - # - if {$grading eq ""} { - set grading [expr {$totalPoints < 40 ? "wi1_noround" : "wi1"}] - } - - set grading_scheme ::xowf::test_item::grading::$grading - if {[info commands $grading_scheme] eq ""} { - set grading_scheme ::xowf::test_item::grading::wi1 - } - #ns_log notice "USE grading_scheme $grading_scheme" - - 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}] - - # - # Add a simple print button for the unaware that makes it easy - # to print the exam protocol to PDF and use e.g. a pdf-tool to - # annotate free text answers. - # - set HTML { - - } - - template::add_event_listener \ - -id print-button \ - -event click \ - -preventdefault=false \ - -script "window.print();" - append HTML [template::collect_body_scripts] - - ::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 {[llength $form_objs] > 0} { - append HTML "

#xowf.question#: [ns_quotehtml [[lindex $form_objs 0] title]]

\n" - } elseif {$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" - append HTML "

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

\n" - } else { - append 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 - } - - if {$export} { - set recutil [xowf::test_item::answer_manager recutil_create \ - -clear \ - -exam_id [$wf parent_id] \ - -fn [expr {$filter_id eq "" ? "all.rec" : "$filter_id.rec"}] - ] - } - - 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] - - #if {[$i state] ne "done"} { - # ns_log notice "online-exam: submission of $userName is not finished (state [$i 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 - $i set __form_objs $form_objs - set question_form [$i render_content] - - if {$export} { - xowf::test_item::answer_manager export_answer \ - -user_answers $i \ - -html $question_form \ - -combined_form_info $combined_form_info \ - -recutil $recutil - } - 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
-
-
- }] - } - if {[llength $form_objs] == 0} { - 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 - } - } else { - set runtime_panel "" - } - - 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 {$export} { - $recutil destroy - } - } - - 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] @@ -933,7 +628,9 @@ set object_url $link set examination_statement_p [expr {![string match *p.try_out_mode=1* $link]}] 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} { + 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}] @@ -1032,12 +729,13 @@ ::xowiki::includelet::personal-notification-messages message_add \ -notification_id ${:item_id} \ -to_user_id [${:package_id} query_parameter user_id:integer ""] \ - -payload [list msg [ns_queryget msg] from [xo::cc user_id] urgency [ns_queryget urgency]] + -payload [list msg [ns_queryget msg] \ + from [xo::cc user_id] \ + urgency [ns_queryget urgency]] ns_return 200 text/plain ok ad_script_abort } - } # Index: openacs-4/packages/xowf/tcl/test-item-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/tcl/test-item-procs.tcl,v diff -u -r1.7.2.129 -r1.7.2.130 --- openacs-4/packages/xowf/tcl/test-item-procs.tcl 2 May 2021 11:46:16 -0000 1.7.2.129 +++ openacs-4/packages/xowf/tcl/test-item-procs.tcl 5 May 2021 12:26:10 -0000 1.7.2.130 @@ -1115,6 +1115,9 @@ } } + :method dict_value {dict key {default ""}} { + expr {[dict exists $dict $key] ? [dict get $dict $key] : $default} + } } } namespace eval ::xowf::test_item { @@ -1329,6 +1332,7 @@ # # - runtime_panel # - render_answers_with_edit_history + # - render_answers # # - marked_results # - answers_panel @@ -1344,6 +1348,10 @@ # - state_periods # + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: create_workflow + #---------------------------------------------------------------------- :public method create_workflow { {-answer_workflow /packages/xowf/lib/online-exam-answer.wf} {-master_workflow en:Workflow.form} @@ -1424,8 +1432,10 @@ } } - ######################################################################## - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: get_label_from_options + #---------------------------------------------------------------------- :method get_label_from_options {value options} { foreach option $options { if {[lindex $option 1] eq $value} { @@ -1435,7 +1445,10 @@ return "" } - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: recutil_create + #---------------------------------------------------------------------- :public method recutil_create { -exam_id:integer {-fn "answers.rec"} @@ -1464,19 +1477,23 @@ return [::xo::recutil new -file $export_dir$fn] } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: export_answer + #---------------------------------------------------------------------- :public method export_answer { - -user_answers:object - -html:required -combined_form_info + -html:required -recutil:object,required + -submission:object } { # # Export the provided question and answer in GNU rectuil format. # - #ns_log notice "answers: [$user_answers serialize]" + #ns_log notice "answers: [$submission serialize]" - if {[$user_answers exists __form_fields]} { - set form_fields [$user_answers set __form_fields] + if {[$submission exists __form_fields]} { + set form_fields [$submission set __form_fields] } else { # # We do not have the newest version of xowiki, so locate the @@ -1493,16 +1510,16 @@ } set export_dict "" - set user [$user_answers set creation_user] + set user [$submission set creation_user] if {![info exists ::__running_ids]} { set ::__running_ids "" } if {![dict exists $::__running_ids $user]} { dict set ::__running_ids $user [incr ::__running_id] } - set seeds [$user_answers property seeds] - set instance_attributes [$user_answers set instance_attributes] + set seeds [$submission property seeds] + set instance_attributes [$submission set instance_attributes] set answer_attributes [lmap a $instance_attributes { if {![string match *_ $a]} {continue} set a @@ -1565,8 +1582,10 @@ } } - ######################################################################## - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: time_window_setup + #---------------------------------------------------------------------- :method time_window_setup {parentObj:object {-time_window:required}} { # # Check the provided time_window values, adjust it if necessary, @@ -1631,8 +1650,10 @@ } } - ######################################################################## - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: last_time_in_state + #---------------------------------------------------------------------- :public method delete_all_answer_data {obj:object} { # # Delete all instances of the answer workflow @@ -1645,7 +1666,10 @@ return $wf } - ######################################################################## + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: last_time_in_state + #---------------------------------------------------------------------- :public method delete_scheduled_atjobs {obj:object} { # # Delete previously scheduled atjobs @@ -1667,10 +1691,10 @@ } } - - - ######################################################################## - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: last_time_in_state + #---------------------------------------------------------------------- :public method get_answer_wf {obj:object} { # # return the workflow denoted by the property wfName in obj @@ -1681,8 +1705,10 @@ -forms [$obj property wfName]] } - ######################################################################## - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: last_time_in_state + #---------------------------------------------------------------------- :public method get_wf_instances { {-initialize false} {-orderby ""} @@ -1711,8 +1737,10 @@ -package_id [$wf package_id]] } - ######################################################################## - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: last_time_in_state + #---------------------------------------------------------------------- :public method get_answers {{-state ""} {-extra_attributes {}} wf:object} { # # Extracts wf instances as answers (e.g. extracting their @@ -1741,8 +1769,10 @@ return $results } - ######################################################################## - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: last_time_in_state + #---------------------------------------------------------------------- :public method get_duration {{-exam_published_time ""} revision_sets} { # # Get the duration from a set of revisions and return a dict @@ -1769,8 +1799,10 @@ return $r } - ######################################################################## - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: last_time_in_state + #---------------------------------------------------------------------- :public method get_IPs {revision_sets} { # # Get the IP addresses for the given revision set. Should be @@ -1786,8 +1818,10 @@ return [dict keys $IPs] } - ######################################################################## - + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: last_time_in_state + #---------------------------------------------------------------------- :public method revisions_up_to {revision_sets revision_id} { # # Return the revisions of the provided revision set up the @@ -1803,7 +1837,10 @@ }] } - ######################################################################## + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: last_time_in_state + #---------------------------------------------------------------------- :public method last_time_in_state {revision_sets -state:required -with_until:switch } { # # Loops through revision sets and retrieves the latest date @@ -1824,7 +1861,10 @@ return $result } - ######################################################################## + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: pretty_period + #---------------------------------------------------------------------- :method pretty_period {{-dayfmt %q} {-timefmt %H:%M} from to} { set from_day [lc_time_fmt $from $dayfmt] set from_time [lc_time_fmt $from $timefmt] @@ -1845,7 +1885,10 @@ return $period } - ######################################################################## + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: state_periods + #---------------------------------------------------------------------- :public method state_periods {revision_sets -state:required} { # # Return for the provided revision_sets the time ranges the @@ -1878,8 +1921,11 @@ return $periods } - ######################################################################## - :public method achieved_points {-answer_object:object -answer_attributes:required } { + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: achieved_points + #---------------------------------------------------------------------- + :public method achieved_points {-submission:object -answer_attributes:required } { # # This method has to be called after the instance was rendered, # since it uses the produced form_fields. @@ -1889,7 +1935,7 @@ set achievableTotalPoints 0 set details {} foreach a [dict keys $answer_attributes] { - set f [$answer_object lookup_form_field -name $a $all_form_fields] + set f [$submission lookup_form_field -name $a $all_form_fields] set points {} if {![$f exists test_item_points]} { ns_log warning "question $f [$f name] [$f info precedence] HAS NO POINTS" @@ -1917,7 +1963,10 @@ achievablePoints $achievableTotalPoints] } - ######################################################################## + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: runtime_panel + #---------------------------------------------------------------------- :public method runtime_panel { {-revision_id ""} {-view default} @@ -2041,8 +2090,147 @@ }] return $HTML } - ######################################################################## + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: render_submission=edit_history + #---------------------------------------------------------------------- + :method render_submission=edit_history { + {-submission:object} + {-examWf:object} + {-nameToQuestionObj} + } { + set last_answers {} + set rev_nr 1 + set q_nr 0 + set qnames "" + set report "" + set student_href [$examWf pretty_link -query m=print-answers&id=[$submission set item_id]] + + set revision_sets [$submission get_revision_sets -with_instance_attributes] + foreach s $revision_sets { + set msgs {} + set ia [ns_set get $s instance_attributes] + foreach key [dict keys $ia *_] { + if {![dict exists $qnames $key]} { + dict set qnames $key [incr q_nr] + } + set value [dict get $ia $key] + # + # Determine the question type + # + set form_obj [dict get $nameToQuestionObj $key] + set template_obj [$form_obj page_template] + if {[$template_obj name] eq "en:edit-interaction.wf"} { + set item_type [dict get [$form_obj instance_attributes] item_type] + } else { + switch [$template_obj name] { + en:TestItemShortText.form {set item_type ShortText} + en:TestItemText.form {set item_type Text} + default {set item_type unknown} + } + } + #ns_log notice "Template name = [$template_obj name] -> item_type '$item_type'" + + # + # For the time being, compute the differences just for short text questions + # + if {$item_type in {ShortText}} { + foreach answer_key [dict keys $value] { + set answer_value [string trim [dict get $value $answer_key]] + set what "" + set last_value [:dict_value $last_answers $answer_key ""] + if {$last_value ne ""} { + if {$answer_value eq ""} { + set what cleared + ns_log notice " ==> $answer_key: answer_value '$last_value' cleared in revision $rev_nr" + } elseif {$answer_value ne $last_value} { + set what updated + } + } else { + # last answer was empty + if {$answer_value ne ""} { + set what added + } + } + # + # Remember last answer values + # + dict set last_answers $answer_key $answer_value + if {$what ne ""} { + if {$what eq "cleared"} { + set answer_value $last_value + } + lappend msgs [subst { + + q[string map [list answer "" {*}$qnames] $answer_key] $what [ns_quotehtml '$answer_value'] + + }] + } + } + } else { + # + # Show the full content of the field + # + if {$value ne ""} { + lappend msgs [subst { + q[string map [list answer "" {*}$qnames] $key]: + [ns_quotehtml '$value'] + }] + } + } + } + append report [subst { + [format %02d $rev_nr]: + [join $msgs {; }]
+ }] + incr rev_nr + } + + append HTML [subst { + + [$submission set online-exam-userName] + [$submission set online-exam-fullName] + $report + + }] + + return $HTML + } + + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: render_submissions=edit_history + #---------------------------------------------------------------------- + :method render_submissions=edit_history { + {-examWf:object} + {-submissions:object} + } { + set combined_form_info [::xowf::test_item::question_manager combined_question_form $examWf] + set nameToQuestionObj [xowf::test_item::renaming_form_loader \ + name_to_question_obj_dict \ + [dict get $combined_form_info question_objs]] + # + # Sort items by user name + # + $submissions orderby online-exam-userName + + return [subst { +

Quick Submission Analysis

+ + + [join [lmap submission [$submissions children] { + :render_submission=edit_history \ + -submission $submission -examWf $examWf \ + -nameToQuestionObj $nameToQuestionObj}]] +
NameRevisions
+ }] + } + + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: render_answers_with_edit_history + #---------------------------------------------------------------------- :public method render_answers_with_edit_history { examWf:object } { @@ -2061,129 +2249,477 @@ if {$wf eq ""} { return "" } - set HTML "

Quick Submission Analysis

\n" - set combined_form_info [::xowf::test_item::question_manager combined_question_form $examWf] - set nameToQuestionObj [xowf::test_item::renaming_form_loader \ - name_to_question_obj_dict \ - [dict get $combined_form_info question_objs]] - set items [:get_wf_instances $wf] - 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]] + set submissions [:student_submissions -wf $wf] + set HTML [:render_submissions=edit_history -examWf $examWf -submissions $submissions] + + return $HTML + } + + ######################################################################## + :method render_proctor_images { + {-submission:object} + {-examWf:object} + {-revision_id} + } { + # + # Render for the submission i the proctor images. + # + + set user_id [$submission creation_user] + set img_url [$examWf pretty_link -query m=proctor-image&user_id=$user_id] + + set proctoring_dir [proctoring::folder \ + -object_id [$examWf 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 [:revisions_up_to $revisions $revision_id] + } else { + set filtered_revisions $revisions } - append HTML [subst { - - + 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 + } + return $markup + } + + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: student_submissions + #---------------------------------------------------------------------- + :method student_submissions { + {-creation_user:integer,0..1 ""} + {-filter_id:integer,0..1 ""} + {-revision_id:integer,0..1 ""} + {-wf:object} + } { + # + # Return an ordered composite built form all student submission, + # potentially filtered via the provided values. + # + if {$revision_id ne ""} { + # + # In case we have a revision_id, return this single + # revision. + # + set r [::xowiki::FormPage get_instance_from_db -revision_id $revision_id] + set submissions [::xo::OrderedComposite new -destroy_on_cleanup] + $submissions add $r + } else { + set submissions [:get_wf_instances \ + {*}[expr {$creation_user ne "" ? "-creation_user $creation_user" : ""}] \ + {*}[expr {$filter_id ne "" ? "-item_id $filter_id" : ""}] \ + $wf] + } + + # + # Provide additional attributes to the instances such as the + # userName and fullName. + # + foreach submission [$submissions children] { + $submission set online-exam-userName \ + [acs_user::get_element \ + -user_id [$submission creation_user] \ + -element username] + $submission set online-exam-fullName \ + [::xo::get_user_name [$submission creation_user]] + } + + return $submissions + } + + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: render_print_button + #---------------------------------------------------------------------- + :method render_print_button {} { + # + # Render a simple print button for the unaware that makes it + # easy to print the exam protocol to PDF and use e.g. a pdf-tool + # to annotate free text answers. The function is designed to + # work with streaming HTML output. + # + # @return HTML rendering + # + + template::add_event_listener \ + -id print-button \ + -event click \ + -preventdefault=false \ + -script "window.print();" + + return [subst { + + [template::collect_body_scripts] }] - $items orderby online-exam-userName - foreach i [$items children] { - set last_answers {} - set rev_nr 1 - set q_nr 0 - set qnames "" - set report "" - set student_href [$examWf pretty_link -query m=print-answers&id=[$i set item_id]] + } + + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: render_submission=exam_protocol + #---------------------------------------------------------------------- + :method render_submission=exam_protocol { + {-autograde:boolean false} + {-combined_form_info} + {-examWf:object} + {-filter_id:integer,0..1 ""} + {-form_objs:integer,0..n ""} + {-grading_scheme:object} + {-recutil:object,0..1 ""} + {-revision_id:integer,0..1 ""} + {-submission:object} + {-totalPoints:double} + {-runtime_panel_view default} + {-wf:object} + {-with_signature:boolean false} + {-with_exam_heading:boolean true} + } { - set revision_sets [$i get_revision_sets -with_instance_attributes] - foreach s $revision_sets { - set msgs {} - set ia [ns_set get $s instance_attributes] - foreach key [dict keys $ia *_] { - if {![dict exists $qnames $key]} { - dict set qnames $key [incr q_nr] - } - set value [dict get $ia $key] - # - # Determine the question type - # - set form_obj [dict get $nameToQuestionObj $key] - set template_obj [$form_obj page_template] - if {[$template_obj name] eq "en:edit-interaction.wf"} { - set item_type [dict get [$form_obj instance_attributes] item_type] - } else { - switch [$template_obj name] { - en:TestItemShortText.form {set item_type ShortText} - en:TestItemText.form {set item_type Text} - default {set item_type unknown} - } - } - #ns_log notice "Template name = [$template_obj name] -> item_type '$item_type'" + set userName [$submission set online-exam-userName] + set fullName [$submission set online-exam-fullName] - # - # For the time being, compute the differences just for short text questions - # - if {$item_type in {ShortText}} { - foreach answer_key [dict keys $value] { - set answer_value [string trim [dict get $value $answer_key]] - set what "" - if {[dict exists $last_answers $answer_key]} { - set last_value [dict get $last_answers $answer_key] - } else { - set last_value "" - } - if {$last_value ne ""} { - if {$answer_value eq ""} { - set what cleared - ns_log notice " ==> $answer_key: answer_value '$last_value' cleared in revision $rev_nr" - } elseif {$answer_value ne $last_value} { - set what updated - } - } else { - # last answer was empty - if {$answer_value ne ""} { - set what added - } - } - # - # Remember last answer values - # - dict set last_answers $answer_key $answer_value - if {$what ne ""} { - if {$what eq "cleared"} { - set answer_value $last_value - } - lappend msgs [subst { - - q[string map [list answer "" {*}$qnames] $answer_key] $what [ns_quotehtml '$answer_value'] - - }] - } - } - } else { - # - # Show the full content of the field - # - if {$value ne ""} { - lappend msgs [subst { - q[string map [list answer "" {*}$qnames] $key]: - [ns_quotehtml '$value'] - }] - } - } - } - append report [subst { - [format %02d $rev_nr]: - [join $msgs {; }]
- }] - incr rev_nr + #if {[$submission state] ne "done"} { + # ns_log notice "online-exam: submission of $userName is not finished (state [$submission state])" + # return "" + #} + + set revisions [$submission get_revision_sets] + if {[llength $revisions] <=1 } { + # just an initial revision + ns_log notice "online-exam: submission of $userName is empty. Ignoring." + return "" + } + + # + # 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 - append HTML [subst { - - - - + set achieved_points {} + xo::cc eval_as_user -user_id [$submission creation_user] { + $submission set __feedback_mode 2 + $submission set __form_objs $form_objs + set question_form [$submission render_content] + } + # + # Now, the question_form contains the rendered answer of the + # student. + # + + if {$recutil ne ""} { + :export_answer \ + -submission $submission \ + -html $question_form \ + -combined_form_info $combined_form_info \ + -recutil $recutil + } + if {$with_signature || $autograde} { + set answerAttributes [xowf::test_item::renaming_form_loader \ + answer_attributes [$submission instance_attributes]] + if {$autograde} { + set achieved_points [:achieved_points \ + -submission $submission \ + -answer_attributes $answerAttributes] + dict set achieved_points totalPoints $totalPoints + } + } + + if {$with_signature} { + set sha256 [ns_md string -digest sha256 $answerAttributes] + set signatureString "
online-exam-actual_signature: $sha256
\n" + set submissionSignature [$submission property signature ""] + if {$submissionSignature ne ""} { + append signatureString "
#xowf.online-exam-submission_signature#: $submissionSignature
\n" + } + } else { + set signatureString "" + } + + set time [::xo::db::tcl_date [$submission property _last_modified] tz_var] + set pretty_date [clock format [clock scan $time] -format "%Y-%m-%d"] + + # + # If we filter by student and the exam is proctored, display + # the procoring images as well. + # + if {$filter_id ne "" && [$examWf property proctoring] eq "t"} { + set markup [:render_proctor_images \ + -submission $submission \ + -examWf $examWf \ + -revision_id $revision_id] + set question_form [subst { +
+
+
$question_form
+
$markup
+
+
}] + } + if {$runtime_panel_view ne ""} { + set gradingInfo [$grading_scheme print -achieved_points $achieved_points] + set gradingPanel [:dict_value $gradingInfo panel ""] + set runtime_panel [:runtime_panel \ + -revision_id $revision_id \ + -view $runtime_panel_view \ + -grading_info $gradingPanel \ + $submission] + 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 + } + } else { + set runtime_panel "" } - append HTML "
NameRevisions
[$i set online-exam-userName][$i set online-exam-fullName]$report
\n" + set heading "$userName · $fullName · $pretty_date" + append HTML [subst { +
+
+ [expr {$with_exam_heading ? "

$heading

" : ""}] + $runtime_panel +
+ $signatureString + $question_form +
+ }] + return $HTML } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: render_answers + #---------------------------------------------------------------------- + :public method render_answers { + {-as_student:boolean false} + {-filter_id:integer,0..1 ""} + {-creation_user:integer,0..1 ""} + {-revision_id:integer,0..1 ""} + {-form_objs:integer,0..n ""} + {-export:boolean false} + {-grading:alnum,0..n ""} + {-with_grading_table:boolean false} + examWf:object + } { + # + # Return the answers in HTML format in a somewhat printer + # friendly way, e.g. as the exam protocol. + # + set combined_form_info [::xowf::test_item::question_manager combined_question_form $examWf] + set autograde [dict get $combined_form_info autograde] + set totalPoints [::xowf::test_item::question_manager total_points \ + -max_items [$examWf property max_items ""] \ + $combined_form_info] + set withSignature [$examWf property signature 0] + set examTitle [$examWf title] + set ctx [::xowf::Context require $examWf] + + set wf [:get_answer_wf $examWf] + if {$wf eq ""} { + return [list do_stream 0 HTML ""] + } + + if {$form_objs ne "" && $form_objs ni [dict get $combined_form_info question_objs]} { + ns_log warning "inclass-exam: ignore invalid form_obj '$form_objs';" \ + "valid [dict get $combined_form_info question_objs]" + set form_objs "" + } + # + # 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 per default, 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). + # + if {$grading eq ""} { + set grading [expr {$totalPoints < 40 ? "wi1_noround" : "wi1"}] + } + + set grading_scheme ::xowf::test_item::grading::$grading + if {[info commands $grading_scheme] eq ""} { + set grading_scheme ::xowf::test_item::grading::wi1 + } + #ns_log notice "USE grading_scheme $grading_scheme" + + set :grade_dict {} + set :grade_csv "" + + set items [:student_submissions \ + -creation_user $creation_user \ + -filter_id $filter_id \ + -revision_id $revision_id \ + -wf $wf] + # + # In case we have many items to render (which might take a + # while), use streaming mode. + # + set do_stream [expr {[llength [$items children]] > 100}] + + set HTML [:render_print_button] + ::xo::cc set_parameter template_file view-plain-master + ::xo::cc set_parameter MenuBar 0 + + if {[llength $form_objs] > 0} { + # + # Filter by questions. For the time being, we allow only a + # single question, ... and we take the first ones. + # + append HTML "

#xowf.question#: [ns_quotehtml [[lindex $form_objs 0] title]]

\n" + set runtime_panel_view "" + + } elseif {$as_student} { + # + # Show the student his own submission + # + 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" + append HTML "

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

\n" + set runtime_panel_view "student" + + } else { + # + # Provide the full protocol (or a subset of it) + # + append HTML "

#xowf.online-exam-protocol#

\n" + if {$filter_id ne ""} { + set runtime_panel_view "revision_overview" + } else { + set runtime_panel_view "default" + } + } + + if {$do_stream} { + # ns_log notice STREAM-[info level]-$::template::parse_level + # + # The following line is tricky: set on the parsing level the + # title of and context of the page, since this is needed by + # the streaming template. + # + uplevel #$::template::parse_level [subst {set title "$examTitle"; set context .}] + ad_return_top_of_page [ad_parse_template \ + -params [list context title] \ + [template::streaming_template]] + ns_write [subst { +
+
+

[ns_quotehtml $examTitle]

+ [lang::util::localize $HTML] + }] + set HTML "" + } + + if {$export} { + set recutil [xowf::test_item::answer_manager recutil_create \ + -clear \ + -exam_id [$wf parent_id] \ + -fn [expr {$filter_id eq "" ? "all.rec" : "$filter_id.rec"}] + ] + } else { + set recutil "" + } + + # + # Iterate over the items sorted by userName. + # + $items orderby online-exam-userName + foreach submission [$items children] { + + set html [:render_submission=exam_protocol \ + -submission $submission \ + -wf $wf \ + -examWf $examWf \ + -autograde $autograde \ + -combined_form_info $combined_form_info \ + -filter_id $filter_id \ + -form_objs $form_objs \ + -grading_scheme $grading_scheme \ + -recutil $recutil \ + -revision_id $revision_id \ + -totalPoints $totalPoints \ + -runtime_panel_view $runtime_panel_view \ + -with_exam_heading [expr {!$as_student}] \ + -with_signature $withSignature] + + if {$do_stream && $html ne ""} { + ns_write [lang::util::localize $html] + } else { + append HTML $html + } + } + + if {$export} { + $recutil destroy + } + + if {$with_grading_table && $autograde} { + append HTML

[:grading_table -csv ${:grade_csv} ${:grade_dict}]

+ } + + return [list do_stream $do_stream HTML $HTML] + } + + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: participant_result + #---------------------------------------------------------------------- :method participant_result { -obj:object answerObj:object @@ -2228,6 +2764,10 @@ return $answer } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: answer_form_field_objs + #---------------------------------------------------------------------- :method answer_form_field_objs {-clear:switch -wf:object form_info} { set key ::__test_item_answer_form_fields if {$clear} { @@ -2254,6 +2794,10 @@ } } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: grading_table + #---------------------------------------------------------------------- :public method grading_table {{-csv ""} grade_dict} { # # Produce HTML markup based on a dict with grades as keys and @@ -2279,6 +2823,10 @@ return $gradingTable } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: results_table + #---------------------------------------------------------------------- :public method results_table { -package_id:integer -items:object,required @@ -2288,6 +2836,11 @@ {-grading_scheme ::xowf::test_item::grading::wi1} wf:object } { + # + # Render the results in forma of a table and return HTML. + # Currently deactivated. + # + #set form_info [:combined_question_form -with_numbers $wf] set form_info [::xowf::test_item::question_manager combined_question_form $wf] set answer_form_field_objs [:answer_form_field_objs -wf $wf $form_info] @@ -2486,6 +3039,10 @@ return $HTML } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: participants_table + #---------------------------------------------------------------------- :public method participants_table { -package_id:integer -items:object,required @@ -2598,6 +3155,10 @@ return $dialogs$HTML } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: marked_results + #---------------------------------------------------------------------- :public method marked_results {-obj:object -wf:object form_info} { # # Return for every participant the individual results for an exam @@ -2617,6 +3178,10 @@ return $results } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: answers_panel + #---------------------------------------------------------------------- :public method answers_panel { {-polling:switch false} {-heading #xowf.submitted_answers#} @@ -2696,6 +3261,10 @@ return $answerStatus } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: prevent_multiple_tabs + #---------------------------------------------------------------------- :public method prevent_multiple_tabs { {-cookie_name multiple_tabs} } { @@ -2723,6 +3292,10 @@ }] } + #---------------------------------------------------------------------- + # Class: Answer_manager + # Method: countdown_timer + #---------------------------------------------------------------------- :public method countdown_timer { {-target_time:required} {-id:required} @@ -2739,7 +3312,8 @@ # client browser is set incorrectly. # set nowMs [clock milliseconds] - set nowIsoTime [clock format [expr {$nowMs/1000}] -format "%Y-%m-%dT%H:%M:%S"].[format %.3d [expr {$nowMs % 1000}]] + set nowIsoTime [clock format [expr {$nowMs/1000}] \ + -format "%Y-%m-%dT%H:%M:%S"].[format %.3d [expr {$nowMs % 1000}]] template::add_body_script -script [subst { var countdown_target_date = new Date('$target_time').getTime(); @@ -3408,7 +3982,7 @@ # # autograde ok on the question level # - } elseif {[dict exists $formAttributes auto_correct] && [dict get $formAttributes auto_correct]} { + } elseif {[:dict_value $formAttributes auto_correct 0]} { # # autograde ok on the form level # @@ -3581,9 +4155,7 @@ # list order is important, since it determines also the ordering # in the message. # - if {[dict exists $question_info show_max] - && [dict get $question_info show_max] ne "" - } { + if {[:dict_value $question_info show_max ""] ne ""} { foreach key {choice_options sub_questions} { if {[dict exists $question_info $key] && [dict get $question_info show_max] ne [dict get $question_info $key] @@ -3607,10 +4179,6 @@ return #xowf.shuffle_$m# } } - :method dict_value {dict key {default ""}} { - expr {[dict exists $dict $key] ? [dict get $dict $key] : $default} - } - :public method describe_form {{-asHTML:switch} form_obj} { # # Call for every form field of the form_obj the "describe"