::xo::library doc { Test Item procs - support for different kind of tests and exercises. @author Gustaf Neumann } ::xo::db::require package xowiki ::xo::library require xowf-procs ::xo::library require -package xowiki menu-procs ::xo::library require -package xowiki form-field-procs namespace eval ::xowiki::formfield { ########################################################### # # ::xowiki::formfield::FormGeneratorField # ########################################################### Class create FormGeneratorField -superclass CompoundField -parameter { } FormGeneratorField set abstract 1 FormGeneratorField instproc pretty_value {v} { return [${:object} property form ""] } FormGeneratorField instproc render_input {} { ::xo::Page requireCSS /resources/xowf/form-generator.css next } ########################################################### # # ::xowiki::formfield::TestItemField # ########################################################### Class create TestItemField -superclass FormGeneratorField -parameter { {feedback_level full} {auto_correct:boolean false} {nr_attachments 15} } -ad_doc { Abstract class for defining common attributes for all Test Item fields. @param feedback_level "full", "single", or "none" @param auto_correct boolean to let user add auto correction fields } TestItemField set abstract 1 TestItemField instproc text_attachments {} { set html "" if {[:exists_named_sub_component attachments]} { set attachments_ff [:get_named_sub_component attachments] set attachments_count [$attachments_ff count_values [$attachments_ff value]] set attachments_label [expr {$attachments_count > 1 ? "#general-comments.Attachments# ($attachments_count)" : "#attachments.Attachment#"}] for {set i 1} {$i <= $attachments_count} {incr i} { set label [lindex [dict get [:get_named_sub_component_value -from_repeat attachments $i] name] 0] set encoded_label [ns_urlencode $label] append attachments_links \ {
} if {$attachments_count > 0} { append html "since this causes a newline in the checkbox label regexp {^\s*(
)(.*)$} $text . . text regexp {^(.*)(
)\s*$} $text . text . lappend options [list $text [incr count]] lappend correct [dict get $value $fieldName.correct] lappend solution [dict get $value $fieldName.solution] } dict set fc_dict richtext 1 dict set fc_dict answer $correct dict set fc_dict options $options dict set fc_dict descriptions $solution dict set fc_dict shuffle_kind [${:parent_field} get_named_sub_component_value shuffle] dict set fc_dict grading [${:parent_field} get_named_sub_component_value grading] dict set fc_dict show_max [${:parent_field} get_named_sub_component_value show_max] set interaction_class [expr {${:multiple} ? "mc_interaction" : "sc_interaction"}] set twocol [:twocol_layout] append form \ "\n" set widget [expr {${:multiple} ? "checkbox" : "radio"}] set fc {} lappend fc \ answer:[:dict_to_fc -type $widget $fc_dict] \ @categories:off @cr_fields:hidden #ns_log notice "mc_interaction2 $form\n$fc" ${:object} set_property -new 1 form $form ${:object} set_property -new 1 form_constraints $fc set anon_instances true ;# TODO make me configurable ${:object} set_property -new 1 anon_instances $anon_instances ${:object} set_property -new 1 auto_correct ${:auto_correct} ${:object} set_property -new 1 has_solution false } # # ::xowiki::formfield::mc_field # Class create mc_field -superclass TestItemField -parameter { {n ""} } mc_field instproc initialize {} { if {${:__state} ne "after_specs"} return # # Create component structure. # set widget [test_item set richtextWidget] # {correct {boolean_checkbox,horizontal=true,label=#xowf.Correct#,form_item_wrapper_CSSclass=form-inline}} :create_components [subst { {text {$widget,height=50px,label=#xowf.choice_option#,plugins=OacsFs}} {correct {boolean_checkbox,horizontal=true,default=f,label=#xowf.Correct#,form_item_wrapper_CSSclass=form-inline}} {solution {textarea,rows=2,label=#xowf.Solution#,form_item_wrapper_CSSclass=form-inline}} }] set :__initialized 1 } } namespace eval ::xowiki::formfield { ########################################################### # # ::xowiki::formfield::upload_interaction # ########################################################### Class create upload_interaction -superclass TestItemField -parameter { } upload_interaction set auto_correct false upload_interaction instproc initialize {} { if {${:__state} ne "after_specs"} { return } set widget [test_item set richtextWidget] :create_components [subst { {text {$widget,height=150px,label=#xowf.exercise-text#,plugins=OacsFs}} {attachments {[:attachments_widget ${:nr_attachments}]}} }] set :__initialized 1 } upload_interaction instproc convert_to_internal {} { next set intro_text [:get_named_sub_component_value text] set max_nr_submission_files [${:parent_field} get_named_sub_component_value max_nr_submission_files] #dict set file_dict choose_file_label "Datei hochladen" if {$max_nr_submission_files > 1} { dict set file_dict repeat 1..$max_nr_submission_files dict set file_dict repeat_add_label #xowiki.form-repeatable-add-another-file# dict set file_dict label #xowf.online-exam-submission_files# } else { dict set file_dict label #xowf.online-exam-submission_file# } append intro_text [:text_attachments] set twocol [:twocol_layout] append form \ "\n" append fc \ "@categories:off @cr_fields:hidden\n" \ "{answer:[:dict_to_fc -type file $file_dict]}" ${:object} set_property -new 1 form $form ${:object} set_property -new 1 form_constraints $fc set anon_instances true ;# TODO make me configurable ${:object} set_property -new 1 anon_instances $anon_instances ${:object} set_property -new 1 auto_correct [[self class] set auto_correct] ${:object} set_property -new 1 has_solution false } } namespace eval ::xowiki::formfield { ########################################################### # # ::xowiki::formfield::test_section # ########################################################### Class create test_section -superclass {form_page} -parameter { {multiple true} } test_section instproc pretty_value {v} { return [${:object} property form ""] } test_section instproc convert_to_internal {} { # # Build a complex form composed of the specified form pages names # contained in the value of this field. The form-fields have to # be renamed. This affects the input field names in the form and # the form constraints. We use the item_id contained pages as the # prefix for the form-fields. This method must be most likely # extended for other question types. # set form "\n" ${:object} set_property -new 1 form $form ${:object} set_property -new 1 form_constraints $fc set anon_instances true ;# TODO make me configurable ${:object} set_property -new 1 anon_instances $anon_instances # for mixed test sections (e.g. text interaction and mc), we have # to combine the values of the items ${:object} set_property -new 1 auto_correct true ;# should be computed ${:object} set_property -new 1 has_solution true ;# should be computed #:msg "fc=$fc" } } ############################################################################ # Generic Assement interface ############################################################################ namespace eval ::xowf::test_item { nx::Class create AssessmentInterface { # # Abstract class for common functionality # :method assert_assessment_container {o:object} { set ok [expr {[$o is_wf_instance] == 0 && [$o is_wf] == 1}] if {!$ok} { ns_log notice "NO ASSESSMENT CONTAINER [$o title]" ns_log notice "NO ASSESSMENT CONTAINER page_template [[$o page_template] title]" ns_log notice "NO ASSESSMENT CONTAINER iswfi [$o is_wf_instance] iswf [$o is_wf]" ns_log notice "[$o serialize]" error "'[lindex [info level -1] 0]': not an assessment container" } } :method assert_assessment {o:object} { if {[llength [$o property question]] == 0} { ns_log notice "NO ASSESSMENT [$o title]" ns_log notice "NO ASSESSMENT page_template [[$o page_template] title]" ns_log notice "NO ASSESSMENT iswfi [$o is_wf_instance] iswf [$o is_wf]" ns_log notice "[$o serialize]" error "'[lindex [info level -1] 0]': object has no questions" } } :method assert_answer_instance {o:object} { # we could include as well {[$o property answer] ne ""} in case we initialize it set ok [expr {[$o is_wf_instance] == 1 && [$o is_wf] == 0}] if {!$ok} { ns_log notice "NO ANSWER [$o title]" ns_log notice "NO ANSWER page_template [[$o page_template] title]" ns_log notice "NO ANSWER iswfi [$o is_wf_instance] iswf [$o is_wf]" ns_log notice "[$o serialize]" error "'[lindex [info level -1] 0]': not an answer instance" } } :method dict_value {dict key {default ""}} { expr {[dict exists $dict $key] ? [dict get $dict $key] : $default} } } } namespace eval ::xowf::test_item { nx::Class create Renaming_form_loader -superclass AssessmentInterface { # # Form loader that renames "generic" form-field-names as provided # by the test-item form-field classes (@answer@) into names based # on the form name, such that multiple of these form names can be # processed together without name clashes. # # - answer_attributes # - answer_for_form # - answers_for_form # - form_name_based_attribute_stem # - name_to_question_obj_dict # # - get_form_object # - rename_attributes # :method map_form_constraints {form_constraints oldName newName} { # # Rename form constraints starting with $oldName into $newName. # Handle as well "answer=$oldName" form constraint properties. # return [lmap f $form_constraints { #:msg check?'$f' if {[string match "${oldName}*" $f]} { regsub $oldName $f $newName f if {[string match "*answer=$oldName*" $f]} { regsub answer=$oldName $f answer=$newName f #:log "MAP VALUE=answer=$oldName => answer=$newName " } } set f }] } :public method form_name_based_attribute_stem {formName} { # # Produce from the provided 'formName' an attribute stem for the # input fields of this form. # set strippedName [lindex [split $formName :] end] regsub -all -- {[-]} $strippedName _ stem return ${stem}_ } :public method name_to_question_obj_dict {question_objs} { # # Produce a dict for attribute name to question_obj # set nameToQuestionObjDict {} foreach o $question_objs { dict set nameToQuestionObjDict [:form_name_based_attribute_stem [$o name]] $o } return $nameToQuestionObjDict } :public method answer_attributes {instance_attributes} { # # Return all form-loader specific attributes from # instance_attributes. # set result "" foreach key [lsort [dict keys $instance_attributes]] { if {[string match *_ $key]} { lappend result $key [dict get $instance_attributes $key] } } return $result } :public method answer_for_form {formName instance_attributes} { # # Return answer for the provided formName from # instance_attributes of a single object. # set result "" set stem [:form_name_based_attribute_stem $formName] set answerAttributes [:answer_attributes $instance_attributes] #ns_log notice "answer_for_form\ninstance_attributes $instance_attributes" if {[dict exists $answerAttributes $stem]} { set value [dict get $answerAttributes $stem] if {$value ne ""} { lappend result $value } } return $result } :public method answers_for_form {formName answers} { # # Return a list of dicts for the provided formName from the # answers (as returned from [answer_manager get_answers ...]). # set stem [:form_name_based_attribute_stem $formName] set result "" foreach answer $answers { set value answer_for_form set answerAttributes [dict get $answer answerAttributes] if {[dict exists $answerAttributes $stem]} { set value [dict get $answerAttributes $stem] if {$value ne ""} { lappend result [list item [dict get $answer item] value $value] } } } return $result } :public method rename_attributes {form_obj:object} { # # Perform attribute renaming in the provided form_obj and return # this form_obj. In essence, this changes the generic "@answer@" # value in the form and in the form constraints to a name based # on the form name. # set form [$form_obj get_property -name form] set fc [$form_obj get_property -name form_constraints] # # Map "answer" to a generic name in the form "@answer@" and in the # form constraints. # set newName [:form_name_based_attribute_stem [$form_obj name]] regsub -all -- {@answer} $form @$newName form set fc [:map_form_constraints $fc "answer" $newName] set disabled_fc [lmap f $fc { if {[string match "$newName*" $f]} { append f ,disabled=true } set f }] lappend fc @cr_fields:hidden lappend disabled_fc @cr_fields:hidden #:msg fc=$fc $form_obj set_property -new 1 form $form $form_obj set_property -new 1 form_constraints $fc $form_obj set_property -new 1 disabled_form_constraints $disabled_fc #ns_log notice "RENAMED form $form\n$fc\n$disabled_fc" return $form_obj } :public method -deprecated get_form_object {ctx:object form_name} { # # Return the form object based on the provided form name. This # function performs attribute renaming on the returned form # object. # set form_id [$ctx default_load_form_id $form_name] set obj [$ctx object] set form_obj [::xo::db::CrClass get_instance_from_db -item_id $form_id] return [:rename_attributes $form_obj] } } Renaming_form_loader create renaming_form_loader } namespace eval ::xowf::test_item { ad_proc -private fc_to_dict {form_constraints} { # # Convert from form_constraint syntax to a dict. This is just a # partial implementation, since form constraints are interprted # from left to right, changing types, etc., which is not # supported here. # foreach fc $form_constraints { #ns_log notice "... fc_to_dict works on <$fc>" if {[regexp {^([^:]+):(.*)$} $fc _ field_name definition]} { if {[string match @* $field_name]} continue set elements [split $definition ,] dict set result $field_name type [lindex $elements 0] foreach s [lrange $elements 1 end] { switch -glob -- $s { *=* { set p [string first = $s] set attribute [string range $s 0 $p-1] set value [::xowiki::formfield::FormField fc_decode [string range $s $p+1 end]] dict set result $field_name $attribute $value } default { ns_log notice "... fc_to_dict ignores <$s>" } } } dict set result $field_name definition $definition } } return $result } nx::Class create Answer_manager -superclass AssessmentInterface { # # Public API: # # - create_workflow # - delete_all_answer_data # - get_answer_wf # - get_wf_instances # - get_answers # # - runtime_panel # - render_answers_with_edit_history # - render_answers # # - marked_results # - answers_panel # - results_table # - grading_table # - grade # - participants_table # # - get_duration # - get_IPs # - revisions_up_to # - last_time_in_state # - 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} parentObj:object } { # # 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 workflow instance and # recorded in the formfield "wfName". # #:log "create_answer_workflow $parentObj" # first delete workflow and data, when it exists if {[$parentObj property wfName] ne ""} { set wf [:delete_all_answer_data $parentObj] if {$wf ne ""} {$wf delete} } # # Create a fresh workflow (e.g. instance of the online-exam, # inclass-quiz, ...). # set wfName [$parentObj name].wf $parentObj set_property -new 1 wfName $wfName set wfTitle [$parentObj property _title] set questionObjs [::xowf::test_item::question_manager question_objs $parentObj] set wfQuestionNames {} set wfQuestionTitles {} set attributeNames {} foreach form_obj $questionObjs { lappend attributeNames [xowf::test_item::renaming_form_loader \ form_name_based_attribute_stem [$form_obj name]] lappend wfQuestionNames ../[$form_obj name] lappend wfQuestionTitles [$form_obj title] } set wfID [$parentObj item_id] set wfDef [subst -nocommands { set wfID $wfID set wfQuestionNames [list $wfQuestionNames] xowf::include $answer_workflow }] set attributeNames [join $attributeNames ,] #:log "create workflow by filling out form '$master_workflow'" set WF [::xowiki::Weblog instantiate_forms \ -parent_id [$parentObj parent_id] \ -package_id [$parentObj package_id] \ -default_lang [$parentObj lang] \ -forms $master_workflow] set fc "" append fc \ "@table:_item_id,_state,$attributeNames,_last_modified " \ "@table_properties:view_field=_item_id " \ @cr_fields:hidden set wf [$WF create_form_page_instance \ -name $wfName \ -nls_language [$parentObj nls_language] \ -publish_status ready \ -parent_id [$parentObj item_id] \ -package_id [$parentObj package_id] \ -default_variables [list title $wfTitle] \ -instance_attributes [list workflow_definition $wfDef \ form_constraints $fc]] $wf save_new #ns_log notice "create_answer_workflow $wf DONE [$wf pretty_link] IA <[$wf instance_attributes]>" #ns_log notice "create_answer_workflow parent $parentObj IA <[$parentObj instance_attributes]>" set time_window [$parentObj property time_window] if {$time_window ne ""} { :time_window_setup $parentObj -time_window $time_window } } #---------------------------------------------------------------------- # Class: Answer_manager # Method: get_label_from_options #---------------------------------------------------------------------- :method get_label_from_options {value options} { foreach option $options { if {[lindex $option 1] eq $value} { return [lindex $option 0] } } return "" } #---------------------------------------------------------------------- # Class: Answer_manager # Method: recutil_create #---------------------------------------------------------------------- :public method recutil_create { -exam_id:integer {-fn "answers.rec"} -clear:switch } { # # Create recfile # # @see http://www.gnu.org/software/recutils/ # set export_dir [acs_root_dir]/log/exam-exports/$exam_id/ if {![file isdirectory $export_dir]} { file mkdir $export_dir } if {$clear && [file exists $export_dir$fn]} { file delete -- $export_dir$fn } # # If we have no recutils, create for the time being a stub # if {![nsf::is class ::xo::recutil]} { ns_log warning "no recutil class available" set r [::xotcl::Object new -proc ins args {;}] return $r } return [::xo::recutil new -file $export_dir$fn] } #---------------------------------------------------------------------- # Class: Answer_manager # Method: export_answer #---------------------------------------------------------------------- :public method export_answer { -combined_form_info -html:required -recutil:object,required -submission:object } { # # Export the provided question and answer in GNU rectuil format. # #ns_log notice "answers: [$submission serialize]" 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 # objs the hard way based on the naming convention. # set form_field_objs [lmap f [::xowiki::formfield::FormField info instances -closure] { if {![string match *_ [$f name]]} {continue} set f }] foreach form_field_obj $form_field_objs { dict set form_fields [$form_field_obj name] $form_field_obj } ns_log notice "export_answers: old style form_fields: $form_fields" } set export_dict "" 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 [$submission property seeds] set instance_attributes [$submission set instance_attributes] set answer_attributes [lmap a $instance_attributes { if {![string match *_ $a]} {continue} set a }] #ns_log notice "export_answers: combined_form_info: $combined_form_info" #set title_infos [dict get $combined_form_info title_infos] # # Get the question dict, which is a mapping between question # names and form_obj_ids. # set question_dict [renaming_form_loader name_to_question_obj_dict \ [dict get $combined_form_info question_objs]] # ns_log notice "export_answers: question_dict: $question_dict" set form_constraints [lsort -unique [dict get $combined_form_info form_constraints]] set fc_dict [fc_to_dict $form_constraints] #ns_log notice "... form_constraints ([llength $form_constraints]) $form_constraints" #ns_log notice ".... dict $fc_dict" # # Every answer_attribute contains the answer to a test_item # (which potentially sub answers). # foreach a $answer_attributes { #ns_log notice "answers <[dict get $instance_attributes $a]>" foreach {alternative_id answer} [dict get $instance_attributes $a] { set alt_value [lindex [split $alternative_id .] 1] set form_obj [dict get $question_dict $a] #set ff [dict get $form_fields $a] #ns_log notice "answer $a: [dict get $instance_attributes $a] [$ff serialize]" #ns_log notice "answer $a: form_obj [$form_obj serialize]" set form_obj_ia [$form_obj instance_attributes] #ns_log notice "answer $a: [dict get $instance_attributes $a] [dict keys [dict get $form_obj_ia question]]" #ns_log notice "INTERACTION [dict get [dict get $form_obj_ia question] question.interaction]" set intro [dict get [dict get [dict get $form_obj_ia question] question.interaction] question.interaction.text] #ns_log notice "TEXT $intro" #set question_title [question_manager question_property $form_obj title] #set question_minutes [question_manager question_property $form_obj minutes] #ns_log notice "answer $a: [dict get $instance_attributes $a] [dict keys [dict get $form_obj_ia question]]" #dict set export_dict name $a dict set export_dict name $alternative_id dict set export_dict user_id $user dict set export_dict running_id [dict get $::__running_ids $user] dict set export_dict question_obj $form_obj dict set export_dict question_title [$form_obj title] dict set export_dict question_intro [ns_striphtml $intro] dict set export_dict question_minutes [dict get $fc_dict $a test_item_minutes] dict set export_dict question_points [dict get $fc_dict $a test_item_points] dict set export_dict question_text [ns_striphtml [:get_label_from_options $alt_value [dict get $fc_dict $a options]]] #dict set export_dict options [dict get $fc_dict $a options] dict set export_dict answer $answer ns_log notice "answer $a: DICT $export_dict" #ns_log notice "avail $a: [dict get $fc_dict $a]" $recutil ins $export_dict } } } #---------------------------------------------------------------------- # 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, # and make sure, according atjobs are provided. # set dtstart [dict get $time_window time_window.dtstart] set dtend [dict get $time_window time_window.dtend] if {$dtstart ne ""} { set total_minutes [question_manager total_minutes_for_exam -manager $parentObj] ns_log notice "#### create_workflows: atjobs for time_window <$time_window> total-mins $total_minutes" set start_clock [clock scan $dtstart -format %Y-%m-%dT%H:%M] if {$dtend eq ""} { # # No end given. set it to start + exam time + 5 minutes # set end_clock [expr {$start_clock + ($total_minutes + 5) * 60}] set new_dtend [clock format $end_clock -format %H:%M] ns_log notice "#### no dtend given. set it from $dtend to $new_dtend" } else { set end_date [clock format $start_clock -format %Y-%m-%d]T$dtend set end_clock [clock scan $end_date -format %Y-%m-%dT%H:%M] if {($end_clock - $start_clock) < ($total_minutes * 60)} { # # The specified end time is too early. Set it to start + # exam time + 5 minutes # set end_clock [expr {$start_clock + ($total_minutes + 5)*60}] set new_dtend [clock format $end_clock -format %H:%M] ns_log notice "#### dtend is too early. Move it from $dtend to $new_dtend" } else { set new_dtend $dtend } } if {$new_dtend ne $dtend} { ns_log notice "#### create_workflows: must change dtend from <$dtend> to <$new_dtend>" set ia [$parentObj instance_attributes] dict set time_window time_window.dtend $new_dtend dict set ia time_window $time_window #ns_log notice "SAVE updated ia <${:instance_attributes}>" $parentObj update_attribute_from_slot [$parentObj find_slot instance_attributes] $ia } # # Delete previously scheduled atjobs # :delete_scheduled_atjobs $parentObj # # Schedule new atjobs # $parentObj schedule_action \ -time [clock format $start_clock -format "%Y-%m-%d %H:%M:%S"] \ -action publish $parentObj schedule_action \ -time [clock format $end_clock -format "%Y-%m-%d %H:%M:%S"] \ -action unpublish } } #---------------------------------------------------------------------- # Class: Answer_manager # Method: last_time_in_state #---------------------------------------------------------------------- :public method delete_all_answer_data {obj:object} { # # Delete all instances of the answer workflow # set wf [:get_answer_wf $obj] if {$wf ne ""} { set items [:get_wf_instances -initialize false $wf] foreach i [$items children] { $i delete } } return $wf } #---------------------------------------------------------------------- # Class: Answer_manager # Method: last_time_in_state #---------------------------------------------------------------------- :public method delete_scheduled_atjobs {obj:object} { # # Delete previously scheduled atjobs # ns_log notice "#### delete_scheduled_atjobs" set item_id [$obj item_id] set atjob_form_id [::xowf::atjob form_id -parent_id $item_id -package_id [ad_conn package_id]] set to_delete [xo::dc list get_children { select item_id from xowiki_form_instance_item_index where parent_id = :item_id and page_template = :atjob_form_id }] foreach id $to_delete { ns_log notice "#### xo::db::sql::content_item proc delete -item_id $id" xo::db::sql::content_item delete -item_id $id } } #---------------------------------------------------------------------- # 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 # return [::[$obj package_id] instantiate_forms \ -parent_id [$obj item_id] \ -default_lang [$obj lang] \ -forms [$obj property wfName]] } #---------------------------------------------------------------------- # Class: Answer_manager # Method: last_time_in_state #---------------------------------------------------------------------- :public method get_wf_instances { {-initialize false} {-orderby ""} -creation_user:integer -item_id:integer wf:object } { # get_wf_instances: return the workflow instances :assert_assessment_container $wf set extra_where_clause "" foreach var {creation_user item_id} { if {[info exists $var]} { append extra_where_clause "AND $var = [ns_dbquotevalue [set $var]] " } } return [::xowiki::FormPage get_form_entries \ -base_item_ids [$wf item_id] \ -form_fields "" \ -always_queried_attributes "*" \ -initialize $initialize \ -orderby $orderby \ -extra_where_clause $extra_where_clause \ -publish_status all \ -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 # answer-specific attributes) # # @param wf the workflow # @param state retrieve only instances in this state # # @return a list of dicts # set results {} set items [:get_wf_instances $wf] foreach i [$items children] { if {$state ne "" && [$i state] ne $state} { continue } set answerAttributes [xowf::test_item::renaming_form_loader answer_attributes \ [$i instance_attributes]] foreach extra $extra_attributes { lappend answerAttributes $extra [$i property $extra] } #ns_log notice "GETANSWERS $i: <$answerAttributes> ALL [$i instance_attributes]" lappend results [list item $i answerAttributes $answerAttributes state [$i state]] } 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 # containing "from", "fromClock","to", "toClock", "seconds", and # "duration". # set first [lindex $revision_sets 0] set last [lindex $revision_sets end] set fromClock [clock scan [::xo::db::tcl_date [ns_set get $first creation_date] tz]] set toClock [clock scan [::xo::db::tcl_date [ns_set get $last last_modified] tz]] dict set r fromClock $fromClock dict set r toClock $toClock dict set r from [clock format $fromClock -format "%H:%M:%S"] dict set r to [clock format $toClock -format "%H:%M:%S"] set timeDiff [expr {$toClock - $fromClock}] dict set r duration "[expr {$timeDiff/60}]m [expr {$timeDiff%60}]s" dict set r seconds $timeDiff if {$exam_published_time ne ""} { set examPublishedClock [clock scan [::xo::db::tcl_date $exam_published_time tz]] dict set r examPublishedClock $examPublishedClock dict set r examPublished [clock format $examPublishedClock -format "%H:%M:%S"] set epTimeDiff [expr {$toClock - $examPublishedClock}] dict set r examPublishedDuration "[expr {$epTimeDiff/60}]m [expr {$epTimeDiff%60}]s" dict set r examPublishedSeconds $epTimeDiff } 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 # actually only one. The revision_set must not be empty. # set IPs "" foreach revision_set $revision_sets { set ip [ns_set get $revision_set creation_ip] if {$ip ne ""} { dict set IPs [ns_set get $revision_set creation_ip] 1 } } 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 # provided revision_id. If this revision_id does not exist, # return the full set. # set result "" set stop 0 return [lmap s $revision_sets { if {$stop} break set stop [expr {[ns_set get $s revision_id] eq $revision_id}] set s }] } #---------------------------------------------------------------------- # 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 # where state is equal the specified value. # # @param revision_sets a list of ns_sets containing revision # data. List is assumed to be sorted in descending # creation_date order (as retrieved by get_revision_sets) # # @return a date # set result "" foreach ps $revision_sets { if {$state eq [ns_set get $ps state]} { set result [ns_set get $ps last_modified] } } 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] if {$to ne ""} { set to_day [lc_time_fmt $to $dayfmt] set to_time [lc_time_fmt $to $timefmt] } else { set to_day "" set to_time "" } if {$to_day eq ""} { set period "$from_day, $from_time -" } elseif {$from_day eq $to_day} { set period "$from_day, $from_time - $to_time" } else { set period "$from_day, $from_time - $to_day, $to_time" } 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 # workflow was in the provided state. # set periods "" set from "" set last_from "" set until "" foreach ps $revision_sets { set current_state [ns_set get $ps state] if {$state eq $current_state} { if {$until ne ""} { lappend periods [:pretty_period $last_from $until] } set from [ns_set get $ps creation_date] set until "" } elseif {$until eq "" && $current_state ne $state && $from ne ""} { set until [ns_set get $ps last_modified] set last_from $from set from "" } } if {$until ne ""} { lappend periods [:pretty_period $last_from $until] } elseif {$from ne ""} { lappend periods [:pretty_period $from ""] } #ns_log notice "state_periods $state <$from> <$last_from> <$until> <$periods>" return $periods } #---------------------------------------------------------------------- # 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. # set all_form_fields [::xowiki::formfield::FormField info instances -closure] set totalPoints 0 set achievableTotalPoints 0 set details {} foreach a [dict keys $answer_attributes] { 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" $f set test_item_points 0 } set achievablePoints [$f set test_item_points] set achievableTotalPoints [expr {$achievableTotalPoints + $achievablePoints}] if {[$f exists correction_data]} { set cd [$f set correction_data] #ns_log notice "FOO: $a <$f> $cd" if {[dict exists $cd points]} { set points [dict get $cd points] set totalPoints [expr {$totalPoints + $points}] } else { ns_log warning "$a: no points in correction_data, ignoring in points calculation" } } lappend details [dict create \ attributeName $a \ achieved $points \ achievable $achievablePoints] } return [list achievedPoints $totalPoints \ details $details \ achievablePoints $achievableTotalPoints] } #---------------------------------------------------------------------- # Class: Answer_manager # Method: runtime_panel #---------------------------------------------------------------------- :public method runtime_panel { {-revision_id ""} {-view default} {-grading_info ""} answerObj:object } { # # Return statistics for the provided object: # - minimal statistics: when view default # - statistics with clickable revisions: when view = revision_overview # - per-revision statistics: when view = revision_overview and revision_id is provided # set revision_sets [$answerObj get_revision_sets] set parent_revsion_sets [[$answerObj parent_id] get_revision_sets] set item_id [$answerObj item_id] set live_revision_id [xo::dc get_value -prepare integer live_revision_id { select live_revision from cr_items where item_id = :item_id }] set current_question [expr {[dict get [$answerObj instance_attributes] position] + 1}] set page_info "#xowf.question#: $current_question" if {$view eq "default"} { set url [ad_return_url]&id=$item_id set revisionDetails "#xowf.nr_changes#: [llength $revision_sets]Name | Revisions |
---|
[:grading_table -csv ${:grade_csv} ${:grade_dict}]
} if {$create_zip_file} { $zipFile ns_returnZipFile [$zipFile cget -name].zip $zipFile destroy ad_script_abort } # # If we have already some file submission we are showing a link # for bulk-downloading the submissions # if {$file_submission_exists} { # # Avoid empty entries for query parameters # if {[llength $form_objs] > 0} { set fos $form_objs } foreach value {revision_id filter_id} var {rid id} { if {[set $value] ne ""} { set $var [set $value] } } set href [$examWf pretty_link -query [export_vars { {m print-answers} {create-file-submission-zip-file 1} fos rid id }]] append HTML \ "" \ "" } return [list do_stream $do_stream HTML $HTML] } #---------------------------------------------------------------------- # Class: Answer_manager # Method: participant_result #---------------------------------------------------------------------- :method participant_result { -obj:object answerObj:object form_info form_field_objs } { :assert_answer_instance $answerObj :assert_assessment $obj set instance_attributes [$answerObj instance_attributes] set answer [list item $answerObj] foreach f $form_field_objs { set att [$f name] if {[dict exists $instance_attributes $att]} { set value [dict get $instance_attributes $att] #ns_log notice "### '$att' value '$value'" $answerObj combine_data_and_form_field_default 1 $f $value $f set_feedback 1 $f add_statistics -options {word_statistics word_cloud} # # Leave the form-field in statistics mode in a state with # correct anwers. # $f make_correct #ns_log notice "FIELD $f [$f name] [$f info class] -> VALUE [$f set value]" if {[$f exists correction]} { set correction [$f set correction] } else { set correction "" ns_log warning "form-field [$f name] of type [$f info class] " \ "does not provide variable correction via 'make_correct'" } lappend answer \ [list name $att \ value $value \ correction $correction \ evaluated_answer_result [$f set evaluated_answer_result]] } } 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} { # # The -clear option is needed, when there are multiple # assessments protocols/tables on the same page (currently # not). # unset -nocomplain $key } else { #ns_log notice "### key exists [info exists $key]" if {![info exists $key]} { #ns_log notice "form_info: $form_info" set fc [dict get $form_info disabled_form_constraints] set pc_params [::xo::cc perconnection_parameter_get_all] #ns_log notice "### create_form_fields_from_form_constraints <$fc>" set $key [$wf create_form_fields_from_form_constraints \ -lookup \ [lsort -unique $fc]] ::xo::cc perconnection_parameter_set_all $pc_params $wf form_field_index [set $key] } return [set $key] } } #---------------------------------------------------------------------- # 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 # counts as values. # set gradingTable { #xowf.Download_file_submissions##xowf.Grade# | # | " \ "\n" set nrGrades 0 foreach v [dict values $grade_dict] { incr nrGrades $v} foreach k [lsort [dict keys $grade_dict]] { set count [dict get $grade_dict $k] set countPercentage [format %.2f [expr {$count *100.0 / $nrGrades}]] append gradingTable \|
---|---|---|
$k | $count | }] \ [subst {
$csv
#xowf.Grade# | # | " \ "\n" set nrGrades 0 foreach v [dict values $grade_count] { incr nrGrades $v} foreach k [lsort [dict keys $grade_count]] { set count [dict get $grade_count $k] set countPercentage [expr {$count*100.0/$nrGrades}] append gradingTable \|
---|---|---|
$k | $count | }] \ [subst {
$gradingTable
} return $HTML } #---------------------------------------------------------------------- # Class: Answer_manager # Method: participants_table #---------------------------------------------------------------------- :public method participants_table { -package_id:integer -items:object,required {-view_all_method print-answers} {-state done} wf:object } { # # This method returns an HTML table containing a row for every # participant with Name and short summary information. This # table provides as well an interface for sending messages to # this student. # set form_field_objs {} lappend form_field_objs \ [$wf create_raw_form_field \ -name _online-exam-userName \ -spec text,label=#xowf.participant#] \ [$wf create_raw_form_field \ -name _online-exam-fullName \ -spec label,label=#acs-subsite.Name#,disableOutputEscaping=true] \ [$wf create_raw_form_field \ -name _state \ -spec text,label=#xowf.Status#] \ [$wf create_raw_form_field \ -name _online-exam-seconds \ -spec number,label=#xowf.Seconds#] \ [$wf create_raw_form_field \ -name _creation_date \ -spec date,label=#xowiki.Page-last_modified#] # # Take "orderby" from the query parameter. If not set, order by # the first field. # set orderby [::$package_id query_parameter orderby:token ""] if {$orderby eq "" && [llength $form_field_objs] > 0} { set orderby [[lindex $form_field_objs 0] name],asc } # # Create table widget. # set table_widget [::xowiki::TableWidget create_from_form_fields \ -package_id $package_id \ -form_field_objs $form_field_objs \ -orderby $orderby] # # Extend properties of individual answers and add notification # dialogs. # set dialogs "" set user_list {} foreach p [$items children] { #foreach ff_obj $answer_form_field_objs { # $ff_obj object $p # set property [$ff_obj name] # $ff_obj value [$p property $property] #} # # Provide a notification dialog only before the student has # submitted her exam. # if {[$p state] ne "done"} { set dialog_info [::xowiki::includelet::personal-notification-messages \ modal_message_dialog -to_user_id [$p creation_user]] append dialogs [dict get $dialog_info dialog] \n set notification_dialog_button [dict get $dialog_info link] lappend user_list [$p creation_user] } else { set notification_dialog_button "" } # # Extend every answer with corresponding precomputed extra # "_online-exam-*" values to ease rendering: # set duration [:get_duration [$p get_revision_sets]] $p set_property -new 1 _online-exam-seconds [dict get $duration seconds] $p set online-exam-fullName "$notification_dialog_button [$p set online-exam-fullName]" } ::xowiki::includelet::personal-notification-messages \ modal_message_dialog_register_submit \ -url [$wf pretty_link -query m=send-participant-message] set bulk_notification_HTML "" if {$state eq "done"} { set uc {tcl {[$p state] ne "done"}} } else { set uc {tcl {false}} if {[llength $user_list] > 0} { # # Provide bulk notification message dialog to send message to all users # set dialog_info [::xowiki::includelet::personal-notification-messages \ modal_message_dialog -to_user_id $user_list] append dialogs [dict get $dialog_info dialog] \n set notification_dialog_button [dict get $dialog_info link] set bulk_notification_HTML " " } } # # Render table widget with extended properties. # set HTML [$table_widget render_page_items_as_table \ -package_id $package_id \ -items $items \ -form_field_objs $form_field_objs \ -csv true \ -uc $uc \ -view_field _online-exam-userName \ -view_filter_link [$wf pretty_link -query m=$view_all_method] \ {*}[expr {[info exists generate] ? [list -generate $generate] : ""}] \ -return_url [ad_return_url] \ -return_url_att local_return_url \ ] $table_widget destroy return $dialogs$HTML$bulk_notification_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 # set form_field_objs [:answer_form_field_objs -wf $wf $form_info] set items [:get_wf_instances $wf] set results "" foreach i [$items children] { xo::cc eval_as_user -user_id [$i creation_user] { set participantResult [:participant_result -obj $obj $i $form_info $form_field_objs] } append results $participantResult \n } #ns_log notice "=== marked_results of [llength [$items children]] items => $results" return $results } #---------------------------------------------------------------------- # Class: Answer_manager # Method: answers_panel #---------------------------------------------------------------------- :public method answers_panel { {-polling:switch false} {-heading #xowf.submitted_answers#} {-submission_msg #xowf.participants_answered_question#} {-manager_obj:object} {-target_state ""} {-wf:object} {-current_question ""} {-extra_text ""} } { # # Produce HTML code for an answers panel, containing the number # of participants of an e-assessment and the number of # participants, who have already answered. # # @param polling when specified, provide live updates # of the numbers via AJAX calls # @param extra_text optional extra text for the panel, # has to be provided with valid HTML markup. # set answers [xowf::test_item::answer_manager get_answers $wf] set nrParticipants [llength $answers] if {$current_question ne ""} { set answered [xowf::test_item::renaming_form_loader answers_for_form \ [$current_question name] \ $answers] } else { set answered [xowf::test_item::answer_manager get_answers \ -state $target_state $wf] } set nrAnswered [llength $answered] set answerStatus [subst {$question_info" lappend msgList "$msg\n" } } return $msgList } :public method exam_summary {obj} { # # Provide a summary of all questions of an exam. # set HTML [subst {
#xowf.question_structure# | #xowf.Minutes# | #xowf.Points# | #xowf.Shuffle# | ||
---|---|---|---|---|---|
$title_value | [:dict_value $chunk type]: $structure | [:dict_value $chunk Minutes] | [:dict_value $chunk Points] | [:pretty_shuffle [:dict_value $chunk shuffle]] | [:dict_value $chunk grading] |
[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 [:total_minutes -max_items $max_items $combined_form_info] set total_points [: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
[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 {, }]
" : ""}]