%3 ::xowf::test_item::Answer_manager ::xowf::test_item::Answer_manager achieved_points allow_answering answer_form_field_objs answers_panel countdown_timer create_workflow delete_all_answer_data delete_scheduled_atjobs dom class add dom class remove dom node appendXML dom node delete dom node replace dom node replaceXML exam_results export_answer export_file_submission get_IPs get_answer_attributes get_answer_wf get_duration get_exam_results get_label_from_options get_non_empty_file_formfields get_wf_instances grading_dialog_setup grading_scheme grading_table last_time_in_state last_time_switched_to_state marked_results participant_result participants_table postprocess_question_html pretty_formfield_name pretty_period prevent_multiple_tabs recutil_create render_answers render_answers_with_edit_history render_filter_bar render_full_submission_form render_print_button render_proctor_images render_submission=edit_history render_submission=exam_protocol render_submissions=edit_history result_table per_participant result_table per_question results_table revisions_up_to runtime_panel set_exam_results state_periods student_submissions_exist submissions time_window_setup waiting_room_message ::xowf::test_item::AssessmentInterface ::xowf::test_item::AssessmentInterface add_to_fc assert_answer_instance assert_assessment assert_assessment_container export_links list_equal render_feedback_files replace_in_fc ::xowf::test_item::Answer_manager->::xowf::test_item::AssessmentInterface ::xowf::test_item::Question_manager ::xowf::test_item::Question_manager add_seeds aggregated_form combined_question_form current_question_form current_question_name current_question_number current_question_obj current_question_title describe_form disable_text_field_feature disallow_paste disallow_spellcheck disallow_translation exam_base_time exam_configuration_block exam_configuration_modifiable_field_names exam_configuration_popup exam_configuration_render_fields exam_info_block exam_target_time get_pool_questions get_pool_replacement_candidates goto_page hint_box hint_boxes initialize item_substitute_markup load_question_objs max_items minutes_string more_ahead nth_question_form nth_question_obj pagination_actions pagination_button_css_class percent_substitute percent_substitute_in_form points_string pretty_ncorrect pretty_nr_alternatives pretty_shuffle qualified_question_names question_count question_info question_info_block question_is_autograded question_names question_objs question_overview_block question_property question_randomization_ok question_statistics_block question_summary questions_without_minutes render_describe_infos replace_pool_question replace_pool_questions shuffled_index title_infos total total_minutes total_minutes_for_exam total_points ::xowf::test_item::Question_manager->::xowf::test_item::AssessmentInterface ::xowf::test_item::Renaming_form_loader ::xowf::test_item::Renaming_form_loader answer_attributes answer_for_form answers_for_form form_name_based_attribute_stem map_form_constraints name_to_question_obj_dict rename_attributes ::xowf::test_item::Renaming_form_loader->::xowf::test_item::AssessmentInterface ::nx::Object ::nx::Object __default_accessor __default_method_call_protection __object_configureparameter __resolve_method_path contains copy delete object method delete object property delete object variable destroy_on_cleanup info info info lookup parameters info lookup slots info lookup syntax info lookup variables info object method args info object method body info object method callprotection info object method debug info object method definition info object method definitionhandle info object method deprecated info object method exists info object method handle info object method origin info object method parameters info object method registrationhandle info object method returns info object method submethods info object method syntax info object method type info object slots info object variables info variable definition info variable name info variable parameter move object alias object forward object method object property object variable private protected public qn require namespace require object method require private object method require protected object method require public object method serialize ::xowf::test_item::AssessmentInterface->::nx::Object

Class ::xowf::test_item::Question_manager

::xowf::test_item::Question_manager[i] create ...

This code manages questions and the information related to a current (selected) question via qthe "position" instance attribute. It provides the following public API: - goto_page - more_ahead - pagination_actions - current_question_form - current_question_obj - current_question_name - current_question_title - nth_question_obj - nth_question_form - exam_configuration_popup - exam_configuration_modifiable_field_names - combined_question_form - question_objs - question_names - question_count - question_property - add_seeds - total_minutes - total_points - questions_without_minutes - total_minutes_for_exam - exam_target_time - exam_base_time - percent_substitute_in_form - item_substitute_markup - describe_form - question_summary - question_info_block - question_statistics_block
Defined in packages/xowf/tcl/test-item-procs.tcl

Class Relations

  • class: ::nx::Class[i]
  • superclass: ::xowf::test_item::AssessmentInterface[i]
::nx::Class create ::xowf::test_item::Question_manager \
     -superclass ::xowf::test_item::AssessmentInterface

