+ }
+ if {$attachments_count > 0} {
+ append html "
$attachments_label $attachments_links
"
+ }
+ }
+ #ns_log notice text_attachments=>$attachments_html
+ return $html
+ }
+
+ TestItemField instproc attachments_widget {nr_attachments} {
+
+ dict set attachments_dict repeat 0..${:nr_attachments}
+ dict set attachments_dict repeat_add_label #xowiki.form-repeatable-add-file#
+ dict set attachments_dict label #general-comments.Attachments#
+
+ # {attachments {file,0..${:nr_attachments},label=#general-comments.Attachments#}}
+ # {attachments {bootstrap_file_input,multiple=true,label=#general-comments.Attachments#}}
+ # {attachments {file,multiple=true,label=#general-comments.Attachments#}}
+ return [:dict_to_fc -type file $attachments_dict]
+ }
+
+ TestItemField instproc comp_correct_when_from_value {value} {
+ set correct_whens {}
+ foreach {compound_key compound_entries} $value {
+ if {![string match "*.0" $compound_key]} {
+ # ns_log notice "key $compound_key, value $compound_entries"
+ set d {}
+ foreach {entry_key entry_value} $compound_entries {
+ set tail [lindex [split $entry_key .] end]
+ # ns_log notice "... entry_key $tail, entry_value $entry_value"
+ dict set d $tail $entry_value
+ }
+ set text [string trim [dict get $d text]]
+ if {$text ne ""} {
+ set correct_when "[dict get $d operator] "
+ append correct_when [expr {[dict get $d nocase] ? "-nocase " : ""}]
+ append correct_when $text
+ lappend correct_whens $correct_when
+ } else {
+ set correct_when ""
+ }
+ }
+ }
+ if {[llength $correct_whens] < 2} {
+ set correct_when [lindex $correct_whens 0]
+ } else {
+ set correct_when "AND $correct_whens"
+ }
+ ns_log notice FINAL-correct_when([self])='$correct_when'
+ return $correct_when
+ }
+
+ TestItemField instproc correct_when_widget {{-nr 10}} {
+ dict set dict repeat 1..10
+ dict set dict repeat_add_label #xowiki.form-repeatable-add-condition#
+ dict set dict help_text #xowiki.formfield-comp_correct_when-help_text#
+ dict set dict label #xowf.correct_when#
+
+ return [:dict_to_fc -type comp_correct_when $dict]
+ }
+
+ TestItemField instproc correct_when_spec {{-nr 10}} {
+ if {${:auto_correct}} {
+ return [list [list correct_when [:correct_when_widget -nr $nr]]]
+ }
+ return ""
+ }
+
+ TestItemField instproc twocol_layout {} {
+ return [expr {[${:parent_field} get_named_sub_component_value -default 0 twocol]
+ ? "col-sm-6" : "col-xs-12"}]
+ }
+
+ TestItemField instproc form_markup {
+ -interaction
+ -intro_text
+ -body
+ } {
+ set twocol [:twocol_layout]
+ return [string cat \
+ "\n"]
+ }
+
+
###########################################################
#
+ # ::xowiki::formfield::test_item_name
+ #
+ ###########################################################
+ Class create test_item_name -superclass text \
+ -extend_slot_default validator name -ad_doc {
+ Name sanitizer for test items
+ }
+ test_item_name instproc check=name {value} {
+ set valid [regexp {^[[:alnum:]:/_-]+$} $value]
+ if {!$valid} {
+ :uplevel {set __langPkg xowf}
+ }
+ return $valid
+ }
+
+
+ ###########################################################
+ #
# ::xowiki::formfield::test_item
#
###########################################################
- Class create test_item -superclass FormGeneratorField -parameter {
+ Class create test_item -superclass TestItemField -parameter {
{question_type mc}
{nr_choices 5}
- {feedback_level full}
+ {grading exact}
+ } -ad_doc {
+
+ Wrapper for composite 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, an 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 xinha JavaScript for test-items
+ # Provide a default setting for the rich-text widgets.
#
- test_item set xinha(javascript) [::xowiki::formfield::FormField fc_encode {
- xinha_config.toolbar = [
- ['popupeditor', 'bold','italic','createlink','insertimage','separator'],
- ['killword','removeformat','htmlmode']
- ];
- }]
+ test_item set richtextWidget {richtext}
- test_item instproc feed_back_definition {auto_correct} {
+ test_item instproc feedback_definition {} {
#
# Return the definition of the feed_back widgets depending on the
- # value of auto_correct. If we can't determine automatically,
- # what's wrong, we can't provide different feedback for right or
- # wrong.
+ # value of :feedback_level.
#
- :instvar inplace feedback_level
- if {$feedback_level eq "none"} {
- return ""
- }
+ set widget [test_item set richtextWidget]
- set widget "richtext,editor=ckeditor4,height=150px"
- if {$auto_correct} {
- return [subst {
- {feedback_correct {$widget,label=#xowf.feedback_correct#}}
- {feedback_incorrect {$widget,label=#xowf.feedback_incorrect#}}
+ 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#}}
+ }]
+ }
+ }
+ if {${:with_correction_notes}} {
+ append definition [subst {
+ {correction_notes {$widget,height=150px,label=#xowf.Correction_notes#}}
}]
}
- return [subst {
- {feedback {$widget,label=#xowf.feedback#}}
- }]
+
+ return $definition
}
#
- # test_item is the wrapper for interaction to be used in
+ # "test_item" is the wrapper for interaction to be used in
# evaluations. Different wrapper can be defined in a similar way for
- # questionairs, which might need less input fields.
+ # questionnaires, which might need less input fields.
#
test_item instproc initialize {} {
- if {${:__state} ne "after_specs"} return
- :instvar inplace feedback_level
+ if {${:__state} ne "after_specs"} {
+ return
+ }
set options ""
+ set typeSpecificComponentSpec ""
#
# Provide some settings for name short-cuts
#
- switch -- [:question_type] {
- mc { # we should support as well: minChoices, maxChoices, shuffle
+ 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, shuffle
- set interaction_class mc_interaction
- set options nr_choices=[:nr_choices],multiple=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
}
- ot { set interaction_class text_interaction }
- default {error "unknown question type: [:question_type]"}
+ 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
+ set typeSpecificComponentSpec {{max_nr_submission_files {number,form_item_wrapper_CSSclass=form-inline,min=1,default=1,label=Maximale Anzahl von Abgaben}}}
+ }
+ section -
+ case {
+ set interaction_class test_section
+ set options ""
+ set auto_correct false
+ set can_shuffle false
+ }
+ pool {
+ set interaction_class pool_question
+ set options ""
+ set auto_correct false
+ set can_shuffle false
+ }
+ default {error "unknown question type: ${:question_type}"}
}
+ #:log test_item-auto_correct=$auto_correct
- set auto_correct [expr {[$interaction_class exists auto_correct] &&
- [$interaction_class set auto_correct] == false ? 0 : 1}]
-
- # For the time being, we set inplace to false, otherwise we can't
- # currently edit empty fields
- set inplace true
-
#
- # handle feedback_level
+ # Handle feedback_level (typically defined in "TestItem*.form.page")
#
# 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
+ set :feedback_level $feedback_level_property
}
}
+ #set :feedback_level "full"
+ if {${:grading} ne "none" && [llength ${:grading}] >1} {
+ set grading_dict {_name grading _type select}
+ 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#
+ dict set grading_dict required true
+ set gradingSpec [list [:dict_to_spec -aspair $grading_dict]]
+ } else {
+ set gradingSpec ""
+ }
+
+ if {$can_shuffle} {
+ set shuffle_dict {_name shuffle _type radio}
+ dict set shuffle_dict horizontal true
+ dict set shuffle_dict form_item_wrapper_CSSclass form-inline
+ dict set shuffle_dict form_widget_CSSclass form-check
+ dict set shuffle_dict default peruser
+ dict set shuffle_dict label #xowf.Shuffle#
+ dict set shuffle_dict options \
+ "{#xowf.shuffle_none# none} {#xowf.shuffle_peruser# peruser} {#xowf.shuffle_always# always}"
+ set shuffleSpec [subst {
+ [list [:dict_to_spec -aspair $shuffle_dict]]
+ {show_max {number,form_item_wrapper_CSSclass=form-inline,min=1,label=#xowf.show_max#}}
+ }]
+ } else {
+ set shuffleSpec ""
+ }
+
#
- :create_components [subst {
- {minutes numeric,size=2,label=#xowf.Minutes#}
- {grading {select,options={exact exact} {partial partial},default=exact,label=#xowf.Grading-Schema#}}
- {interaction {$interaction_class,$options,feedback_level=$feedback_level,inplace=$inplace}}
- [:feed_back_definition $auto_correct]
+ # Default twocol spec
+ #
+ set twocolDict {
+ _name twocol
+ _type boolean_checkbox
+ label #xowf.Twocol_layout#
+ default f
+ form_item_wrapper_CSSclass form-inline
+ }
+
+ if {${:question_type} in {section case}} {
+ #
+ # Don't show "minutes" and "points" in the full composite test
+ # item form but still define it, such we can compute and update
+ # it in convert_to_internal with little effort, since all
+ # "question" content is built based on included form fields.
+ #
+ set pointsSpec {
+ {minutes hidden}
+ {points hidden}
+ }
+ set typeSpecificComponentSpec {
+ {show_minutes boolean_checkbox,form_item_wrapper_CSSclass=form-inline,default=t,label=#xowf.Composite_Show_minutes#}
+ {show_points boolean_checkbox,form_item_wrapper_CSSclass=form-inline,default=f,label=#xowf.Composite_Show_points#}
+ {show_title boolean_checkbox,form_item_wrapper_CSSclass=form-inline,default=f,label=#xowf.Composite_Show_title#}
+ }
+ #
+ # Things different between "section" and "case".
+ #
+ switch ${:question_type} {
+ "section" {}
+ "case" {
+ dict set twocolDict default t
+ }
+ default {error "this can't happen"}
+ }
+ } else {
+ set pointsSpec {
+ {minutes number,form_item_wrapper_CSSclass=form-inline,min=0,default=2,step=0.1,label=#xowf.Minutes#}
+ {points number,form_item_wrapper_CSSclass=form-inline,min=0.0,step=0.1,label=#xowf.Points#}
+ }
+ }
+ if {${:question_type} eq "pool"} {
+ set twocolDict ""
+ }
+
+ :create_components [subst {
+ $pointsSpec
+ $shuffleSpec
+ $gradingSpec
+ $typeSpecificComponentSpec
+ [list [:dict_to_spec -aspair $twocolDict]]
+ {interaction {$interaction_class,$options,feedback_level=${:feedback_level},auto_correct=${:auto_correct},label=}}
+ [:feedback_definition]
}]
set :__initialized 1
}
-
}
+
namespace eval ::xowiki::formfield {
###########################################################
#
# ::xowiki::formfield::mc_interaction
#
###########################################################
- Class create mc_interaction -superclass FormGeneratorField -parameter {
- {feedback_level full}
- {inplace false}
- {shuffle false}
+ Class create mc_interaction -superclass TestItemField -parameter {
{nr_choices 5}
{multiple true}
}
+ mc_interaction set closed_question_type true
+ #mc_interaction set item_type MC ;# just used for reverse lookup in pool questions,
+ # where the old MC questions are not supported
mc_interaction instproc set_compound_value {value} {
set r [next]
- if {![:multiple]} {
+
+ 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.
@@ -156,55 +451,50 @@
}
mc_interaction instproc initialize {} {
+
if {${:__state} ne "after_specs"} return
- test_item instvar {xinha(javascript) javascript}
- :instvar feedback_level inplace input_field_names nr_choices
+
#
# build choices
#
-
- if {![:multiple]} {
+ if {!${:multiple}} {
append choices "{correct radio,omit}\n"
}
#
# create component structure
#
- :create_components [subst {
- {text {richtext,required,height=150px,editor=ckeditor4,label=#xowf.exercise-text#}}
- {mc {mc_choice,feedback_level=$feedback_level,label=#xowf.alternative#,multiple=[:multiple],repeat=1..$nr_choices}}
+ 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 set auto_correct true
+
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
+ # Actually, this method computes the properties "form" and
# "form_constraints" based on the components of this form field.
- #
- set form "\n"
+ ns_log notice FORM=$form\nFC=$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 auto_correct [[self class] set closed_question_type]
${:object} set_property -new 1 has_solution true
}
@@ -272,21 +570,15 @@
#
###########################################################
- Class create mc_choice -superclass FormGeneratorField -parameter {
- {feedback_level full}
- {inplace true}
+ Class create mc_choice -superclass TestItemField -parameter {
{multiple true}
}
mc_choice instproc initialize {} {
if {${:__state} ne "after_specs"} return
- if {1} {
- test_item instvar {xinha(javascript) javascript}
- set text_config [subst {height=100px,label=Text}]
- } else {
- set text_config [subst {editor=wym,height=100px,label=Text}]
- }
+ 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#}}
@@ -295,22 +587,24 @@
} else {
set feedback_fields ""
}
- if {[:multiple]} {
- # We are in a multiple choice item; provide for editing a radio
+
+ 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 {richtext,editor=ckeditor4,$text_config}}
+ {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
+ # 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
+ regsub -all -- {[.][^.]+$} ${:name} "" groupname
:create_components [subst {
- {text {richtext,editor=ckeditor4,$text_config}}
- {correct {radio,label=#xowf.correct#,forced_name=$groupname.correct,options={"" [:name]}}}
+ {text {$widget,$text_config}}
+ {correct {radio,label=#xowf.correct#,forced_name=$groupname.correct,options={"" ${:name}}}}
$feedback_fields
}]
}
@@ -325,59 +619,483 @@
#
###########################################################
- Class create text_interaction -superclass FormGeneratorField -parameter {
- {feedback_level full}
- {inplace true}
+ Class create text_interaction -superclass TestItemField -parameter {
}
- text_interaction set auto_correct false
+ text_interaction set closed_question_type false
+ text_interaction set item_type Text
text_interaction instproc initialize {} {
if {${:__state} ne "after_specs"} return
- test_item instvar {xinha(javascript) javascript}
- :instvar feedback_level inplace input_field_names
+
#
- # create component structure
+ # Create component structure.
#
+ set widget [test_item set richtextWidget]
+
:create_components [subst {
- {text {richtext,required,editor=ckeditor4,height=150px,label=#xowf.exercise-text#,plugins=OacsFs,javascript=$javascript,inplace=$inplace}}
- {lines {numeric,default=10,size=3,label=#xowf.lines#}}
- {columns {numeric,default=60,size=3,label=#xowf.columns#}}
+ {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#}}
+ {attachments {[:attachments_widget ${:nr_attachments}]}}
+ [:correct_when_spec]
}]
set :__initialized 1
}
text_interaction instproc convert_to_internal {} {
- set form "
\n"
- set fc "@categories:off @cr_fields:hidden\n"
+ next
+
set intro_text [:get_named_sub_component_value text]
- set lines [:get_named_sub_component_value lines]
- set columns [:get_named_sub_component_value columns]
- append form "
$intro_text
\n"
- append form "\n"
- append fc "answer:textarea"
- append form "
\n"
+ append intro_text [:text_attachments]
+
+ set fc_dict {
+ _name answer
+ _type textarea
+ disabled_as_div 1
+ label #xowf.answer#
+ autosave true
+ }
+ dict set fc_dict rows [:get_named_sub_component_value lines]
+ dict set fc_dict cols [:get_named_sub_component_value columns]
+
+ if {${:auto_correct}} {
+ dict set fc_dict correct_when \
+ [:comp_correct_when_from_value \
+ [:get_named_sub_component_value correct_when]]
+ }
+
+ set form [:form_markup -interaction text -intro_text $intro_text -body @answer@]
+ lappend fc \
+ @categories:off @cr_fields:hidden \
+ [:dict_to_spec $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 [[self class] set auto_correct]
+ ${: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 25}
+ }
+ short_text_interaction set item_type ShortText
+ short_text_interaction set closed_question_type false
+
+ 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}"
+
+ if {[acs_user::site_wide_admin_p]} {
+ set substvalues "{substvalues {textarea,label=Substitution Values}}"
+ } else {
+ set substvalues ""
+ }
+ #{substvalues {textarea,label=Substitution Values}}
+ :create_components [subst {
+ {text {$widget,height=100px,label=#xowf.exercise-text#,plugins=OacsFs}}
+ {attachments {[:attachments_widget ${:nr_attachments}]}}
+ {answer {short_text_field,repeat=1..${:nr},label=}}
+ $substvalues
+ }]
+ set :__initialized 1
+ }
+
+ short_text_interaction instproc convert_to_internal {} {
+ next
+
+ set intro_text [:get_named_sub_component_value text]
+ append intro_text [:text_attachments]
+ set answerFields [:get_named_sub_component_value -from_repeat answer]
+ if {[acs_user::site_wide_admin_p]} {
+ set substvalues [:get_named_sub_component_value substvalues]
+ } else {
+ set substvalues ""
+ }
+ set options {}
+ set render_hints {}
+ set answer {}
+ set solution {}
+ 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 [:comp_correct_when_from_value [dict get $value $fieldName.correct_when]]
+ lappend solution [dict get $value $fieldName.solution]
+ lappend render_hints [list \
+ words [dict get $value $fieldName.options] \
+ lines [dict get $value $fieldName.lines]]
+ }
+
+ set fc_dict { _name answer _type text_fields disabled_as_div 1 label ""}
+ 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 options $options
+ dict set fc_dict answer $answer
+ dict set fc_dict descriptions $solution
+ dict set fc_dict render_hints $render_hints
+ dict set fc_dict substvalues $substvalues
+
+ set form [:form_markup -interaction short_text -intro_text $intro_text -body @answer@]
+
+ set fc {}
+ lappend fc \
+ [:dict_to_spec $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
+ ${:object} set_property -new 1 substvalues $substvalues
+ }
+
+ #
+ # ::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
+ }
+
+ 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}"
+ } " "]
+ #
+ # The options field is made "required" to avoid deselecting.
+ #
+ set textEntryConfigSpec [subst {
+ {options {radio,horizontal=true,form_item_wrapper_CSSclass=form-inline,options=$render_hints,value=multiple_words,required,label=#xowf.answer#}}
+ {lines {number,form_item_wrapper_CSSclass=form-inline,value=1,min=1,label=#xowf.lines#}}
+ }]
+
+ :create_components [subst {
+ {text {$widget,height=100px,label=#xowf.sub_question#,plugins=OacsFs}}
+ $textEntryConfigSpec [:correct_when_spec]
+ {solution {textarea,rows=2,label=#xowf.Solution#}}
+ }]
+ set :__initialized 1
+ }
+
+}
+
namespace eval ::xowiki::formfield {
+ ###########################################################
+ #
+ # ::xowiki::formfield::reorder_interaction
+ #
+ ###########################################################
+ Class create reorder_interaction -superclass TestItemField -parameter {
+ {nr 25}
+ }
+ reorder_interaction set item_type {Reorder}
+ reorder_interaction set closed_question_type true
+
+ reorder_interaction instproc initialize {} {
+ if {${:__state} ne "after_specs"} return
+ #
+ # Create component structure.
+ #
+ set widget [test_item set richtextWidget]
+
+ :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
+ }
+
+ set fc_dict {_name answer _type reorder_box}
+ 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 grading [${:parent_field} get_named_sub_component_value grading]
+
+ set form [:form_markup -interaction reorder -intro_text $intro_text -body @answer@]
+ set fc {}
+ lappend fc \
+ [:dict_to_spec $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
+ #ns_log notice "${:name} FINAL FC $fc"
+ }
+}
+
+
+
+namespace eval ::xowiki::formfield {
###########################################################
#
+ # ::xowiki::formfield::mc_interaction2
+ #
+ ###########################################################
+
+ Class create mc_interaction2 -superclass TestItemField -parameter {
+ {nr 25}
+ {multiple true}
+ }
+ mc_interaction2 set item_type {SC MC}
+ mc_interaction2 set closed_question_type 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 {}
+ set solution {}
+
+ foreach {fieldName value} $answerFields {
+ #ns_log notice ...fieldName=$fieldName->$value
+ #set af answer[incr count]
+ set text [dict get $value $fieldName.text]
+
+ # Trim leading
and whitespace. Trimming leading
is
+ # necessary since this causes a newline in the checkbox label
+ regsub -all {^(\s|
)+} $text "" text
+ regsub -all {(\s|
)+$} $text "" text
+ # skip empty entries
+ if {$text eq ""} {
+ continue
+ }
+ lappend options [list $text [incr count]]
+ lappend correct [dict get $value $fieldName.correct]
+ lappend solution [dict get $value $fieldName.solution]
+ }
+
+ dict set fc_dict _name answer
+ dict set fc_dict _type [expr {${:multiple} ? "checkbox" : "radio"}]
+ 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 [expr {${:multiple} ? "mc" : "sc"}]
+ set form [:form_markup -interaction $interaction -intro_text $intro_text -body @answer@]
+ set fc {}
+ lappend fc \
+ [:dict_to_spec $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 closed_question_type false
+ upload_interaction set item_type Upload
+
+ 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"
+
+ set file_dict {_name answer _type file}
+ 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 form [:form_markup -interaction upload -intro_text $intro_text -body @answer@]
+ lappend fc \
+ @categories:off @cr_fields:hidden \
+ [:dict_to_spec $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 closed_question_type]
+ ${:object} set_property -new 1 has_solution false
+ }
+}
+
+
+namespace eval ::xowiki::formfield {
+
+ ###########################################################
+ #
# ::xowiki::formfield::test_section
#
###########################################################
- Class create test_section -superclass {form_page} -parameter {
+ Class create test_section -superclass {TestItemField} -parameter {
{multiple true}
+ {form en:edit-interaction.wf}
}
+ test_section set item_type Composite
+ test_section set closed_question_type false
+ test_section instproc initialize {} {
+
+ if {${:__state} ne "after_specs"} {
+ return
+ }
+ next
+ set widget [test_item set richtextWidget]
+
+ # We could exclude the "self" item (inclusion would lead to
+ # infinite loops), but that is as well excluded, when no Composite
+ # items are allowed.
+ #
+ # set item_id [${:object} item_id]
+ # {selection {form_page,form=en:edit-interaction.wf,unless=_item_id=$item_id,multiple=true}}
+
+ :create_components [subst {
+ {text {$widget,height=150px,label=#xowf.exercise-text#,plugins=OacsFs}}
+ {selection {form_page,form=en:edit-interaction.wf,unless=item_type=Composite|PoolQuestion,multiple=true,label=#xowf.question_selection#}}
+ }]
+
+ set :__initialized 1
+ }
+
test_section instproc pretty_value {v} {
return [${:object} property form ""]
}
@@ -387,90 +1105,7075 @@
# 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 a the
- # prefix for the form-fields. This method must be most likely
- # extended for other question types.
- #
- set form "
\n"
- set fc "@categories:off @cr_fields:hidden\n"
- set intro_text [${:object} property _text]
- append form "$intro_text\n\n"
- foreach v [:value] {
- # TODO: the next two commands should not be necessary to lookup
- # again, since the right values are already loaded into the
- # options
- set item_id [[${:object} package_id] lookup -name $v]
- set page [::xo::db::CrClass get_instance_from_db -item_id $item_id]
- append form "
[$item_id title]
\n"
- set prefix c$item_id
- set __ia [$page set instance_attributes]
+ # the form constraints.
+ #
+ set intro_text [:get_named_sub_component_value text]
+ set selection [:get_named_sub_component_value selection]
+
+ #
+ # Load the forms specified via "selection".
+ #
+ set package_id [${:object} package_id]
+ set formObjs [::$package_id instantiate_forms \
+ -forms [join [split $selection \n] |] \
+ -default_lang en]
+
+ # foreach formObj $formObjs {
+ # set substvalues [$formObj property substvalues]
+ # if {$substvalues ne ""} {
+ # ns_log notice ".... [$formObj name] has substvalues $substvalues"
+ # set d [:QM percent_substitute_in_form \
+ # -obj ${:object} \
+ # -form_obj $formObj \
+ # -position $position \
+ # $html]
+ # $form_obj set_property form [dict get $d form]
+ # $form_obj set_property form_constraints [dict get $d form_constraints]
+ # $form_obj set_property disabled_form_constraints [dict get $d disabled_form_constraints]
+ # }
+ # }
+
+ #
+ # Build a form with the usual numbering containing all the
+ # formObjs and remove all form tags.
+ #
+ set number 0
+ set numbers [lmap formObj $formObjs {incr number}]
+
+ set option_dict {with_minutes t with_points f with_title f}
+ foreach field {show_minutes show_points show_title} {
+ set value [${:parent_field} get_named_sub_component_value -default "" $field]
+ if {$value ne ""} {
+ dict set option_dict $field $value
+ }
+ }
+ set title_options [lmap kind {minutes points title} {
+ if {![dict get $option_dict show_$kind]} {
+ continue
+ }
+ set result "-with_$kind"
+ }]
+ set question_infos [:QM question_info \
+ -question_number_label "#xowf.subquestion#" \
+ {*}$title_options \
+ -numbers $numbers \
+ -no_position \
+ -obj ${:object} \
+ $formObjs]
+ # ns_log notice "SELECTION question_info '$question_infos'"
+
+ #
+ # Build a single clean form based on the question infors,
+ # containing all selected items.
+ #
+ set aggregatedForm [:QM aggregated_form \
+ -with_grading_box hidden \
+ $question_infos]
+ set aggregatedFC [dict get $question_infos form_constraints]
+ #ns_log notice "SELECTION aggregatedFC\n$aggregatedFC"
+
+ #
+ # The following regexps are dangerous (esp. on form
+ # constraints). I think, we have already a better function for
+ # this.
+ #
+ set names [regexp -inline -all {@([^@]+_)@} $aggregatedForm]
+ foreach {. name} $names {
+ regsub -all "@$name@" $aggregatedForm "@answer_$name@" aggregatedForm
+ regsub -all ${name}: $aggregatedFC "answer_${name}:" aggregatedFC
+ }
+
+ ns_log notice "AGGREGATED FORM $aggregatedForm\nFC\n$aggregatedFC\n"
+
+ #
+ # Automatically compute the minutes and points of the composite
+ # field and update the form field.
+ #
+ set total_minutes [:QM total_minutes $question_infos]
+ set total_points [:QM total_points $question_infos]
+
+ [${:parent_field} get_named_sub_component minutes] value $total_minutes
+ [${:parent_field} get_named_sub_component points] value $total_points
+
+ set form [:form_markup -interaction composite -intro_text $intro_text -body $aggregatedForm]
+
+ ${:object} set_property -new 1 form $form
+ ${:object} set_property -new 1 form_constraints $aggregatedFC
+ set anon_instances true ;# TODO make me configurable
+ ${:object} set_property -new 1 anon_instances $anon_instances
+ }
+}
+
+namespace eval ::xowiki::formfield {
+ ###########################################################
+ #
+ # ::xowiki::formfield::pool_question
+ #
+ ###########################################################
+
+ Class create pool_question -superclass TestItemField -parameter {
+ }
+ pool_question set closed_question_type false ; # the replacement query might be or not autocorrection capable
+ pool_question set item_type PoolQuestion
+
+ pool_question set item_types {
+ Composite
+ MC
+ Reorder
+ SC
+ ShortText
+ Text
+ Upload
+ }
+ pool_question proc all_item_types_selected {item_types} {
+ #
+ # Check, if all item types were selected
+ #
+ foreach item_type [pool_question set item_types] {
+ if {$item_type ni $item_types} {
+ return 0
+ }
+ }
+ return 1
+ }
+ pool_question instproc initialize {} {
+ if {${:__state} ne "after_specs"} {
+ return
+ }
+ set item_types [pool_question set item_types]
+ set item_type_options [lmap item_type $item_types {
+ list #xowf.menu-New-Item-${item_type}Interaction# $item_type
+ }]
+
+ set current_folder_id [[${:object} parent_id] item_id]
+ set parent_folder_id [::$current_folder_id parent_id]
+ set fi [::xowiki::includelet::folders new -destroy_on_cleanup]
+ set folder_objs [$fi collect_folders \
+ -package_id [${:object} package_id] \
+ -parent_id $parent_folder_id]
+ set folder_options [list]
+ lappend folder_options {*}[lmap folder_obj $folder_objs {
+ if {[$folder_obj parent_id] ne $parent_folder_id} {
+ continue
+ }
+ list [$folder_obj title] ../[$folder_obj name]
+ }]
+
+ set pool_dict {_name folder _type select}
+ dict set pool_dict required true
+ dict set pool_dict options $folder_options
+ dict set pool_dict default ../[::$current_folder_id name]
+ dict set pool_dict label #xowf.pool_question_folder#
+
+ set item_dict {_name item_types _type checkbox}
+ dict set item_dict options $item_type_options
+ dict set item_dict default $item_types
+ dict set item_dict label #xowf.pool_question_item_types#
+
+ :create_components [subst {
+ [list [:dict_to_spec -aspair $pool_dict]]
+ [list [:dict_to_spec -aspair $item_dict]]
+ {pattern {text,default=*,label=#xowf.pool_question_pattern#}}
+ }]
+
+ set :__initialized 1
+ }
+
+ pool_question instproc convert_to_internal {} {
+ next
+ set allowed_item_types [:get_named_sub_component_value item_types]
+
+ set fc_dict {_name answer _type pool_question_placeholder}
+ dict set fc_dict folder [:get_named_sub_component_value folder]
+ dict set fc_dict pattern [:get_named_sub_component_value pattern]
+ dict set fc_dict item_types $allowed_item_types
+
+ set form "
@answer@
"
+ lappend fc \
+ @categories:off @cr_fields:hidden \
+ [:dict_to_spec $fc_dict]
+
+ ${:object} set_property -new 1 form $form
+ ${:object} set_property -new 1 form_constraints $fc
+
+ #
+ # Turn on "auto_correct" when for every selected item_type, *all*
+ # items are fully closed and therefore suited for
+ # auto_correction. For example, for composite questions, this might
+ # or might not be true (to handle this more aggressively, we would
+ # have to iterate over all exercises of this question types and
+ # check their detailed subcomponents).
+ #
+ set auto_correct 1
+ foreach class [::xowiki::formfield::TestItemField info subclass -closure] {
+ if {[$class exists item_type]} {
+ foreach item_type [$class set item_type] {
+ if {$item_type in $allowed_item_types && ![$class set closed_question_type]} {
+ set auto_correct 0
+ break
+ }
+ }
+ }
+ }
+ #ns_log notice "... FINAL auto_correct $auto_correct (allowed $allowed_item_types)"
+ ${:object} set_property -new 1 auto_correct $auto_correct
+ }
+
+ Class create pool_question_placeholder -superclass {TestItemField} -parameter {
+ folder
+ pattern
+ item_types
+ }
+
+}
+
+
+
+############################################################################
+# Generic Assessment interface
+############################################################################
+
+namespace eval ::xowf::test_item {
+
+ # the following is not yet ready for prime time.
+ if {0 && [acs::icanuse "nx::alias object"]} {
+ set register_command "alias"
+ } else {
+ set register_command "forward"
+ }
+
+ nx::Class create AssessmentInterface {
+ #
+ # Abstract class for common functionality
+ #
+
+ :public alias dict_value ::xowiki::formfield::dict_value
+ :alias fc_to_dict ::xowiki::formfield::fc_to_dict
+
+ :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"
+ }
+ }
+
+ #----------------------------------------------------------------------
+ # Class: AssessmentInterface
+ # Method: render_feedback_files
+ #----------------------------------------------------------------------
+ :public method render_feedback_files {
+ {-question_name:required}
+ {-feedbackFiles {}}
+ } {
#
- # If for some reason, we have not form entry, we ignore it.
- # TODO: We should deal here with computed forms and with true
- # ::xowiki::forms as well...
+ # Render feedback files which are children of the submit
+ # instances. Note that one submit instances contains the
+ # feedback files for all questions. For associating the files
+ # with the right quesitions, the content repository name has to
+ # start with "file:${questions_name}/*
#
- if {![dict exists $__ia form]} {
- :msg "$v has no form included"
- continue
+ # @param question_name name (prefix) for selecting files to be shown
+ # @param feedbackFiles is of pairs containing "item_id" and "name"
+ #
+ # @return HTML rendering
+ #
+ set chunkList [lmap pair $feedbackFiles {
+ lassign $pair item_id name
+ #ns_log warning "render_feedback_files: check '$name'"
+ if {![regexp {^file:(.*)/(.*)$} $name . qn fileName]} {
+ ns_log warning "render_feedback_files: ignoring file with unexpected name '$name'"
+ continue
+ }
+ if {$qn ne $question_name} {
+ #
+ # The found file is for a different question
+ #
+ #ns_log notice "render_feedback_files: required '$question_name' found '$qn'"
+ continue
+ }
+ set fileObj [::xowiki::File get_instance_from_db -item_id $item_id]
+
+ #
+ # Provide markup for delete likn. When the user has no rights
+ # to delete the file, do not offer the delete link.
+ #
+ set local_return_url [ad_return_url]
+ set package_id [$fileObj package_id]
+ set deleteLink [::$package_id make_link $fileObj delete local_return_url]
+ if {$deleteLink ne ""} {
+ set deleteLinkHTML \
+ [subst [ns_trim -delimiter | {
+ |
+ |
+ |
+ }]]
+ } else {
+ set deleteLinkHTML ""
+ }
+ set viewLink [::$package_id make_link $fileObj download]
+ if {$viewLink ne ""} {
+ set viewHref [subst {href="[ns_quotehtml $viewLink]"}]
+ set viewTitle {title="#xowiki.view#"}
+ } else {
+ set viewHref ""
+ set viewTitle ""
+ }
+ set iconName [::xowiki::CSS icon_name $fileName]
+ subst [ns_trim -delimiter | {
+ |
+ |
+ |
+ | [ns_quotehtml $fileName]$deleteLinkHTML
+ |
+ |
}]
+ }]
+
+ set HTML ""
+ if {[llength $chunkList] > 0} {
+ #
+ # Since the content will be post-processed via tDOM, we have
+ # to resolve the ADP tags already here.
+ #
+ append HTML \
+ {
+ }]
+ append HTML [:export_links -examWf $examWf -filter_form_ids $filter_form_ids -b_aggregate true]
+
+ return $HTML
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Answer_manager
+ # Method: render_full_submission_form
+ #----------------------------------------------------------------------
+ :method render_full_submission_form {
+ -wf:object
+ -submission:object
+ -filter_form_ids:integer,0..n
+ -with_feedback:switch
+ -with_correction_notes:switch
+ } {
+ #
+ # Compute the HTML of the full submission of the user with all
+ # form fields instantiated according to randomization.
+ #
+ # @param filter_form_ids used for filtering questions
+ # @return HTML of question form object containing all (wanted) questions
+ #
+
+ #
+ # Flush all form fields, since their contents depend on
+ # randomization. In later versions, we should introduce a more
+ # intelligent caching respecting randomization.
+ #
+ 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
+
+ #
+ # 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.
+ #
+ xo::cc eval_as_user -user_id [$submission creation_user] {
+ $submission set __feedback_mode 2
+ $submission set __form_objs $filter_form_ids
+ $submission set __aggregated_form_options \
+ "-with_feedback=$with_feedback -with_correction_notes=$with_correction_notes"
+ set question_form [$submission render_content]
+ }
+
+ return $question_form
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Answer_manager
+ # Method: get_non_empty_file_formfields
+ #----------------------------------------------------------------------
+ :method get_non_empty_file_formfields {
+ {-submission:object}
+ } {
+ if {[$submission exists __form_fields]} {
+ set objs [lmap {name obj} [$submission set __form_fields] {set obj}]
+
+ #
+ # Filter out the form-fields, which have a nonempty
+ # revision_id.
+ #
+ return [::xowiki::formfield::child_components \
+ -filter {[$_ hasclass "::xowiki::formfield::file"]
+ && [dict exists [$_ value] revision_id]
+ && [dict get [$_ value] revision_id] ne ""} \
+ $objs]
+ } else {
+ return ""
+ }
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Answer_manager
+ # Method: pretty_formfield_name
+ #----------------------------------------------------------------------
+ :method pretty_formfield_name {f_obj} {
+ regsub {_[.]answer([0-9]+)} [$f_obj name] {-\1} exercise_name
+ #ns_log notice "PRETTY '[$f_obj name]' -> '$exercise_name'"
+ return $exercise_name
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Answer_manager
+ # Method: export_file_submission
+ #----------------------------------------------------------------------
+ :method export_file_submission {
+ {-submission:object}
+ {-zipFile:object}
+ {-check_for_file_submission_exists:boolean false}
+ } {
+ #
+ # Get all nonempty file form-fields and add these to a zip
+ # file. The filename is composed of the user, the exercise and
+ # the provided file-name.
+ #
+ foreach f_obj [:get_non_empty_file_formfields -submission $submission] {
+ set exercise_name [:pretty_formfield_name $f_obj]
+ foreach file_revision_id [dict get [$f_obj value] revision_id] {
+ set file_object [::xo::db::CrClass get_instance_from_db -revision_id $file_revision_id]
+ set download_file_name ""
+ append download_file_name \
+ [$submission set online-exam-userName] "-" \
+ $exercise_name "-" \
+ [$file_object title]
+ $zipFile addFile \
+ [$file_object full_file_name] \
+ [$zipFile cget -name]/[ad_sanitize_filename $download_file_name]
+ }
+ }
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Answer_manager
+ # Method: dom ensemble for tDOM manipluations
+ #----------------------------------------------------------------------
+ :method "dom node replace" {domNode xquery script} {
+ set node [$domNode selectNodes $xquery]
+ if {$node ne ""} {
+ foreach child [$node childNodes] {
+ $child delete
+ }
+ :uplevel [list $node appendFromScript $script]
+ }
+ }
+ :method "dom node replaceXML" {domNode xquery XML} {
+ set node [$domNode selectNodes $xquery]
+ if {$node ne ""} {
+ foreach child [$node childNodes] {
+ $child delete
+ }
+ #
+ # There is in tDOM only an appendXML and no appendHTML. If the
+ # replace-text is an " XML-parse fails since there is no
+ # ending tag. So, we use the following heuristic. Note that
+ # this does not happen in full installations, where icon sets
+ # are available, but it might show up in a native regression
+ # test with minimal packages.
+ #
+ if {[string match "
+ }
+ :uplevel [list $node appendXML $XML]
+ }
+ }
+ :method "dom node appendXML" {domNode xquery XML} {
+ set node [$domNode selectNodes $xquery]
+ :uplevel [list $node appendXML $XML]
+ }
+ :method "dom node delete" {domNode xquery} {
+ set nodes [$domNode selectNodes $xquery]
+ foreach node $nodes {
+ $node delete
+ }
+ }
+ :method "dom class add" {domNode xquery class} {
+ set nodes [$domNode selectNodes $xquery]
+ foreach node $nodes {
+ set oldClass [$node getAttribute class]
+ if {$class ni $oldClass} {
+ $node setAttribute class "$oldClass $class"
+ }
+ }
+ }
+ :method "dom class remove" {domNode xquery class} {
+ set nodes [$domNode selectNodes $xquery]
+ foreach node $nodes {
+ set oldClass [$node getAttribute class]
+ set pos [lsearch $oldClass $class]
+ if {$pos != -1} {
+ $node setAttribute class [lreplace $oldClass $pos $pos]
+ }
+ }
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Answer_manager
+ # Method: postprocess_question_html
+ #----------------------------------------------------------------------
+ :method postprocess_question_html {
+ {-question_form:required}
+ {-achieved_points:required}
+ {-manual_grading:required}
+ {-submission:object,required}
+ {-runtime_panel_view:required}
+ {-exam_state:required}
+ {-feedbackFiles ""}
+ } {
+ #
+ # Post-process the HTML of a question by adding information of
+ # the student as data attributes, such as achieved and
+ # achievable points, setting CSS classes, mangling names of
+ # composite questions to match with the data in achieved_points,
+ #
+ # @return HTML block
+
+ #ns_log notice "QF=$question_form"
+ dom parse -html -- $question_form doc
$doc documentElement root
- set alt_inputs [list]
- set alt_values [list]
- foreach html_type {input textarea} {
- foreach n [$root selectNodes "//$html_type\[@name != ''\]"] {
- set alt_input [$n getAttribute name]
- $n setAttribute name $prefix-$alt_input
- if {$html_type eq "input"} {
- set alt_value [$n getAttribute value]
+ if {$root eq ""} {
+ error "form '$form' is not valid"
+ }
+
+ set per_question_points ""
+ foreach pd [:dict_value $achieved_points details] {
+ set qn [dict get $pd attributeName]
+ dict set per_question_points $qn achieved [dict get $pd achieved]
+ dict set per_question_points $qn achievable [dict get $pd achievable]
+ }
+
+ #
+ # The aggregated form (method :aggregated_form) is generated
+ # before submissions are available. For e.g., the exam protocol,
+ # the same grading-box with the same raw data will be reused
+ # potentially per submission. To ensure uniqueness of the HTML
+ # IDs for the dialogs, we have to fill in the submission IDs.
+ #
+ set grading_boxes [$root selectNodes {//div[contains(@class,'grading-box')]}]
+ foreach grading_box $grading_boxes {
+ $grading_box setAttribute id [$grading_box getAttribute id]-[$submission item_id]
+ }
+
+ #
+ # For every composite question:
+ #
+ # - update the question_name of the subquestion by prefixing it
+ # with the name of the composite, since this is what we have
+ # in the details of achieved_points.
+ # - hide the grading box of the composite
+ # - unhide the grading box of the composite children
+ #
+ set composite_grading_boxes \
+ [$root selectNodes \
+ {//div[@data-item_type='Composite']/div[contains(@class,'grading-box')]}]
+ foreach composite_grading_box $composite_grading_boxes {
+ set composite_qn [$composite_grading_box getAttribute "data-question_name"]
+ set parentNode [$composite_grading_box parentNode]
+ :dom class add $composite_grading_box {.} [::xowiki::CSS class d-none]
+ foreach grading_box [$parentNode selectNodes {div//div[contains(@class,'grading-box')]}] {
+ set qn [$grading_box getAttribute data-question_name]
+ regsub {^answer_} $qn ${composite_qn}_ new_qn
+ #ns_log notice "CHILD of Composite: rename QN from $qn to $new_qn"
+ $grading_box setAttribute data-question_name $new_qn
+ $grading_box setAttribute id ${composite_qn}_[$grading_box getAttribute id]
+ :dom class remove $grading_box {.} [::xowiki::CSS class d-none]
+ #
+ # The composite questions are prerendered and do not have
+ # hint boxes, since we do not want to have even hidden in
+ # the HTML rendering show to the student during the
+ # exam. Therefore, we add these now for the exam protocol in
+ # an extra step. We try to add here both, feedback and
+ # correction notes (if available). The loop over all grading
+ # boxes below should care for the visibility of the hint
+ # boxes due to percentages.
+ #
+ if {[$grading_box hasAttribute data-question_id]} {
+ set subquestion_id [$grading_box getAttribute data-question_id]
+ set subquestion_obj [::xowiki::FormPage get_instance_from_db -item_id $subquestion_id]
+ #ns_log notice "CHILD of Composite has form_id $subquestion_id [nsf::is object ::$subquestion_id]"
+ set HTML [:QM hint_boxes \
+ -question_obj $subquestion_obj \
+ -with_feedback=1 \
+ -with_correction_notes=1]
+ if {$HTML ne ""} {
+ dom parse -simple -html $HTML hintsDoc
+ $hintsDoc documentElement hintsBody
+ foreach child $hintsBody {
+ [$grading_box parentNode] appendChild $child
+ }
+ }
} else {
- set alt_value ""
+ #
+ # Probably some legacy item
+ #
+ ::util_user_message -message "Composite Exercise looks like legacy data; please edit+save"
+ ad_log warning "composite_grading_box has no data-question_id"
}
- lappend alt_inputs $alt_input
- lappend alt_values $alt_value
}
}
- # We have to drop the toplevel
of the included form
- foreach n [$root childNodes] {append form [$n asHTML]}
- append form "
\n"
+
#
- # Replace the formfield names in the form constraints
+ # Composite grading-boxes are done, now general code over all
+ # grading-boxes.
#
- foreach f [dict get $__ia form_constraints] {
- if {[regexp {^([^:]+):(.*)$} $f _ field_name definition]} {
- if {[string match @* $field_name]} continue
- # keep all form-constraints for which we have altered the name
- #my msg "old fc=$f, [list lsearch -exact $alt_inputs $field_name] => [lsearch -exact $alt_inputs $field_name] $alt_values"
- set ff [${:object} create_raw_form_field -name $field_name -spec $definition]
- #my msg "ff answer => '[$ff answer]'"
- if {$field_name in $alt_inputs} {
- lappend fc $prefix-$f
- } elseif {[$ff exists answer] && $field_name eq [$ff answer]} {
- # this rules is for single choice
- lappend fc $prefix-$f
+ set submission_state [$submission state]
+ #set noManualGrading [expr {$submission_state ne "done" || $exam_state eq "published"}]
+ set noManualGrading [expr {$exam_state eq "published"}]
+
+ set grading_boxes [$root selectNodes {//div[contains(@class,'grading-box')]}]
+ foreach grading_box $grading_boxes {
+ set qn [$grading_box getAttribute "data-question_name"]
+ set item_node [$grading_box parentNode]
+ set item_type [expr {[$item_node hasAttribute "data-item_type"]
+ ? [$item_node getAttribute "data-item_type"]
+ : ""}]
+
+ set feedbackFilesHTML [:render_feedback_files \
+ -question_name $qn \
+ -feedbackFiles $feedbackFiles]
+
+ #ns_log notice "FEEDBACK '$qn' feedbackFiles $feedbackFiles HTML\n$feedbackFilesHTML"
+ #ns_log notice "... QN '$qn' item_type '$item_type'" \
+ "submission state $submission_state" \
+ "exam state $exam_state noManualGrading $noManualGrading"
+
+ if {$noManualGrading} {
+ :dom class add $grading_box {a[contains(@class,'manual-grade')]} \
+ [::xowiki::CSS class d-none]
+ }
+
+ #
+ # Get manual gradings, if these were already provided.
+ #
+ if {[dict exists $manual_grading $qn achieved]} {
+ set achieved [dict get $manual_grading $qn achieved]
+ } else {
+ set achieved ""
+ }
+ if {[dict exists $manual_grading $qn comment]} {
+ set comment [dict get $manual_grading $qn comment]
+ } else {
+ set comment ""
+ }
+
+ if {[dict exists $per_question_points $qn achieved]} {
+ #
+ # Manual grading has higher priority than automated grading.
+ #
+ if {$achieved eq ""} {
+ set achieved [dict get $per_question_points $qn achieved]
}
+ set achievable [dict get $per_question_points $qn achievable]
+ $grading_box setAttribute data-autograde 1
+ } else {
+ set achievable ""
}
+ #ns_log notice "... QN '$qn' item_type $item_type achieved '$achieved' achievable '$achievable'"
+
+ set percentage ""
+ if {$achieved eq ""} {
+ set warning [::template::icon \
+ -class [xowiki::CSS class text-warning] \
+ -name warn ]
+ set pencil [::template::icon -name pencil]
+ :dom node replaceXML $grading_box \
+ {span[@class='points']} \
+ [dict get $warning HTML]
+ :dom node replaceXML $grading_box \
+ {a[@class='manual-grade-edit']/span/..} \
+ [dict get $pencil HTML]
+ #
+ # The last case with "span/.." is for legacy cases, where
+ # composite items were generated before bootstrap5 support
+ # and/or where composite items were generated under
+ # bootstrap5 but are rendered with bootstrap3
+ #
+ :dom node replaceXML $grading_box \
+ {a[@class='manual-grade']/span/..} \
+ [dict get $pencil HTML]
+
+ } else {
+ :dom node replace $grading_box {span[@class='points']} {::html::t $achieved}
+ if {$achievable ne ""} {
+ set percentage [format %.2f [expr {$achieved*100.0/$achievable}]]
+ :dom node replace $grading_box {span[@class='percentage']} {::html::t ($percentage%)}
+ }
+ }
+ #
+ # handling of legacy items
+ #
+ set changes [expr {[::xowiki::CSS toolkit] eq "bootstrap"
+ ? {bs-toggle toggle bs-target target}
+ : {toggle bs-toggle target bs-target}}]
+ foreach node [$grading_box selectNodes {a[@class='manual-grade']}] {
+ foreach {old new} $changes {
+ if {[$node hasAttribute data-$old]} {
+ $node setAttribute data-$new [$node getAttribute data-$old]
+ $node removeAttribute data-$old
+ }
+ }
+ }
+
+ if {$feedbackFilesHTML ne ""} {
+ #ns_log notice "REPLACE thumbnail-files-wrapper in\n[$grading_box asXML]"
+ if {[llength [$grading_box selectNodes {div[@class='thumbnail-files-wrapper']}]] == 0} {
+ #
+ # Must be a legacy composite item without the thumbnail
+ # wrapper.
+ #
+ $grading_box appendXML \
+ {}
+ }
+ :dom node replaceXML $grading_box \
+ {div[@class='thumbnail-files-wrapper']} \
+ $feedbackFilesHTML
+ }
+ #
+ # When "comment" is empty, do not show the label.
+ #
+ :dom node replace $grading_box {span[@class='comment']} {::html::t $comment}
+ if {$comment eq ""} {
+ :dom class add $grading_box {span[@class='feedback-label']} \
+ [::xowiki::CSS class d-none]
+ } else {
+ :dom class remove $grading_box {span[@class='feedback-label']} \
+ [::xowiki::CSS class d-none]
+ }
+
+ $grading_box setAttribute data-user_id [$submission creation_user]
+ $grading_box setAttribute data-user_name [$submission set online-exam-userName]
+ $grading_box setAttribute data-full_name [$submission set online-exam-fullName]
+ $grading_box setAttribute data-achieved $achieved
+ $grading_box setAttribute data-achievable $achievable
+ $grading_box setAttribute data-comment $comment
+ $grading_box setAttribute data-link [::[$submission package_id] make_link $submission file-upload]
+
+ #
+ # Feedback handling (should be merged with the individual feedback)
+ #
+ set correct_feedback_node [$item_node selectNodes {div[contains(@class,'feedback-correct')]}]
+ set incorrect_feedback_node [$item_node selectNodes {div[contains(@class,'feedback-incorrect')]}]
+ set correction_notes_node [$item_node selectNodes {div[contains(@class,'correction-notes')]}]
+
+ if {$percentage ne "" && $percentage < 50 && $incorrect_feedback_node ne ""} {
+ #
+ # Remove positive and keep negative feedback.
+ #
+ if {$correct_feedback_node ne ""} {
+ $correct_feedback_node delete
+ set correct_feedback_node ""
+ }
+ }
+ if {$correct_feedback_node ne "" && $incorrect_feedback_node ne ""} {
+ #
+ # If we still have a positive feedback, remove negative
+ # feedback.
+ #
+ $incorrect_feedback_node delete
+ }
+
+ #
+ # In student review mode ('Einsicht'), remove
+ # - correction notes, and
+ # - edit controls.
+ #
+ if {$runtime_panel_view eq "student"} {
+ if {$correction_notes_node ne ""} {
+ $correction_notes_node delete
+ }
+ :dom node delete $grading_box {a}
+ }
}
+ return [$root asHTML]
}
- append 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
- #my msg "fc=$fc"
+
+ #----------------------------------------------------------------------
+ # Class: Answer_manager
+ # Method: render_submission=exam_protocol
+ #----------------------------------------------------------------------
+ :method render_submission=exam_protocol {
+ {-autograde:boolean false}
+ {-combined_form_info}
+ {-examWf:object}
+ {-exam_question_dict}
+ {-filter_submission_id:integer,0..1 ""}
+ {-filter_form_ids:integer,0..n ""}
+ {-grading_scheme:object}
+ {-recutil:object,0..1 ""}
+ {-zipFile: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 userName [$submission set online-exam-userName]
+ set fullName [$submission set online-exam-fullName]
+ set user_id [$submission set creation_user]
+ set manual_gradings [:get_exam_results -obj $examWf manual_gradings]
+ set results ""
+
+ #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 } {
+ #
+ # We have always an initial revision. This revision might be
+ # already updated via autosave, in which case we show the
+ # content.
+ #
+ set rev [lindex $revisions 0]
+ set unmodified [string equal [ns_set get $rev last_modified] [ns_set get $rev creation_date]]
+ if {$unmodified} {
+ ns_log notice "online-exam: submission of $userName is empty. Ignoring."
+ return ""
+ }
+ }
+
+ #
+ # We have to distinguish between the answered attributes (based
+ # on the instance attributes in the database) and the answer
+ # attributes, which should be rendered. The latter one might be
+ # a subset, especially in cases, where filtering (e.g., show
+ # only one question of all candidates) happens.
+ #
+ set exam_question_objs [dict values $exam_question_dict]
+
+ set answeredAnswerAttributes \
+ [:FL answer_attributes [$submission instance_attributes]]
+ set formAnswerAttributeNames \
+ [dict keys [:FL name_to_question_obj_dict $exam_question_objs]]
+ set usedAnswerAttributes {}
+ foreach {k v} $answeredAnswerAttributes {
+ if {$k in $formAnswerAttributeNames} {
+ dict set usedAnswerAttributes $k $v
+ }
+ }
+
+ #ns_log notice "filter_form_ids <$filter_form_ids>"
+ #ns_log notice "question_objs <[dict get $combined_form_info question_objs]>"
+ #ns_log notice "answeredAnswerAttributes <$answeredAnswerAttributes>"
+ #ns_log notice "formAnswerAttributeNames <$formAnswerAttributeNames> [:FL name_to_question_obj_dict $filter_form_ids]"
+ #ns_log notice "usedAnswerAttributes <$usedAnswerAttributes>"
+
+ #
+ # "render_full_submission_form" calls "summary_form" to obtain the
+ # user's answers to all questions.
+ #
+ set question_form [:render_full_submission_form \
+ -wf $wf \
+ -submission $submission \
+ -filter_form_ids $filter_form_ids \
+ -with_correction_notes=[expr {$runtime_panel_view ne "student"}] \
+ -with_feedback \
+ ]
+
+ if {$recutil ne ""} {
+ :export_answer \
+ -submission $submission \
+ -html $question_form \
+ -combined_form_info $combined_form_info \
+ -recutil $recutil
+ }
+
+ if {$zipFile ne ""} {
+ :export_file_submission -submission $submission -zipFile $zipFile
+ }
+
+ #
+ # Achieved_points are computed for autograded and manually
+ # graded exams.
+ #
+ set achieved_points [:achieved_points \
+ -manual_grading [:dict_value $manual_gradings $user_id] \
+ -submission $submission \
+ -exam_question_dict $exam_question_dict \
+ -answer_attributes $usedAnswerAttributes]
+ dict set achieved_points totalPoints $totalPoints
+
+ #ns_log notice "user $user_id: achieved_points [dict get $achieved_points details]"
+ #ns_log notice "user $user_id: manual_gradings [:dict_value $manual_gradings $user_id]"
+
+ foreach pd [:dict_value $achieved_points details] {
+ set qn [dict get $pd attributeName]
+ dict set results $qn achieved [dict get $pd achieved]
+ dict set results $qn achievable [dict get $pd achievable]
+ dict set results $qn question_id [dict get $pd question_id]
+ }
+
+ set question_form [:postprocess_question_html \
+ -question_form $question_form \
+ -achieved_points $achieved_points \
+ -manual_grading [:dict_value $manual_gradings $user_id] \
+ -submission $submission \
+ -exam_state [$examWf state] \
+ -runtime_panel_view $runtime_panel_view \
+ -feedbackFiles [$submission set online-exam-feedbackFiles]]
+
+ if {$with_signature} {
+ set sha256 [ns_md string -digest sha256 $answeredAnswerAttributes]
+ set signatureString "
online-exam-actual_signature: $sha256
\n"
+ set submissionSignature [$submission property signature ""]
+ if {$submissionSignature ne ""} {
+ append signatureString "
\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_submission_id ne ""} {
+ set runtime_panel_view "revision_overview"
+ } else {
+ set runtime_panel_view "default"
+ }
+ }
+ append HTML [:grading_dialog_setup $examWf]
+ #ns_log notice "RENDER ANSWERS 1"
+
+ 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 [:recutil_create \
+ -clear \
+ -exam_id [$wf parent_id] \
+ -fn [expr {$filter_submission_id eq "" ? "all.rec" : "$filter_submission_id.rec"}]
+ ]
+ } else {
+ set recutil ""
+ }
+ #ns_log notice "RENDER ANSWERS 2"
+
+ #
+ # Create zip file from file submissions
+ #
+ set create_zip_file [::xo::cc query_parameter create-file-submission-zip-file:boolean 0]
+ if {$create_zip_file} {
+ package req nx::zip
+
+ [$examWf package_id] get_lang_and_name -name [$examWf set name] lang stripped_name
+
+ if {[string equal [nx::zip::Archive info lookup parameters create name] -name]} {
+ set zipFile [nx::zip::Archive new -name [ad_sanitize_filename $stripped_name]]
+ } else {
+ set zipFile [::nx::zip::Archive new]
+ #
+ # Post-register property, since it is not yet available in
+ # this version of nx.
+ #
+ $zipFile object property name
+ $zipFile configure -name [ad_sanitize_filename $stripped_name]
+ }
+ } else {
+ set zipFile ""
+ }
+ #ns_log notice "RENDER ANSWERS 3 (submissions: [llength [$items children]])"
+
+ set file_submission_exists 0
+
+ set form_objs_exam [:QM load_question_objs $examWf [$examWf property question]]
+ set question_dict [:FL name_to_question_obj_dict $form_objs_exam]
+ #ns_log notice "passed filter_form_ids <$filter_form_ids> form_objs_exam <$form_objs_exam>"
+
+ #
+ # Iterate over the items sorted by orderby.
+ #
+ $items orderby $orderby
+ foreach submission [$items children] {
+
+ set d [:render_submission=exam_protocol \
+ -submission $submission \
+ -wf $wf \
+ -examWf $examWf \
+ -exam_question_dict $question_dict \
+ -autograde $autograde \
+ -combined_form_info $combined_form_info \
+ -filter_submission_id $filter_submission_id \
+ -filter_form_ids $filter_form_ids \
+ -grading_scheme $grading_scheme \
+ -recutil $recutil \
+ -zipFile $zipFile \
+ -revision_id $revision_id \
+ -totalPoints $totalPoints \
+ -runtime_panel_view $runtime_panel_view \
+ -with_exam_heading [expr {!$as_student}] \
+ -with_signature $withSignature]
+
+ set html [:dict_value $d HTML]
+ #ns_log notice "RENDER ANSWERS setting result"
+
+ dict set results [$submission set creation_user] [:dict_value $d results]
+
+ if {$do_stream && $html ne ""} {
+ ns_write [lang::util::localize $html]
+ } else {
+ append HTML $html
+ }
+
+ #
+ # Check if we have found a file submission
+ #
+ if {!$file_submission_exists
+ && !$export
+ && [llength [:get_non_empty_file_formfields -submission $submission]] > 0
+ } {
+ set file_submission_exists 1
+ }
+
+ }
+ #ns_log notice "RENDER ANSWERS 4"
+
+ if {$export} {
+ $recutil destroy
+ }
+
+ if {$with_grading_table && $autograde && $grading ne "none"} {
+ append HTML