::xo::library doc { Test Item procs - support for different kind of tests and exercises. @author Gustaf Neumann } :::xo::db::require package xowiki 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} } -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 ########################################################### # # ::xowiki::formfield::test_item # ########################################################### Class create test_item -superclass TestItemField -parameter { {question_type mc} {nr_choices 5} {grading exact} } -ad_doc { Wrapper for complex test items, containing specification for minutes, grading scheme, feedback levels, handling different types of questions ("interactions" in the terminology of QTI). When such a question is saved, a HTML form is generated, which is used as a question. @param feedback_level "full", "single", or "none" @param grading one of "exact", "none", or one of the partial grading schemes @param nr_choices number of choices @param question_type "mc", "sc", "ot", or "st" } # # Provide a default setting for the rich-text widgets. # test_item set richtextWidget {richtext,editor=ckeditor4,ck_package=standard,extraPlugins=} test_item instproc feed_back_definition {} { # # Return the definition of the feed_back widgets depending on the # value of :feedback_level. # if {${:feedback_level} eq "none"} { return "" } set widget [test_item set richtextWidget] switch ${:feedback_level} { "none" { set definition "" } "single" { set definition [subst { {feedback_correct {$widget,height=150px,label=#xowf.feedback#}} }] } "full" { set definition [subst { {feedback_correct {$widget,height=150px,label=#xowf.feedback_correct#}} {feedback_incorrect {$widget,height=150px,label=#xowf.feedback_incorrect#}} }] } } return $definition } # # "test_item" is the wrapper for interaction to be used in # evaluations. Different wrapper can be defined in a similar way for # questionnaires, which might need less input fields. # test_item instproc initialize {} { if {${:__state} ne "after_specs"} { return } set options "" # # Provide some settings for name short-cuts # switch -- ${:question_type} { mc { # we should support as well: minChoices, maxChoices # # Old style, kept just for backwards compatibility for the # time being. one should use "mc2" instead. # set interaction_class mc_interaction set options nr_choices=[:nr_choices] set options "" set auto_correct true set can_shuffle false } sc { # we should support as well: minChoices, maxChoices set interaction_class mc_interaction2 set options multiple=false set auto_correct true set can_shuffle true } mc2 { # we should support as well: minChoices, maxChoices set interaction_class mc_interaction2 set options "" set auto_correct true set can_shuffle true } ot { set interaction_class text_interaction set auto_correct ${:auto_correct} set can_shuffle false } ro { set interaction_class reorder_interaction set auto_correct ${:auto_correct} set can_shuffle false } te - st { set interaction_class short_text_interaction #set options nr_choices=[:nr_choices] set auto_correct ${:auto_correct} set can_shuffle true } ul { # set interaction_class upload_interaction set options "" set auto_correct false set can_shuffle false } default {error "unknown question type: ${:question_type}"} } :log test_item-auto_correct=$auto_correct # # Handle feedback_level. # # The object might be a form, just use the property, if we are on # a FormPage. # if {[${:object} istype ::xowiki::FormPage]} { set feedback_level_property [${:object} property feedback_level] if {$feedback_level_property ne ""} { set :feedback_level $feedback_level_property } } if {${:grading} ne "none" && [llength ${:grading}] >1} { dict set grading_dict default [lindex ${:grading} 0] dict set grading_dict options {} foreach o ${:grading} { dict lappend grading_dict options [list $o $o] } dict set grading_dict form_item_wrapper_CSSclass form-inline dict set grading_dict label #xowf.Grading-Scheme# set gradingSpec [list [list grading [:dict_to_fc -type select $grading_dict]]] } else { set gradingSpec "" } if {$can_shuffle} { set shuffle_options "{#xowf.shuffle_none# none} {#xowf.shuffle_peruser# peruser} {#xowf.shuffle_always# always}" set shuffleSpec [subst { {shuffle {radio,horizontal=true,form_item_wrapper_CSSclass=form-inline,options=$shuffle_options,default=none,label=#xowf.Shuffle#}} {show_max {number,form_item_wrapper_CSSclass=form-inline,min=2,label=#xowf.show_max#}} }] } else { set shuffleSpec "" } :create_components [subst { {minutes number,form_item_wrapper_CSSclass=form-inline,min=1,default=2,label=#xowf.Minutes#} $shuffleSpec $gradingSpec {interaction {$interaction_class,$options,feedback_level=${:feedback_level},auto_correct=${:auto_correct},label=}} [:feed_back_definition] }] set :__initialized 1 } } namespace eval ::xowiki::formfield { ########################################################### # # ::xowiki::formfield::mc_interaction # ########################################################### Class create mc_interaction -superclass TestItemField -parameter { {nr_choices 5} {multiple true} } mc_interaction set auto_correct true mc_interaction instproc set_compound_value {value} { set r [next] if {!${:multiple}} { # For single choice questions, we have a fake-field for denoting # the correct entry. We have to distribute this to the radio # element, which is rendered. set correct_field_name [:get_named_sub_component_value correct] if {$correct_field_name ne ""} { foreach c [:components] { if {[$c name] eq $correct_field_name} { ${c}::correct value $correct_field_name } } } } return $r } mc_interaction instproc initialize {} { if {${:__state} ne "after_specs"} return # # build choices # if {!${:multiple}} { append choices "{correct radio,omit}\n" } # # create component structure # set widget [test_item set richtextWidget] :create_components [subst { {text {$widget,required,height=150px,label=#xowf.exercise-text#}} {mc {mc_choice,feedback_level=${:feedback_level},label=#xowf.alternative#,multiple=${:multiple},repeat=1..${:nr_choices}}} }] set :__initialized 1 } mc_interaction instproc convert_to_internal {} { # # Build a form from the components of the exercise on the fly. # Actually, this methods computes the properties "form" and # "form_constraints" based on the components of this form field. # set form "
\n" #ns_log notice FORM=$form #ns_log notice FC=$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 [[self class] set auto_correct] ${:object} set_property -new 1 has_solution true } ########################################################### # # ::xowiki::formfield::mc_choice # ########################################################### Class create mc_choice -superclass TestItemField -parameter { {multiple true} } mc_choice instproc initialize {} { if {${:__state} ne "after_specs"} return set text_config [subst {height=100px,label=Text}] if {[:feedback_level] eq "full"} { set feedback_fields { {feedback_correct {textarea,cols=60,label=#xowf.feedback_correct#}} {feedback_incorrect {textarea,cols=60,label=#xowf.feedback_incorrect#}} } } else { set feedback_fields "" } set widget [test_item set richtextWidget] if {${:multiple}} { # We are in a multiple-choice item; provide for editing a radio # group per alternative. :create_components [subst { {text {$widget,$text_config}} {correct {boolean,horizontal=true,label=#xowf.correct#}} $feedback_fields }] } else { # We are in a single-choice item; provide for editing a single # radio group spanning all entries. Use as name for grouping # the form-field name minus the last segment. regsub -all {[.][^.]+$} ${:name} "" groupname :create_components [subst { {text {$widget,$text_config}} {correct {radio,label=#xowf.correct#,forced_name=$groupname.correct,options={"" ${:name}}}} $feedback_fields }] } set :__initialized 1 } } namespace eval ::xowiki::formfield { ########################################################### # # ::xowiki::formfield::text_interaction # ########################################################### Class create text_interaction -superclass TestItemField -parameter { } #text_interaction set auto_correct false text_interaction instproc initialize {} { if {${:__state} ne "after_specs"} return # # Create component structure. # set widget [test_item set richtextWidget] if {${:auto_correct}} { set autoCorrectSpec {{correct_when {correct_when,label=#xowf.correct_when#}}} } else { set autoCorrectSpec "" } :create_components [subst { {text {$widget,label=#xowf.exercise-text#,plugins=OacsFs}} {lines {number,form_item_wrapper_CSSclass=form-inline,min=1,default=10,label=#xowf.answer_lines#}} {columns {number,form_item_wrapper_CSSclass=form-inline,min=1,max=80,default=60,label=#xowf.answer_columns#}} $autoCorrectSpec }] set :__initialized 1 } text_interaction instproc convert_to_internal {} { set intro_text [:get_named_sub_component_value text] dict set fc_dict rows [:get_named_sub_component_value lines] dict set fc_dict cols [:get_named_sub_component_value columns] dict set fc_dict disabled_as_div 1 dict set fc_dict label #xowf.answer# if {${:auto_correct}} { dict set fc_dict correct_when [:get_named_sub_component_value correct_when] } append form \ "\n" append fc \ "@categories:off @cr_fields:hidden\n" \ "{answer:[:dict_to_fc -type textarea $fc_dict]}" #ns_log notice "text_interaction $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 } } namespace eval ::xowiki::formfield { ########################################################### # # ::xowiki::formfield::short_text_interaction # ########################################################### Class create short_text_interaction -superclass TestItemField -parameter { {nr 15} } short_text_interaction instproc initialize {} { if {${:__state} ne "after_specs"} return # # Create component structure. # set widget [test_item set richtextWidget] ns_log notice "[self] [:info class] auto_correct=${:auto_correct}" :create_components [subst { {text {$widget,height=100px,label=#xowf.exercise-text#,plugins=OacsFs}} {answer {short_text_field,repeat=1..${:nr},label=}} }] set :__initialized 1 } short_text_interaction instproc convert_to_internal {} { set intro_text [:get_named_sub_component_value text] set answerFields [:get_named_sub_component_value -from_repeat answer] set options {} set render_hints {} set answer {} set count 0 foreach {fieldName value} $answerFields { #ns_log notice ...fieldName=$fieldName->$value set af answer[incr count] lappend options [list [dict get $value $fieldName.text] $af] lappend answer [dict get $value $fieldName.correct_when] lappend render_hints [list \ words [dict get $value $fieldName.options] \ lines [dict get $value $fieldName.lines]] } dict set fc_dict shuffle_kind [${:parent_field} get_named_sub_component_value shuffle] dict set fc_dict show_max [${:parent_field} get_named_sub_component_value show_max] dict set fc_dict disabled_as_div 1 dict set fc_dict label "" dict set fc_dict options $options dict set fc_dict answer $answer dict set fc_dict render_hints $render_hints append form \ "\n" set fc {} lappend fc \ answer:[:dict_to_fc -type text_fields $fc_dict] \ @categories:off @cr_fields:hidden ns_log notice "short_text_interaction $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::short_text_field # Class create short_text_field -superclass TestItemField -parameter { } short_text_field instproc initialize {} { if {${:__state} ne "after_specs"} return # # Create component structure. # set widget [test_item set richtextWidget] # # Get "auto_correct" from the interaction (passing "auto_correct=" # via form constrain would require to extend the RepeatContainer, # otherwise the attribute is rejected). # set p [:info parent] while {1} { if {![$p istype ::xowiki::formfield::FormField]} break if {![$p istype ::xowiki::formfield::short_text_interaction]} { set p [$p info parent] continue } set :auto_correct [$p set auto_correct] break } #:log "[:name] auto_correct ${:auto_correct}" if {${:auto_correct}} { set autoCorrectSpec {{correct_when {correct_when,label=#xowf.correct_when#}}} } else { set autoCorrectSpec "" } set render_hints [join { "{#xowiki.number# number}" "{#xowiki.single_word# single_word}" "{#xowiki.multiple_words# multiple_words}" "{#xowiki.multiple_lines# multiple_lines}" "{#xowiki.file_upload# file_upload}" } " "] set textEntryConfigSpec [subst { {options {radio,horizontal=true,form_item_wrapper_CSSclass=form-inline,options=$render_hints,default=single_word,label=#xowf.answer#}} {lines {number,form_item_wrapper_CSSclass=form-inline,default=1,min=1,label=#xowf.lines#}} }] #:msg autoCorrectSpec=$autoCorrectSpec :create_components [subst { {text {$widget,height=100px,label=#xowf.sub_question#,plugins=OacsFs}} $textEntryConfigSpec $autoCorrectSpec }] set :__initialized 1 } } namespace eval ::xowiki::formfield { ########################################################### # # ::xowiki::formfield::reorder_interaction # ########################################################### Class create reorder_interaction -superclass TestItemField -parameter { {nr 15} } reorder_interaction instproc initialize {} { if {${:__state} ne "after_specs"} return # # Create component structure. # set widget [test_item set richtextWidget] ns_log notice "[self] [:info class] auto_correct=${:auto_correct}" :create_components [subst { {text {$widget,height=100px,label=#xowf.exercise-text#,plugins=OacsFs}} {answer {text,repeat=1..${:nr},label=#xowf.reorder_question_elements#}} }] set :__initialized 1 } reorder_interaction instproc convert_to_internal {} { set intro_text [:get_named_sub_component_value text] set answerFields [:get_named_sub_component_value -from_repeat answer] set options {} set answer {} set count 0 foreach {fieldName value} $answerFields { #ns_log notice ...fieldName=$fieldName->$value lappend options [list $value $count] lappend answer $count incr count } #dict set fc_dict shuffle_kind [${:parent_field} get_named_sub_component_value shuffle] #dict set fc_dict show_max [${:parent_field} get_named_sub_component_value show_max] dict set fc_dict disabled_as_div 1 dict set fc_dict label "" dict set fc_dict options $options dict set fc_dict answer $answer append form \ "\n" set fc {} lappend fc \ answer:[:dict_to_fc -type reorder_box $fc_dict] \ @categories:off @cr_fields:hidden ns_log notice "reorder_interaction $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 } } namespace eval ::xowiki::formfield { ########################################################### # # ::xowiki::formfield::mc_interaction2 # ########################################################### Class create mc_interaction2 -superclass TestItemField -parameter { {nr 15} {multiple true} } mc_interaction2 instproc initialize {} { if {${:__state} ne "after_specs"} return # # Create component structure. # set widget [test_item set richtextWidget] #ns_log notice "[self] [:info class] auto_correct=${:auto_correct}" :create_components [subst { {text {$widget,height=100px,label=#xowf.exercise-text#,plugins=OacsFs}} {answer {mc_field,repeat=1..${:nr},label=}} }] set :__initialized 1 } mc_interaction2 instproc convert_to_internal {} { set intro_text [:get_named_sub_component_value text] set answerFields [:get_named_sub_component_value -from_repeat answer] set count 0 set options {} set correct {} foreach {fieldName value} $answerFields { #ns_log notice ...fieldName=$fieldName->$value #set af answer[incr count] set text [dict get $value $fieldName.text] # trim leadingsince 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] } dict set fc_dict richtext 1 dict set fc_dict answer $correct dict set fc_dict options $options 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"}] 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 { } mc_field instproc initialize {} { if {${:__state} ne "after_specs"} return # # Create component structure. # set widget [test_item set richtextWidget] if {${:auto_correct}} { set autoCorrectSpec {{correct_when {correct_when,label=#xowf.correct_when#}}} } else { set autoCorrectSpec "" } #:msg autoCorrectSpec=$autoCorrectSpec :create_components [subst { {text {$widget,height=50px,label=#xowf.sub_question#,plugins=OacsFs}} {correct {boolean,horizontal=true,label=Korrekt}} }] 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}} }] set :__initialized 1 } upload_interaction instproc convert_to_internal {} { set intro_text [:get_named_sub_component_value text] append form \ "\n" append fc \ "@categories:off @cr_fields:hidden\n" \ "answer:file" ${: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" } } } } 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 # # - 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 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} { 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 get_form_object {{-set_title:boolean true} ctx:object form_name} { 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 { nx::Class create Answer_manager -superclass AssessmentInterface { # # Public API: # # - create_workflow # - delete_all_answer_data # - get_answer_wf # - get_wf_instances # - get_answers # # - get_duration # - get_IPs # - runtime_panel # # - marked_results # - answers_panel # - result_table # :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]>" } ######################################################################## :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 } ######################################################################## :public method get_answer_wf {obj:object} { # # return the workflow denoted by the property wfName in obj # return [::xowiki::Weblog instantiate_forms \ -parent_id [$obj item_id] \ -package_id [$obj package_id] \ -default_lang [$obj lang] \ -forms [$obj property wfName]] } ######################################################################## :public method get_wf_instances { {-initialize false} {-orderby ""} wf:object } { # get_wf_instances: return the workflow instances :assert_assessment_container $wf return [::xowiki::FormPage get_form_entries \ -base_item_ids [$wf item_id] \ -form_fields "" \ -always_queried_attributes "*" \ -initialize $initialize \ -orderby $orderby \ -publish_status all \ -package_id [$wf package_id]] } ######################################################################## :public method get_answers {{-state ""} wf:object} { 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]] lappend results [list item $i answerAttributes $answerAttributes state [$i state]] } return $results } ######################################################################## :public method get_duration {revision_sets} { # # Get the duration from a set of revisions and return a dict # containing "from", "fromClock","to", "toClock", 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 creation_date] 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" return $r } ######################################################################## :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] } ######################################################################## :method revisions_up_to {revision_sets revision_id} { 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 }] } ######################################################################## :public method runtime_panel { {-revision_id ""} {-filter_id ""} answerObj:object } { # # Return statistics for the provided object: # - minimal statistics: when 'filter_id' is empty # - statistics with clickable revisions: when 'filter_id' is non empty # - per-revision statistics: when revision_id is provided # set revision_sets [$answerObj get_revision_sets] set page_info "" if {$filter_id ne ""} { set baseUrl [ns_conn url] set filtered_revision_sets [:revisions_up_to $revision_sets $revision_id] set c 0 foreach s $revision_sets { set rid [ns_set get $s revision_id] set revision_url $baseUrl?[::xo::update_query [ns_conn query] rid $rid] if {$rid == [$answerObj revision_id]} { set suffix "*" set CSSclass "current" set current_item [expr {[dict get [$answerObj instance_attributes] position] + 1}] set page_info "#xowf.question#: $current_item" } else { set suffix "" set CSSclass "other" } lappend revision_list [subst {[incr c]$suffix}] } set revision_sets $filtered_revision_sets set revisionDetails "#xowiki.revisions#: [join $revision_list {, }]" } else { set revisionDetails "#xowf.nr_changes#: [llength $revision_sets]" } set duration [xowf::test_item::answer_manager get_duration $revision_sets] set IPs [xowf::test_item::answer_manager get_IPs $revision_sets] set state [$answerObj state] if {$state eq "done"} { set submission_info "#xowf.submitted#" } else { set submission_info "#xowf.not_submitted# ($page_info)" } set HTML [subst { $revisionDetails