Methods (to be applied on instances)

  • add_seeds (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> add_seeds \
        [ -obj obj ] [ -seed seed ] [ -number number ]

    Add property "seed" to the provided object, consisting of a list of the specified number of random values starting with a base seed. This can be used to use e.g. per user different random seeds depending on the position of an item.

    Switches:
    -obj (optional, object)
    -seed (optional, integer)
    -number (optional, integer)

    Testcases:
    No testcase defined.
    expr {srand($seed * [clock microseconds])}
    set seeds {}
    for {set i 0} {$i < $number} {incr i} {
      lappend seeds [expr {int(rand() * $seed * [clock microseconds])}]
    }
    $obj set_property -new 1 seeds $seeds
  • aggregated_form (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> aggregated_form \
        [ -titleless_form ] [ -with_feedback ] [ -with_correction_notes ] \
        [ -with_grading_box with_grading_box ] question_infos

    Compute an aggregated form (containing potentially multiple questions) based on the chunks available in question_infos.

    Switches:
    -titleless_form (optional, defaults to "false")
    -with_feedback (optional, defaults to "false")
    -with_correction_notes (optional, defaults to "false")
    -with_grading_box (optional)
    might be: "hidden" (but included), "true", "" (omitted)
    Parameters:
    question_infos (required)
    Returns:
    HTML form content

    Testcases:
    No testcase defined.
    set full_form ""
    set count 0
    foreach  question_form [dict get $question_infos question_forms]  title_info [dict get $question_infos title_infos]  question_obj [dict get $question_infos question_objs] {
          set item_type [$question_obj property item_type]
          append full_form  "<div class='test-item' data-item_type='$item_type'>"
    
          if {!$titleless_form} {
            append full_form  "<h4>[dict get $title_info full_title]</h4>\n"
          }
          if {$with_grading_box ne ""} {
            set question_name [:FL form_name_based_attribute_stem [$question_obj name]]
            set hiddenCSSclass [expr {$with_grading_box eq "hidden" ? [::template::CSS class d-none] : ""}]
            if {$with_grading_box eq "hidden"} {
              set question_name answer_$question_name
            }
            set data_attribute [expr {[::template::CSS toolkit] eq "bootstrap5" ? "data-bs" : "data"}]
            append full_form [subst [ns_trim -delimiter | {
              |<div id='grading-box-[incr count]' class='grading-box $hiddenCSSclass'
              |     data-question_name='$question_name' data-title='[$question_obj title]'
              |     data-question_id='[$question_obj item_id]'>
              |  #xowf.Points#: <span class='points'></span>
              |  <span class='percentage'></span>
              |  <span class='feedback-label'>#xowf.feedback#: </span><span class='comment'></span>
              |  <a class='manual-grade' href='#' $data_attribute-toggle='modal'
              |    $data_attribute-target='#grading-modal'>
              |    <span class='manual-grade-edit'>[::xowiki::bootstrap::icon -name pencil]</span>
              |  </a>
              | <div class="thumbnail-files-wrapper"></div>
              |</div>
            }]]
          }
          append full_form  $question_form  [:hint_boxes  -question_obj $question_obj  -with_feedback=$with_feedback  -with_correction_notes=$with_correction_notes]  </div>\n
        }
    
    regsub -all {<[/]?form>} $full_form "" full_form
    #ns_log notice "aggregated_form: STRIP FORM xxx times from full_form"
    return $full_form
  • combined_question_form (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> combined_question_form \
        [ -with_numbers ] [ -with_title ] [ -with_minutes ] \
        [ -with_points ] [ -user_specific ] [ -shuffle_id shuffle_id ] \
        [ -user_answers user_answers ] [ -form_objs form_objs ] obj

    For the provided assessment, return a combined question_form as a single (combined) form, containing the content of all question forms. The result is a dict, containing also title information etc. depending on the provided parameters.

    Switches:
    -with_numbers (optional, defaults to "false")
    -with_title (optional, defaults to "false")
    -with_minutes (optional, defaults to "false")
    -with_points (optional, defaults to "false")
    -user_specific (optional, defaults to "false")
    -shuffle_id (optional, integer, defaults to "-1")
    used only for selecting form_objs
    -user_answers (optional, object, accept empty)
    instance of the answer-wf. Needed for user-specific percent substitutions.
    -form_objs (optional, object, accept empty)
    Parameters:
    obj (required, object)
    is the exam

    Testcases:
    No testcase defined.
    #ns_log notice "combined_question_form called with user_answers <$user_answers> for $obj [$obj name]"
    #if {$user_answers eq ""} {xo::show_stack}
    
    set all_form_objs [:question_objs -shuffle_id $shuffle_id $obj]
    set positions {}
    if {[llength $form_objs] > 0} {
      foreach form_obj $form_objs {
        lappend positions [lsearch $all_form_objs $form_obj]
      }
    }
    #ns_log notice "XXX combined_question_form fos=$form_objs all_form_objs=$all_form_objs <$positions>"
    
    if {$user_specific} {
      set form_objs [:max_items [$obj property max_items ""$form_objs]
    }
    if {$with_numbers} {
      set numbers ""
      for {set i 1} {$i <= [llength $all_form_objs]} {incr i} {
        lappend numbers $i
      }
      if {[llength $form_objs] > 0} {
        set new_numbers {}
        set new_form_objs {}
        foreach form_obj $all_form_objs number $numbers {
          if {$form_obj in $form_objs} {
            lappend new_numbers $number
            lappend new_form_objs $form_obj
          }
        }
        set numbers $new_numbers
        set form_objs $new_form_objs
      } else {
        set form_objs $all_form_objs
      }
      set extra_flags [list -numbers $numbers]
    } else {
      set form_objs $all_form_objs
      set extra_flags ""
    }
    return [:question_info  -with_title=$with_title  -with_minutes=$with_minutes  -with_points=$with_points  {*}$extra_flags  -obj $obj  -user_answers $user_answers  -positions $positions  $form_objs]
  • current_question_form (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> current_question_form \
        [ -with_numbers ] [ -with_title ] obj

    Return the current form object of the provided assessment.

    Switches:
    -with_numbers (optional, defaults to "false")
    -with_title (optional, defaults to "false")
    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    return [:nth_question_form -with_numbers=$with_numbers -with_title=$with_title $obj]
  • current_question_number (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> current_question_number \
        obj

    Translate the position of an object into its question number (as e.g. used by current_question_title).

    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    return [expr {[$obj property position] + 1}]
  • current_question_obj (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> current_question_obj \
        obj

    Load the current question obj based on the current question name.

    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    return [:load_question_objs $obj [:current_question_name $obj]]
  • current_question_title (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> current_question_title \
        [ -with_numbers ] obj

    In case, with_numbers is provided, return a internationalized title for the question, such as "Question 1".

    Switches:
    -with_numbers (optional, defaults to "false")
    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    if {$with_numbers} {
      return "#xowf.question# [:current_question_number $obj]"
    }
  • describe_form (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> describe_form \
        [ -asHTML ] [ -field_name field_name ] form_obj

    Call for every form field of the form_obj the "describe" method and return these infos in a form of a list.

    Switches:
    -asHTML (optional)
    -field_name (optional)
    Parameters:
    form_obj (required)

    Testcases:
    No testcase defined.
    set describe_infos {}
    
    if {[$form_obj property item_type] eq "Composite"} {
      #
      # In the case of a composite Composite question type, describe
      # the components rather than the compound part (maybe, we
      # should describe in the future also the container, but this
      # actually less interesting).
      #
      set selection [dict get [$form_obj instance_attributes]  question question.interaction question.interaction.selection]
      set form_objs [[$form_obj package_id] instantiate_forms  -forms [join [split $selection \n] |]  -default_lang en]
    
      set describe_infos [join [lmap form_obj_tmp $form_objs {
                                  set describe_info [join [:describe_form -field_name $field_name $form_obj_tmp]]
                                  list [lappend describe_info is_composite_subquestion 1]
                              }]]
      set fc {selection:form_page}
    } else {
      set fc [$form_obj property form_constraints]
    }
    
    #
    # We might be willing in the future to get the full set of all
    # options, i.e. remove "show_max" constraints etc.
    #
    #ns_log notice DESCRIBE-BEFORE--$fc
    #set fc [:replace_in_fc -fc $fc shuffle_kind none]
    #set fc [:replace_in_fc -fc $fc show_max ""]
    #ns_log notice DESCRIBE-changed
    
    set form_fields [$form_obj create_form_fields_from_form_constraints  -lookup $fc]
    set ff_describe_infos [lmap form_field $form_fields {
      $form_field describe -field_name $field_name
    }]
    
    #ns_log notice "describe_form [$form_obj name]: $question_infos"
    set describe_infos [:pretty_nr_alternatives "$ff_describe_infos $describe_infos"]
    if {!$asHTML} {
      #ns_log notice "OOO [$form_obj name] early exit $describe_infos"
      return $describe_infos
    } else {
      set HTML [:render_describe_infos $describe_infos]
      return $HTML
    }
  • disallow_paste (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> disallow_paste \
        form_obj

    This function changes the form_constraints of the provided form object by adding "paste=false" properties to textarea or text_fields entries.

    Parameters:
    form_obj (required, object)

    Testcases:
    No testcase defined.
    :disable_text_field_feature $form_obj paste
  • disallow_spellcheck (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> disallow_spellcheck \
        form_obj

    This function changes the form_constraints of the provided form object by adding "spellcheck=false" properties to textarea or text_fields entries.

    Parameters:
    form_obj (required, object)

    Testcases:
    No testcase defined.
    :disable_text_field_feature $form_obj spellcheck
  • disallow_translation (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> disallow_translation \
        form_obj

    This function disallows translation of the full page by setting the HTML5 "translate" attribute of the body to "no".

    Parameters:
    form_obj (required, object)

    Testcases:
    No testcase defined.
    ::xo::Page set_property body translate no
  • exam_base_time (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> exam_base_time \
        [ -manager manager ] [ -answer_obj answer_obj ]

    Calculate the exam base time for a student. This is the time reference, when the timer starts. Depending on whether the exam is synchronous, the time start is either the time when the exam is opened, or when the student starts the exam.

    Switches:
    -manager (optional, object)
    -answer_obj (optional, object)
    Returns:
    time string as returned from the database

    Testcases:
    No testcase defined.
    if {[$manager property synchronized 0]} {
      set parent_obj [::xowiki::FormPage get_instance_from_db -item_id [$answer_obj parent_id]]
      set base_time [$parent_obj last_modified]
    } else {
      set base_time [$answer_obj creation_date]
    }
    return $base_time
  • exam_configuration_modifiable_field_names (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> exam_configuration_modifiable_field_names \
        obj

    Return the names of the modifiable field names in the current state. The state is in essence defined on whether or not students have started to work on this exam. This method can be used to correct small things, even when the students are already working on the exam.

    Parameters:
    obj (required)

    Testcases:
    No testcase defined.
    set modifiable {
      allow_paste allow_spellcheck allow_translation
      show_minutes show_points show_ip
      countdown_audio_alarm grading
    }
    set wf [:AM get_answer_wf $obj]
    if {![:AM student_submissions_exist $wf]} {
      lappend modifiable {*}{
        shuffle_items max_items
        time_budget synchronized time_window
        proctoring proctoring_options proctoring_record signature show_pagination_actions
      }
    }
    return $modifiable
  • exam_configuration_popup (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> exam_configuration_popup \
        obj

    Render the exam configuration popup, add it as a content_header (to avoid putting it to the main workflow form, since nested FORMS are not allowed) and return the rendering of the button for popping-ip the configuration modal.

    Parameters:
    obj (required)
    Returns:
    HTML

    Testcases:
    No testcase defined.
    set modifiable [:exam_configuration_modifiable_field_names $obj]
    #ns_log notice "exam_configuration_popup modifiable '$modifiable'"
    
    set fcrepo [$obj get_fc_repository]
    set content ""
    append content  [:exam_configuration_block  -modifiable $modifiable  -label #xowf.Question_management#  -id config-question  -form_constraints $fcrepo  -obj $obj {
               shuffle_items max_items
               allow_paste allow_spellcheck allow_translation
               show_minutes show_points show_ip
             }]  [:exam_configuration_block  -modifiable $modifiable  -label #xowf.Time_management#  -id config-time  -form_constraints $fcrepo  -obj $obj {
               time_budget synchronized time_window countdown_audio_alarm
             }]  [:exam_configuration_block  -modifiable $modifiable  -label #xowf.Security#  -id config-security  -form_constraints $fcrepo  -obj $obj {
               proctoring proctoring_options proctoring_record signature iprange
             }]  [:exam_configuration_render_fields -modifiable $modifiable  [$obj create_form_fields_from_names -lookup -set_values  -form_constraints $fcrepo  {grading}]]
    
    ::template::add_body_script -script [ns_trim -delimiter | [subst -novariables {
      |$(document).ready(function() {
      |  $('.modal .confirm').on('click', function(ev) {
      |    //
      |    // Submit button of the configuration dialog was pressed.
      |    //
      |    var data = new FormData(document.getElementById('configuration-form'));
      |    console.log(data);
      |    var xhttp = new XMLHttpRequest();
      |    xhttp.open('POST', '[$obj pretty_link -query m=update-config]', true);
      |    xhttp.onload = function () {
      |      if (this.readyState == 4) {
      |        if (this.status == 200) {
      |          var text = this.responseText;
      |          console.log('sent OK ok ' + text);
      |          //window.location.reload(true);
      |        } else {
      |          console.log('sent NOT ok');
      |        }
      |      }
      |    };
      |    xhttp.send(data);
      |  });
      |});
    }]]
    
    
    $obj content_header_append  [::xowiki::bootstrap::modal_dialog  -id configuration-modal  -title "#xowf.Configuration#: <span id='configuration-participant'></span>"  -body $content]
    
    return  [::xowiki::bootstrap::modal_dialog_popup_button  -target configuration-modal  -label [::xowiki::bootstrap::icon -name cog -style "float: right;"]  -title #xowf.Configuration_button_title#  -CSSclass configuration-button]
  • exam_info_block (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> exam_info_block \
        [ -combined_form_info combined_form_info ] obj

    Provide a summative overview of an exam.

    Switches:
    -combined_form_info (optional)
    Parameters:
    obj (required)

    Testcases:
    No testcase defined.
    if {![info exists combined_form_info]} {
      set combined_form_info [:combined_question_form -with_numbers $obj]
    }
    set proctoring   [$obj property proctoring 0]
    set synchronized [$obj property synchronized 0]
    set allow_paste  [$obj property allow_paste 1]
    set max_items    [$obj property max_items ""]
    set time_window  [$obj property time_window ""]
    set allow_spellcheck [$obj property allow_spellcheck true]
    set allow_translation [$obj property allow_translation false]
    
    append text [subst {<p>
      [expr {$synchronized ? "" : "Non-"}]Synchronized Exam
      [expr {$proctoring ? " with Proctoring" : ""}]
      </p>}]
    set question_objs     [dict get $combined_form_info question_objs]
    set nrQuestions       [llength [:question_names $obj]]
    set randomizationOk   [dict get $combined_form_info randomization_for_exam]
    set autograde         [dict get $combined_form_info autograde]
    
    set revision_sets     [$obj get_revision_sets]
    set published_periods [:AM state_periods $revision_sets -state published]
    set review_periods    [:AM state_periods $revision_sets -state submission_review]
    set total_minutes     [:total_minutes -max_items $max_items $combined_form_info]
    set total_points      [:total_points -max_items $max_items $combined_form_info]
    set questions_without_minutes [:questions_without_minutes -max_items $max_items $combined_form_info]
    set max_items_msg     ""
    
    if {$max_items ne ""} {
      set all_minutes [lmap t [dict get $combined_form_info title_infos] {
        dict get $t minutes
      }]
      if {[llength [lsort -unique $all_minutes]] != 1} {
        set max_items_msg [_ xowf.Max_items_not_ok_duration [list n $max_items]]
      } elseif {$max_items > [llength $all_minutes]} {
        set max_items_msg [_ xowf.Max_items_not_ok_number [list n $max_items]]
      } else {
        set max_items_msg [_ xowf.Max_items_ok [list n $max_items]]
      }
    }
    
    set time_window_msg ""
    if {$time_window ne ""} {
      set dtstart [dict get $time_window time_window.dtstart]
      if {$dtstart ne ""} {
        regsub -all T $dtstart " " dtstart
        set dtend [dict get $time_window time_window.dtend]
        set time_window_msg <br>[_ xowf.Automatically_published_from_to [list from $dtstart to $dtend]]
        set time_window_msg "<br>Automatische Freischaltung der Prüfung von $dtstart bis $dtend"
      }
    }
    set question_hint_html ""
    if {$questions_without_minutes > 0} {
      append question_hint_html  " ($questions_without_minutes #xowf.without_minutes#)"
    }
    
    append text [subst {
      <p>
      [expr {$max_items_msg ne "" ? "$max_items_msg" : ""}]
      $nrQuestions [expr {$nrQuestions == 1 ? "#xowf.question#" : "#xowf.questions#"}]$question_hint_html,
      $total_minutes #xowf.Minutes#, $total_points #xowf.Points#<br>
      [expr {$total_minutes <= 1 ? "#xowf.Countdown_timer_is_not_displayed#<br>" : ""}]
      [expr {$autograde ? "#xowf.exam_review_possible#" : "#xowf.exam_review_not_possible#"}]<br>
      [expr {$randomizationOk ? "#xowf.randomization_for_exam_ok#" : "#xowf.randomization_for_exam_not_ok#"}]<br>
      [expr {$allow_paste ? "#xowf.Cut_and_paste_allowed#" : "#xowf.Cut_and_paste_not_allowed#"}]<br>
      [expr {$allow_spellcheck ? "#xowf.Spellcheck_allowed#" : "#xowf.Spellcheck_not_allowed#"}]<br>
      [expr {$allow_translation ? "#xowf.Translation_allowed#" : "#xowf.Translation_not_allowed#"}]<br>
      $time_window_msg
      [expr {[llength $published_periods] > 0 ? "<br>#xowf.inclass-exam-open#: [join $published_periods {, }]<br>" : ""}]
      [expr {[llength $review_periods] > 0 ? "#xowf.inclass-exam-review#: [join $review_periods {, }]<br>" : ""}]
      </p>
    }]
    return "<div class='exam-info-block'>$text</div>"
  • exam_target_time (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> exam_target_time \
        [ -manager manager ] [ -base_time base_time ]

    Calculate the exam target time (finishing time) based on the duration of the exam plus the provided base_time (which is in the format returned by SQL)

    Switches:
    -manager (optional, object)
    exam workflow
    -base_time (optional)
    time in SQL format

    Testcases:
    No testcase defined.
    set total_minutes [:total_minutes_for_exam -manager $manager]
    
    # Use "try" for backward compatibility, versions before
    # factional seconds. TODO: remove me.
    try {
      set base_clock [clock scan [::xo::db::tcl_date $base_time tz secfrac]]
      if {[string length $secfrac] > 3} {
        set secfrac [string range $secfrac 0 2]
      }
    } on error {errorMsg} {
      set base_clock [clock scan [::xo::db::tcl_date $base_time tz]]
      set secfrac 0
    }
    set target_time [clock format [expr {int($base_clock + $total_minutes * 60)}]  -format %Y-%m-%dT%H:%M:%S]
    #ns_log notice "exam_target_time $base_time base clock $base_clock + total_minutes $total_minutes = ${target_time}.$secfrac"
    return ${target_time}.$secfrac
  • get_pool_questions (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> get_pool_questions \
        [ -allowed_forms allowed_forms ] [ -field_name field_name ] \
        pool_question_obj exam_question_names

    Obtain for the specs in the pool_question_obj potential replacement items in form of a replacement dict. For raw forms (i.e., not obtained via the renaming form-loader), we have just the plain "answer", which can be provided via the "field_name" attribute.

    Switches:
    -allowed_forms (optional, defaults to "en:edit-interaction.wf")
    -field_name (optional)
    Parameters:
    pool_question_obj (required)
    exam_question_names (required)

    Testcases:
    No testcase defined.
    set query_dict [:fc_to_dict [$pool_question_obj property form_constraints]]
    if {$field_name eq ""} {
      #
      # No field name was provided, so get the field name from the
      # question obj.
      #
      set field_name [:FL form_name_based_attribute_stem [$pool_question_obj name]]
      if {![dict exists $query_dict $field_name]} {
        #
        # Fall back to field_name "answer". This will be necessary,
        # when called with question_objs not adapted by the renaming
        # form-loader.
        #
        if {[dict exists $query_dict answer]} {
          ns_log notice "get_pool_questions: fallback from field_name '$field_name' to 'answer'"
          set field_name answer
        }
      }
    } elseif {![dict exists $query_dict $field_name]} {
      ns_log warning "QM get_pool_questions: the provided field name '$field_name'"  "is not defined in the form_constraints, fall back to '[lindex [dict keys $query_dict] 0]'"
      set field_name [lindex [dict keys $query_dict] 0]
    }
    set question_attributes [dict get [$pool_question_obj instance_attributes] question]
    set minutes [dict get $question_attributes question.minutes]
    set points [dict get $question_attributes question.points]
    
    set fc_dict [dict get $query_dict $field_name]
    set lang [string range [$pool_question_obj nls_language] 0 1]
    
    append key test-item-replacement-cands  - $minutes - $points - $lang - $fc_dict - [$pool_question_obj revision_id]
    ns_log notice "get_pool_questions fetch via key: '$key'"
    
    return [ns_cache_eval -expires 1m -- ns:memoize $key {
      :get_pool_replacement_candidates  -minutes $minutes  -points $points  -fc_dict $fc_dict  -lang $lang  $pool_question_obj
    }]
  • goto_page (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> goto_page obj \
        position

    ns_log notice "===== goto_page $position" Set the position (test item number) of the workflow (exam). This sets the question number shown to the user.

    Parameters:
    obj (required, object)
    position (required)

    Testcases:
    No testcase defined.
    $obj set_property position $position
  • hint_boxes (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> hint_boxes \
        [ -question_obj question_obj ] [ -with_feedback ] \
        [ -with_correction_notes ]

    Render the hint boxes (feedback and correction notes) for a question object.

    Switches:
    -question_obj (optional, object)
    -with_feedback (optional)
    -with_correction_notes (optional)
    Returns:
    HTML

    Testcases:
    No testcase defined.
    set HTML ""
    if {$with_feedback} {
      set question_data [$question_obj property question]
      foreach feedback {feedback_correct feedback_incorrect} {
        regsub -all _ $feedback - feedback_class
        append HTML [:hint_box  -title #xowf.General_feedback#  -body [:dict_value $question_data question.$feedback ""]  -CSSclass $feedback_class]
      }
    }
    if {$with_correction_notes} {
      append HTML [:hint_box  -title #xowf.Correction_notes#  -body [:dict_value $question_data question.correction_notes ""]  -CSSclass correction-notes]
    }
    return $HTML
  • initialize (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> initialize \
        [ -wfi wfi ]

    Initialize the question manager for a certain workflow instance. This is needed for per-answer-workflow questions (as for pool questions, where different questions are taken for different users).

    Switches:
    -wfi (optional, object)

    Testcases:
    No testcase defined.
    #ns_log notice "QM initialize wfi $wfi"
    set isAnswerInstance [expr {[$wfi is_wf_instance] == 1 && [$wfi is_wf] == 0}]
    if {$isAnswerInstance} {
      #ns_log notice "QM initialize answer instance [$wfi name] // [$wfi instance_attributes]"
      set :wfi $wfi
    } else {
      ns_log warning "initializing question manager for not an answer instance [$wfi name]"  "// [$wfi instance_attributes]"
    }
  • item_substitute_markup (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> item_substitute_markup \
        [ -obj obj ] [ -form_obj form_obj ] [ -position position ] \
        [ -do_substitutions ]

    Substitute everything item-specific in the text, including markup (handling e.g. images resolving in the context of the original question) and also percent-substitutions (if desired). ns_log notice "=== item_substitute_markup [$form_obj name] do percent subst (have pos [info exists position])"

    Switches:
    -obj (optional, object)
    -form_obj (optional, object)
    -position (optional, integer)
    -do_substitutions (optional, defaults to "1")

    Testcases:
    No testcase defined.
    :assert_answer_instance $obj
    
    $obj do_substitutions $do_substitutions
    set html [$obj substitute_markup  -context_obj $form_obj  [$form_obj property form]]
    #ns_log notice "after subst [$obj serialize]\nhtml"
    if {[info exists position]} {
      return [:percent_substitute_in_form  -obj $obj  -form_obj $form_obj  -position $position  $html]
    } else {
      set fc [$form_obj property form_constraints]
      set dfc [$form_obj property disabled_form_constraints]
      return [list form $html form_constraints $fc disabled_form_constraints $dfc]
    }
  • load_question_objs (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> load_question_objs \
        obj names

    Load the question objects for the provided question names and return the question objs.

    Parameters:
    obj (required, object)
    names (required)

    Testcases:
    No testcase defined.
    set parent_id [$obj parent_id]
    #
    # Make sure to have names pointing to a folder.
    # In case, '$ref' refers to a site-wide page, a prefix with
    # the parent name would not help. In these cases, we expect
    # to have the parent obj not instantiated.
    #
    if {[nsf::is object ::$parent_id]} {
      set names [lmap ref $names {
        if {![string match "*/*" $ref]} {
          set ref [::$parent_id name]/$ref
        }
        set ref
      }]
    }
    #ns_log notice "XXX [$obj name] load_question_objs names = <$names>"
    #xo::show_stack
    set questionNames [join $names |]
    set questionForms [::[$obj package_id] instantiate_forms  -default_lang [$obj lang]  -forms $questionNames]
    
    if {[llength $questionForms] < [llength $names]} {
      if {[llength $names] == 1} {
        ns_log warning "load_question_objs: question '$names' could not be loaded"
      } else {
        set loaded [llength $questionForms]
        set out_of [llength $names]
        ns_log warning "load_question_objs: only $loaded out of $out_of from '$names' could be loaded"
      }
    }
    return $questionForms
  • minutes_string (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> minutes_string \
        form_obj

    Get an attribute of the original question

    Parameters:
    form_obj (required, object)

    Testcases:
    No testcase defined.
    set minutes [:question_property $form_obj minutes]
    if {$minutes ne ""} {
      set pretty_label [expr {$minutes eq "1" ? [_ xowf.Minute] : [_ xowf.Minutes]}]
      set minutes "($minutes $pretty_label)"
    }
  • more_ahead (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> more_ahead \
        [ -position position ] obj

    Return true, when this is for the current user not the last question.

    Switches:
    -position (optional)
    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    if {$position eq ""} {
      set position [$obj property position]
    }
    set questions [dict get [$obj instance_attributes] question]
    return [expr {$position + 1 < [:question_count $obj]}]
  • nth_question_form (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> nth_question_form \
        [ -position position ] [ -item_nr item_nr ] [ -with_numbers ] \
        [ -with_title ] [ -titleless_form ] [ -with_minutes ] obj

    Return the question_info of the nth form (question) of the assessment. The information added to the title can be optionally included as expressed by the non-positional parameters.

    Switches:
    -position (optional, integer)
    -item_nr (optional, integer)
    -with_numbers (optional, defaults to "false")
    -with_title (optional, defaults to "false")
    -titleless_form (optional, defaults to "false")
    -with_minutes (optional, defaults to "false")
    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    if {![info exists position]} {
      set position [$obj property position]
    }
    if {![info exists item_nr]} {
      set item_nr $position
    }
    set form_objs [:nth_question_obj $obj $position]
    if {$with_numbers} {
      set number [expr {$item_nr + 1}]
      set extra_flags [list -numbers $number]
    } else {
      set extra_flags ""
    }
    return [:question_info  -with_title=$with_title  -titleless_form=$titleless_form  -with_minutes=$with_minutes  -with_question_count_in_title true  {*}$extra_flags  -obj $obj  $form_objs]
  • nth_question_obj (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> nth_question_obj \
        obj position

    Return the nth question object of an assessment (based on position).

    Parameters:
    obj (required, object)
    position (required, integer)

    Testcases:
    No testcase defined.
    :assert_assessment $obj
    set questions [:question_names $obj]
    set result [:load_question_objs $obj [lindex $questions $position]]
    return $result
  • pagination_actions (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> pagination_actions \
        [ -container container ] [ -question_count question_count ] \
        [ -visited visited ] [ -flagged flagged ] \
        [ -current_position current_position ] [ -CSSclass CSSclass ]

    Create actions used for pagination.

    Switches:
    -container (optional, object)
    -question_count (optional, integer)
    -visited (optional, integer)
    -flagged (optional, integer)
    -current_position (optional, integer)
    -CSSclass (optional, defaults to "btn-sm")

    Testcases:
    No testcase defined.
    set actions ""
    
    if {$question_count > 1} {
      if {[[${:wfi} get_parent_object] property show_pagination_actions t]} {
        set extra_css [:pagination_button_css_class  -CSSclass $CSSclass  -cond [expr {$current_position == 0}]  -extra "disabled"]
        ${container}::previousQuestion configure  -extra_css_class $extra_css  -label "<small><adp:icon name='previous'> #acs-kernel.common_Previous#</small>"  -label_noquote true  -wrapper_CSSclass "pagination"
        lappend actions previousQuestion
    
        for {set count 1} {$count <= $question_count} {incr count} {
          set visited_css [expr {($count - 1) in $visited ? "visited" : ""}]
          set flag_label [expr {($count - 1) in $flagged
                                ? " [::xowiki::bootstrap::icon -name flag -CSSclass text-danger]"
                                : ""}]
          set extra_css [:pagination_button_css_class  -CSSclass "$CSSclass $visited_css"  -cond [expr {$current_position == $count - 1 }]  -extra "active current"]
          ${container}::Action create ${container}::q.$count  -label "$count$flag_label"  -label_noquote true  -state_safe true  -next_state working  -wrapper_CSSclass "pagination"  -extra_css_class $extra_css  -proc activate {obj} [subst {
                #ns_log notice "===== NAVIGATE next"
                next
                #ns_log notice "===== NAVIGATE goto [expr {$count - 1}]"
                :goto_page [expr {$count - 1}]
              }]
          lappend actions q.$count
        }
      }
      set extra_css [:pagination_button_css_class  -CSSclass $CSSclass  -cond [expr {$current_position+2 > $question_count}]  -extra "disabled"]
      ${container}::nextQuestion configure  -extra_css_class $extra_css  -label "<small>#acs-kernel.common_Next# <adp:icon name='next'></small>"  -label_noquote true  -wrapper_CSSclass "pagination"
    
      set flag_state [expr {$current_position in $flagged ? "delete" : "set"}]
      ${container}::flag configure  -label "#xowf.flag_${flag_state}#"  -title "#xowf.flag_${flag_state}_title#"
    
      lappend actions nextQuestion
    }
    return $actions
  • percent_substitute_in_form (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> percent_substitute_in_form \
        [ -obj obj ] [ -form_obj form_obj ] [ -position position ] html

    Perform percent substitution in the provided HTML, form_constraints and disabled_form_constraints and return the result as a dict.

    Switches:
    -obj (optional, object)
    -form_obj (optional, object)
    -position (optional, integer)
    Parameters:
    html (required)

    Testcases:
    No testcase defined.
    set form_name [$form_obj name]
    set seed [lindex [$obj property seeds] $position]
    set substvalues [$form_obj property substvalues]
    #ns_log notice "CHECK-AA $form_name seed <$seed> // seeds <[$obj property seeds]> // subs '$substvalues'"
    
    set fc [$form_obj property form_constraints]
    set dfc [$form_obj property disabled_form_constraints]
    
    if {$seed eq "" && $substvalues ne ""} {
      ns_log warning "percent_substitute_in_form cannot substitute percent variables in $form_name"
    } else {
      if {$substvalues ne ""} {
        set html [:percent_substitute  -seed $seed  -substvalues $substvalues  $html]
        set fc [:percent_substitute  -seed $seed  -substvalues $substvalues  $fc]
        set dfc [:percent_substitute -verbose  -seed $seed  -substvalues [$form_obj property substvalues]  $dfc]
      }
    }
    return [list form $html form_constraints $fc disabled_form_constraints $dfc]
  • points_string (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> points_string \
        form_obj

    Get an attribute of the original question

    Parameters:
    form_obj (required, object)

    Testcases:
    No testcase defined.
    set points [:question_property $form_obj points]
    if {$points eq ""} {
      # just for legacy, questions without points
      set points [:question_property $form_obj minutes]
    }
    if {$points ne ""} {
      set pretty_label [expr {$points eq "1" ? [_ xowf.Point] : [_ xowf.Points]}]
      set minutes "($points $pretty_label)"
    }
  • question_count (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> question_count \
        [ -all ] obj

    Return the number questions in an exam. It is either the number of defined questions, or it might be restricted by the property max_items (if defined for "obj").

    Switches:
    -all (optional, defaults to "false")
    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    set nr_questions [llength [:question_names $obj]]
    if {!$all} {
      set max_items [$obj property max_items ""]
      if {$max_items ne ""} {
        if {$max_items < $nr_questions} {
          set nr_questions $max_items
        }
      }
    }
    return $nr_questions
  • question_info (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> question_info \
        [ -numbers numbers ] [ -with_title ] [ -with_minutes ] \
        [ -with_points ] [ -titleless_form ] [ -obj obj ] \
        [ -user_answers user_answers ] [ -no_position ] \
        [ -question_number_label question_number_label ] \
        [ -positions positions ] [ -with_question_count_in_title on|off ] \
        form_objs

    Returns a dict containing "form", "title_infos", "form_constraints" "disabled_form_constraints" "randomization_for_exam" "autograde" and "question_objs". This information is obtained from the provided "form_objs".

    Switches:
    -numbers (optional)
    -with_title (optional, defaults to "false")
    -with_minutes (optional, defaults to "false")
    -with_points (optional, defaults to "false")
    -titleless_form (optional, defaults to "false")
    -obj (optional, object)
    -user_answers (optional, object, accept empty)
    -no_position (optional, defaults to "false")
    -question_number_label (optional, defaults to "#xowf.question#")
    -positions (optional, integer)
    -with_question_count_in_title (optional, boolean, defaults to "false")
    Parameters:
    form_objs (required)
    Returns:
    dict containing "title_infos", "form_constraints", "disabled_form_constraints", "randomization_for_exam", "autograde", "question_forms", "question_objs"

    Testcases:
    No testcase defined.
    set full_fc {}
    set full_disabled_fc {}
    set title_infos {}
    set question_forms {}
    
    set randomizationOk 1
    set autoGrade 1
    
    if {[llength $positions] == 0} {
      set position -1
      set positions [lmap form_obj $form_objs {incr position}]
    }
    
    set question_count_label ""
    if {$with_question_count_in_title} {
      set question_count_label " / [:question_count $obj]"
    }
    
    foreach form_obj $form_objs number $numbers position $positions {
      set form_obj [:FL rename_attributes $form_obj]
      set form_title [$form_obj title]
      set minutes [:question_property $form_obj minutes]
      set points [:question_property $form_obj points]
      if {$points eq ""} {
        #ns_log notice "[$form_obj name]: NO POINTS, default to minutes $minutes"
        set points $minutes
      }
      set time_budget [$obj property time_budget]
      if {$time_budget ni {"" 100} && $minutes ne ""} {
        set minutes [expr {$time_budget*$minutes/100.0}]
        ns_log notice "[$form_obj name]: TIME BUDGET '$time_budget' -> minutes set to $minutes"
      }
      set mapping {show_points with_points show_minutes with_minutes}
      foreach property {show_points show_minutes} {
        if {[$obj property $property] ne ""} {
          set [dict get $mapping $property] [$obj property $property]
          #ns_log notice "[$form_obj name]: override flag via exam setting: '$property' -> [$obj property $property]"
        }
      }
      set title ""
      if {$number ne ""} {
        append title "$question_number_label $number $question_count_label:"
      }
    
      set title_components {}
      if {$with_title} {
        lappend title_components [ns_quotehtml $form_title]
      }
      if {$with_minutes} {
        lappend title_components [:minutes_string $form_obj]
      }
      if {$with_points} {
        lappend title_components [:points_string $form_obj]
      }
      append title " " [join $title_components " - "]
    
      #
      # The flag "no_position" is just provided for the composite
      # form, in cases where we are called at form generation time,
      # where the position is different from the position in the
      # exam. When the position is fixed, we do not provide it as an
      # argument. As a consequence, the percent substitution is not
      # performed, since it would return always very similar values
      # based on a fixed position.
      #
      if {$no_position} {
        set positionArg {}
      } else {
        set positionArg [list -position $position]
      }
      #ns_log notice "CHECK 0 user_answers <$user_answers> (obj is the inclass exam [$obj name])"
      if {$user_answers eq ""} {
        set user_answers $obj
      }
      #
      # Resolve links in the context of the resolve_object
      #
      set d [:item_substitute_markup  -obj $user_answers  {*}$positionArg  -form_obj $form_obj]
    
      lappend question_forms [dict get $d form]
      lappend title_infos [list full_title $title  title $form_title  minutes $minutes  points $points  number $number]
      lappend full_fc [:add_to_fc  -fc [dict get $d form_constraints]  -minutes $minutes  -points $points  {*}$positionArg]
    
      lappend full_disabled_fc [:add_to_fc  -fc [dict get $d disabled_form_constraints]  -minutes $minutes  -points $points  {*}$positionArg]
    
      if {![:question_is_autograded $form_obj]} {
        set autoGrade 0
      }
      if {![:question_randomization_ok $form_obj]} {
        set randomizationOk 0
      }
    }
    
    return [list  title_infos $title_infos  form_constraints [join [lsort -unique $full_fc] \n]  disabled_form_constraints [join [lsort -unique $full_disabled_fc] \n]  randomization_for_exam $randomizationOk  autograde $autoGrade  question_forms $question_forms  question_objs $form_objs]
  • question_info_block (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> question_info_block \
        obj

    Provide question info block.

    Parameters:
    obj (required)

    Testcases:
    No testcase defined.
    set HTML [:question_overview_block $obj]
    append HTML [:question_statistics_block $obj]
    
    return $HTML
  • question_names (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> question_names \
        obj

    Return the names of the questions of an assessment.

    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    if {[info exists :wfi]} {
      if {![nsf::is object ${:wfi}]} {
        ns_log notice "we cannot trust :wfi '${:wfi}', probably a leftover"
        unset :wfi
      }
    }
    if {[info exists :wfi] && [${:wfi} property question] ne ""} {
      set names [${:wfi} property question]
      #ns_log notice "question_names returns obj-specific [join $names]"
    } else {
      set names [$obj property question]
      #ns_log notice "question_names returns wf-names ($obj property): [join $names]"
    }
    return $names
  • question_objs (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> question_objs \
        [ -shuffle_id shuffle_id ] obj

    For the provided assessment object, return the question objects in the right order, depending on the shuffle_id.

    Switches:
    -shuffle_id (optional, integer, defaults to "-1")
    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    :assert_assessment $obj
    set form_objs [:load_question_objs $obj [:question_names $obj]]
    #ns_log notice "question_objs from $obj => $form_objs shuffle_id $shuffle_id"
    
    if {$shuffle_id > -1} {
      set result {}
      foreach i [::xowiki::randomized_indices -seed $shuffle_id [llength $form_objs]] {
        lappend result [lindex $form_objs $i]
      }
      set form_objs $result
    }
    
    #
    # Return at most max items, when specified.
    #
    return [:max_items [$obj property max_items ""$form_objs]
  • question_property (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> question_property \
        form_obj attribute [ default ]

    Get an attribute of the original question

    Parameters:
    form_obj (required, object)
    attribute (required)
    default (optional)

    Testcases:
    No testcase defined.
    set question [$form_obj get_property -name question]
    #:msg question=$question
    if {[dict exists $question question.$attribute]} {
      set value [dict get $question question.$attribute]
    } else {
      set value $default
    }
    return $value
  • question_statistics_block (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> question_statistics_block \
        obj

    When we have results, we can provide statistics

    Parameters:
    obj (required)

    Testcases:
    No testcase defined.
    set HTML ""
    if {[$obj state] in {done submission_review}} {
    
      template::head::add_link -rel stylesheet -href /resources/xowf/test-item.css
      set combined_form_info [:combined_question_form -with_numbers $obj]
    
      #
      # Get the form-field objects with all alternatives (use flag
      # "-generic")
      #
      set form_field_objs [:AM answer_form_field_objs  -generic  -wf [:AM get_answer_wf $obj]  $combined_form_info]
      #
      # Get the persisted statistics from the workflow
      # instance. These statistics are computed when the exam
      # protocol is rendered.
      #
      set statistics [:AM get_exam_results -obj $obj statistics]
      if {$statistics ne ""} {
        foreach var {success_statistics count_statistics} key {success count} {
          if {[dict exists $statistics $key]} {
            set $var [dict get $statistics $key]
          } else {
            set $var ""
          }
        }
    
        #
        # Merge the statistics into the generic form-fields such we
        # can use the usual form-field based rendering.
        #
        foreach form_field_obj $form_field_objs {
          #
          # The linkage between the statistics and the form-fields
          # is performed via the form-field names. Note that in
          # cases, where multiple folders are used as a source, the
          # names have to be disambiguated.
          #
          set name [$form_field_obj name]
          set result_statistics ""
          if {[dict exists $success_statistics $name]} {
            set result_statistics [dict get $success_statistics $name]
          }
          if {[dict exists $count_statistics $name]} {
            #ns_log notice "statistics question_info_block $name count '[dict get $count_statistics $name]'"
            dict set result_statistics count [dict get $count_statistics $name]
            $form_field_obj set result_statistics $result_statistics
          }
        }
      }
    
      #
      # Substitute form-field place-holders ion the combined form.
      #
      set combined_form [:aggregated_form $combined_form_info]
      set form [$obj regsub_eval   [template::adp_variable_regexp$combined_form  {$obj form_field_as_html -mode display "\\\1" "\2" $form_field_objs}]
    
      append HTML $form
    }
    return $HTML
  • question_summary (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> question_summary \
        obj

    Provide a summary of all questions of an exam.

    Parameters:
    obj (required)

    Testcases:
    No testcase defined.
    set results [:AM get_exam_results -obj $obj results]
    if {$results ne ""} {
      #https://localhost:8443/xowf/online-exam/inclass-exam1?per-question=1&m=print-statistics&format=csv&onlygrades=0
      set href [$obj pretty_link -query m=exam-results&format=csv&per-question=1]
      set results_summary [subst {
        <p>#xowf.export_results#: <a title="#xowf.export_results_title#" href="[ns_quotehtml $href]">
        <adp:icon name="filetype-csv" title="CSV"></a>
      }]
    } else {
      set results_summary ""
    }
    
    set return_url [::xo::cc query_parameter local_return_url:localurl [$obj pretty_link]]
    return [ns_trim -delimiter | [subst {
      | [:question_info_block $obj]
      | $results_summary
      | <hr><p><a class="[::template::CSS class action]" href="[ns_quotehtml $return_url]">#xowiki.back#</a></p>
    }]]
  • questions_without_minutes (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> questions_without_minutes \
        [ -max_items max_items ] form_info

    Compute the number of questions without provided time

    Switches:
    -max_items (optional, integer, accept empty)
    Parameters:
    form_info (required)

    Testcases:
    No testcase defined.
    set number 0
    foreach title_info [:title_infos -max_items $max_items $form_info] {
      if {[dict exists $title_info minutes]} {
        set value [dict get $title_info minutes]
        if {$value eq ""} {
          set value 0
        }
        if {$value == 0} {
          incr number
        }
      }
    }
    return $number
  • replace_pool_question (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> replace_pool_question \
        [ -position position ] [ -seed seed ] \
        [ -allowed_forms allowed_forms ] [ -field_name field_name ] \
        [ -pool_question_obj pool_question_obj ] \
        [ -exam_question_names exam_question_names ]
    Switches:
    -position (optional)
    -seed (optional)
    -allowed_forms (optional, defaults to "en:edit-interaction.wf")
    -field_name (optional)
    -pool_question_obj (optional)
    -exam_question_names (optional)
    Returns:
    an initialized replacement form obj if this is possible

    Testcases:
    No testcase defined.
    set field_name ""## rely on fallback
    set candidate_dict [:get_pool_questions   -allowed_forms $allowed_forms  -field_name $field_name  $pool_question_obj  $exam_question_names]
    
    set candidate_names [dict keys $candidate_dict]
    set nrCandidates [llength $candidate_names]
    if {$nrCandidates == 0} {
      set h [ns_set iget [ns_conn headers] referrer]
      set url [join [lrange [split [xo::cc url] /] 0 end-1] /]?m=edit
      util_user_message -message "could not find a replacement item for pool question: no matching item found"
      ad_returnredirect $url
      ad_script_abort
    }
    
    #
    # It might be the case that we select the same item for an exam
    # twice. Therefore, we have to iterate, until we find different
    # items.
    #
    expr {srand($seed)}
    set maxiter 100
    while {1} {
      set i [expr {int(($nrCandidates) * rand())}]
      set new_name [lindex $candidate_names $i]
      #ns_log notice "replace_pool_question position $position seed $seed random_index $i"
    
      set contained [expr {$new_name in $exam_question_names}]
      #ns_log notice "replace_pool_question replace [$pool_question_obj name] by $new_name contained in"  #    "[lsort $exam_question_names] contained $contained"
      if {!$contained || [incr maxiter -1] < 0} {
        break
      }
    }
    if {$contained} {
      error "could not find a replacement item for [$pool_question_obj name]: only duplicate items found"
    
    }
    set form_obj [::xowiki::FormPage get_instance_from_db  -item_id [dict get $candidate_dict $new_name item_id]]
    
    #$form_obj initialize
    
    # ns_log notice [$form_obj serialize]
    return $form_obj
  • replace_pool_questions (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> replace_pool_questions \
        [ -answer_obj answer_obj ] [ -exam_obj exam_obj ]

    Replaces all pool questions for the exam by random items. In case there were replacement items, set/update the property "question" for the individual answer_obj.

    Switches:
    -answer_obj (optional, object)
    the workflow instance of the answer workflow
    -exam_obj (optional, object)
    the exam objject to which the answer_object belongs to

    Testcases:
    No testcase defined.
    if {[$answer_obj property question] ne ""} {
      ns_log notice "answer_obj $answer_obj has already a 'question' property"  [lsort [dict keys [$answer_obj instance_attributes]]]
      return
    }
    set exam_question_names [$exam_obj property question]
    set form_objs [:load_question_objs $exam_obj $exam_question_names]
    
    #
    # Make sure to normalize all names to ease comparison
    #
    set original_question_names [:qualified_question_names $form_objs]
    
    set replaced_form_objs {}
    set position 0
    set seeds [$answer_obj property seeds]
    foreach form_obj $form_objs {
      #ns_log notice "YYY check item_type '[$form_obj property item_type]' // [$form_obj instance_attributes]"
      if {[$form_obj property item_type] eq "PoolQuestion"} {
        set replaced_form_obj [:replace_pool_question  -position $position  -seed [lindex $seeds $position]  -pool_question_obj $form_obj  -exam_question_names $exam_question_names]
        set exam_question_names [lreplace $exam_question_names $position $position  [:qualified_question_names $replaced_form_obj]]
        lappend replaced_form_objs $replaced_form_obj
      } else {
        lappend replaced_form_objs $form_obj
      }
      incr position
    }
    #ns_log notice "YYYY OLD NAMES [join $original_question_names { }]"
    #ns_log notice "YYYY UPD NAMES [join $exam_question_names { }]"
    if {![:list_equal $original_question_names $exam_question_names]} {
      ns_log notice "YYYY store question names in user's answer workflow"
      $answer_obj set_property -new 1 question $exam_question_names
      #$answer_obj set_property -new 1 question_ids [lmap obj $replaced_form_objs {$obj item_id}]
    }
  • shuffled_index (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> shuffled_index \
        [ -shuffle_id shuffle_id ] obj position

    Return the shuffled index position, in case shuffling is turned on.

    Switches:
    -shuffle_id (optional, integer, defaults to "-1")
    Parameters:
    obj (required, object)
    position (required)

    Testcases:
    No testcase defined.
    if {$shuffle_id > -1} {
      #
      # Take always all questions as the basis for randomization,
      # also when "max_items" is set.
      #
      set shuffled [::xowiki::randomized_indices  -seed $shuffle_id  [:question_count -all $obj]]
      set position [lindex $shuffled $position]
      #ns_log notice "shuffled_index question_count [:question_count $obj] -> <$shuffled> -> position $position"
    }
    return $position
  • total_minutes (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> total_minutes \
        [ -max_items max_items ] form_info

    Compute the duration of an exam based on the form_info dict.

    Switches:
    -max_items (optional, integer, accept empty)
    Parameters:
    form_info (required)

    Testcases:
    No testcase defined.
    return [:total -property minutes [:title_infos -max_items $max_items $form_info]]
  • total_minutes_for_exam (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> total_minutes_for_exam \
        [ -manager manager ]

    Compute the total time of an exam, based on the minutes provided by the single questions.

    Switches:
    -manager (optional, object)

    Testcases:
    No testcase defined.
    set max_items [$manager property max_items ""]
    set combined_form_info [:combined_question_form $manager]
    set total_minutes [:total_minutes  -max_items $max_items  $combined_form_info]
  • total_points (scripted, public)

     <instance of xowf::test_item::Question_manager[i]> total_points \
        [ -max_items max_items ] form_info

    Compute the maximum achievable points of an exam based on the form_info dict.

    Switches:
    -max_items (optional, integer, accept empty)
    Parameters:
    form_info (required)

    Testcases:
    No testcase defined.
    return [:total -property points [:title_infos -max_items $max_items $form_info]]