%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 ::xowf::test_item::AssessmentInterface->::nx::Object

Class ::xowf::test_item::Answer_manager

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

Public API: - create_workflow - delete_all_answer_data - allow_answering - get_answer_wf - get_wf_instances - get_answer_attributes - student_submissions_exist - runtime_panel - render_answers_with_edit_history - render_answers - marked_results - answers_panel - exam_results - grading_table - grading_scheme - grade - participants_table - get_duration - get_IPs - revisions_up_to - last_time_in_state - last_time_switched_to_state - state_periods - time_window_setup - waiting_room_message
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::Answer_manager \
     -superclass ::xowf::test_item::AssessmentInterface

Methods (to be applied on instances)

  • allow_answering (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> allow_answering \
        [ -examwf examwf ] -ip ip 

    Tell if specified IP address is allowed to answer the exam.

    Switches:
    -examwf (optional, object)
    -ip (required)
    Returns:
    boolean

    Testcases:
    No testcase defined.
    set iprange [$examwf property iprange]
    if {$iprange ne ""} {
      set iprangeObj ::xowf::iprange::$iprange
      if {$iprange ne "all"
          && (![nsf::is object $iprangeObj]
              || ![$iprangeObj allow_access $ip]
              )} {
        ns_log notice "ANSWER: [list $iprangeObj allow_access $ip] ->"  [$iprangeObj allow_access $ip]
        return 0
      }
    }
    return 1
  • answer_form_field_objs (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> answer_form_field_objs \
        [ -clear ] [ -wf wf ] [ -generic ] form_info

    Instantiate the form_field objects of the provided form based on form_info.

    Switches:
    -clear (optional)
    -wf (optional, object)
    -generic (optional)
    Parameters:
    form_info (required)

    Testcases:
    No testcase defined.
    set key ::__test_item_answer_form_fields
    if {$clear} {
      #
      # The -clear option is needed, when there are multiple
      # assessments protocols/tables on the same page (currently
      # not).
      #
      unset -nocomplain $key
    } else {
      #ns_log notice "### answer_form_field_objs key exists [info exists $key]"
      if {![info exists $key]} {
        #ns_log notice "form_info: $form_info"
        set fc [lsort -unique [dict get $form_info disabled_form_constraints]]
        #ns_log notice "### FC $fc"
        set pc_params [::xo::cc perconnection_parameter_get_all]
        if {$generic} {
          set fc [:replace_in_fc -fc $fc shuffle_kind none]
          set fc [:replace_in_fc -fc $fc show_max ""]
        }
        set $key [$wf create_form_fields_from_form_constraints -lookup $fc]
        ::xo::cc perconnection_parameter_set_all $pc_params
        $wf form_field_index [set $key]
      }
      return [set $key]
    }
  • answers_panel (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> answers_panel \
        [ -polling ] [ -heading heading ] \
        [ -submission_msg submission_msg ] [ -manager_obj manager_obj ] \
        [ -target_state target_state ] [ -wf wf ] \
        [ -current_question current_question ] [ -extra_text extra_text ]

    Produce HTML code for an answers panel, containing the number of participants of an e-assessment and the number of participants, who have already answered.

    Switches:
    -polling (optional, defaults to "false")
    when specified, provide live updates of the numbers via AJAX calls
    -heading (optional, defaults to "#xowf.submitted_answers#")
    -submission_msg (optional, defaults to "#xowf.participants_answered_question#")
    -manager_obj (optional, object)
    -target_state (optional)
    -wf (optional, object)
    -current_question (optional)
    -extra_text (optional)
    optional extra text for the panel, has to be provided with valid HTML markup.

    Testcases:
    No testcase defined.
    set answers [:get_answer_attributes $wf]
    set nrParticipants [llength $answers]
    if {$current_question ne ""} {
      set answered [:FL answers_for_form  [$current_question name]  $answers]
    } else {
      set answered [:get_answer_attributes -state $target_state $wf]
    }
    set nrAnswered [llength $answered]
    
    set answerStatus [::xowiki::bootstrap::card  -title $heading  -body [subst {<p><span id='answer-status'>$nrAnswered/$nrParticipants</span>
                            $submission_msg<p>$extra_text}]]
    
    if {$polling} {
      #
      # Auto refresh of number of participants and submissions when
      # polling is on.
      #
      set url [$manager_obj pretty_link -query m=poll]
      template::add_body_script -script [subst -nocommands {
        (function poll() {
          setTimeout(function() {
            var xhttp = new XMLHttpRequest();
            xhttp.open("GET", '$url', true);
            xhttp.onreadystatechange = function() {
              if (this.readyState == 4 && this.status == 200) {
                var data = xhttp.responseText;
                var el = document.querySelector('#answer-status');
                el.innerHTML = data;
                poll();
                //activate links if a users started the exam
                var answers = data.split('/');
                if (answers.length == 2 && answers[1] > 0) {
                  var disabledLinkItems = document.querySelectorAll(".list-group-item.link-disabled");
                  disabledLinkItems.forEach(function(linkItem) {
                    linkItem.classList.remove("link-disabled");
                  });
                }
              }
            };
            xhttp.send();
          }, 1000);
        })();
      }]
    }
    
    return $answerStatus
  • countdown_timer (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> countdown_timer \
        -target_time target_time  -id id  [ -audio_alarm on|off ] \
        [ -audio_alarm_cookie audio_alarm_cookie ] \
        [ -audio_alarm_times audio_alarm_times ]

    Accepted formats for target_time, determined by JavaScript ISO 8601, e.g. YYYY-MM-DDTHH:mm:ss.sss" Set current time based on host time instead of new Date().getTime() to avoid surprises, in cases, the time at the client browser is set incorrectly.

    Switches:
    -target_time (required)
    -id (required)
    -audio_alarm (optional, boolean, defaults to "true")
    -audio_alarm_cookie (optional, defaults to "incass_exam_audio_alarm")
    -audio_alarm_times (optional, defaults to "60,30,20,10,5,2")

    Testcases:
    No testcase defined.
    set nowMs [clock milliseconds]
    set nowIsoTime [clock format [expr {$nowMs/1000}]  -format "%Y-%m-%dT%H:%M:%S"].[format %.3d [expr {$nowMs % 1000}]]
    
    template::add_body_script -script [subst {
      var countdown_target_date = new Date('$target_time').getTime();
      var countdown_days, countdown_hours, countdown_minutes, countdown_seconds;
      var countdown = document.getElementById('$id');
    
      // adjust target time by the difference between the host and client time
      countdown_target_date = countdown_target_date - (new Date('$nowIsoTime').getTime() - new Date().getTime());
    
      setInterval(function () {
        var current_date = new Date().getTime();
        var absolute_seconds_left = (countdown_target_date - current_date) / 1000;
        var seconds_left = absolute_seconds_left
        var HTML = '';
    
        countdown_days = parseInt(seconds_left / 86400);
        seconds_left = seconds_left % 86400;
        countdown_hours = parseInt(seconds_left / 3600);
        seconds_left = seconds_left % 3600;
        countdown_minutes = parseInt(seconds_left / 60);
        countdown_seconds = parseInt(seconds_left % 60);
    
        var alarmseconds = countdown.parentNode.dataset.alarmseconds;
        if (typeof alarmseconds !== 'undefined') {
          var full_seconds = Math.trunc(absolute_seconds_left);
          // for testing purposes, use: (full_seconds % 5 == 0)
          if (alarmseconds.includes(full_seconds)) {
            beep(200);
          }
        }
    
        if (seconds_left < -60) {
          countdown.innerHTML = "<span style='color:red;'>&nbsp;[_ xowf.Countdown_timer_expired]</span>"
          return
        }
    
        if (countdown_days != 0) {
          HTML += '<span class="days">' + countdown_days + ' <b> '
          + (countdown_days != 1 ? '[_ xowf.Days]' : '[_ xowf.Day]')
          + '</b></span> ';
        }
        if (countdown_hours != 0 || countdown_days != 0) {
          HTML += '<span class="hours">' + countdown_hours + ' <b> '
          + (countdown_hours != 1 ? '[_ xowf.Hours]' : '[_ xowf.Hour]')
          + '</b></span> ';
        }
        HTML += '<span class="minutes">' + countdown_minutes + ' <b> '
        + (countdown_minutes != 1 ? '[_ xowf.Minutes]' : '[_ xowf.Minute]')
        + '</b></span> '
        + '<span class="seconds">' + countdown_seconds + ' <b> '
        + (countdown_seconds != 1 ? '[_ xowf.Seconds]' : '[_ xowf.Second]')
        + '</b></span> [_ xowf.remaining]' ;
    
        countdown.innerHTML = HTML;
      }, 1000);
    
      var beep = (function () {
        return function (duration, finishedCallback) {
          var container = document.getElementById('$id').parentNode;
    
          //console.log("beep attempt " + duration + ' ' + audioContext + ' ' + container.dataset.alarm);
          if (typeof audioContext !== 'undefined' && (container.dataset.alarm == 'active')) {
    
            //console.log("true beep duration " + duration + ' ' + audioContext + ' ' + audioContext.state);
            var osc = audioContext.createOscillator();
            osc.type = "sine";
            osc.connect(audioContext.destination);
            if (osc.noteOn) osc.noteOn(0); // old browsers
            if (osc.start) osc.start(); // new browsers
    
            setTimeout(function () {
              if (osc.noteOff) osc.noteOff(0); // old browsers
              if (osc.stop) osc.stop(); // new browsers
            }, duration);
          }
        };
      })();
    }]
    
    if {$audio_alarm} {
      #
      # Audio alarm handling is more tricky than expected, since
      # modern browsers do not allow one to create an active sound
      # context without a "user gesture" (requires e.g. a click to
      # start).
      #
      # The code tries to remember the audio state between different
      # pages, such when e.g. being in an exam, the user has to
      # activate/deactivate the audio not on every page. However,
      # when the user does a full reload, then the user has to
      # activate the audio alarm again.
      #
      # The state is symbolized using bootstrap 3 glyphicons or
      # bootstrap icons.  The code is tested primarily with chrome.
      #
      template::add_body_script -script [subst {
        var audioContext = new AudioContext();
        var audioContext_setSate = (function (targetState) {
          var container = document.getElementById('$id').parentNode;
          //console.log('--- state = ' + audioContext.state + ' want ' + targetState);
          if (targetState == 'active') {
            var elements = container.querySelector('i');
            var prefix = 'bi';
            if (!elements) {
              elements = container.querySelector('span');
              prefix = 'glyphicon';
            }
            elements.classList.remove(prefix + '-volume-off');
            elements.classList.add(prefix + '-volume-up');
            container.dataset.alarm = 'active';
            document.cookie = '$audio_alarm_cookie=active; sameSite=strict';
            audioContext.resume().then(() => {console.log('Playback resumed successfully ' + targetState);});
          } else {
            var elements = container.querySelector('i');
            var prefix = 'bi';
            if (!elements) {
              elements = container.querySelector('span');
              prefix = 'glyphicon';
            }
            elements.classList.remove(prefix + '-volume-up');
            elements.classList.add(prefix + '-volume-off');
            container.dataset.alarm = 'inactive';
            document.cookie = '$audio_alarm_cookie=inactive; sameSite=strict';
            audioContext.suspend().then(() => {console.log('Playback suspended successfully ' + targetState);});
          }
          //console.log('setSate ' + audioContext.state + ' alarm ' + container.dataset.alarm);
        });
    
        var audioContext_toggle = (function (event) {
          var container = document.getElementById('$id').parentNode;
          //console.log('audioContext_toggle  ' + audioContext.state);
          if (container.dataset.alarm != 'active') {
            audioContext_setSate('active');
            beep(200);
          } else {
            audioContext_setSate('inactive');
          }
        });
    
        var audioContext_onload = (function (event) {
          var m = document.cookie.match('(^|;)\\s*$audio_alarm_cookie\\s*=\\s*(\[^;\]+)');
          var cookieValue = (m ? m.pop() : 'inactive');
    
          console.log('audioContext_onload ' + audioContext.state + ' cookie ' + cookieValue);
          //
          // When the current state is 'running' the behavior seems
          // cross browser uniform, we can set it to the state we got
          // from the cookie.
          //
          if (audioContext.state == 'running') {
            audioContext_setSate(cookieValue);
          } else {
            //
            // FireFox can switch to "active" after reload, while
            // this does not work on Chrome and friends.
            //
            if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
              audioContext_setSate(cookieValue);
            } else {
              audioContext_setSate('inactive');
            }
          }
        });
        console.log('onload');
        console.log(document.getElementById('$id'));
    
        console.log('register audiocontext_toggle');
        document.getElementById('$id').parentNode.addEventListener('click', audioContext_toggle);
        window.addEventListener('load', audioContext_onload);
      }]
    
      if {[ns_conn isconnected]} {
        #
        # The icon names "volume-off" and "volume-up" exist in the
        # glyph icons and for the bootstrap icons (Bootstrap 5)
        #
        set alarmState [ns_getcookie $audio_alarm_cookie "inactive"]
        set icon [expr {$alarmState eq "inactive" ? "volume-off":"volume-up"}]
      } else {
        set alarmState "inactive"
        set icon "volume-off"
      }
      #ns_log notice "C=$alarmState"
    
      return [subst {
        <div data-alarm='$alarmState' data-alarmseconds='\[$audio_alarm_times\]'>
        <adp:icon name='$icon'>
        <div style='display: inline-block;' id='$id'></div>
        </div>
      }]
    } else {
      return [subst {
        <div style='display: inline-block;' id='$id'></div>
      }]
    }
  • create_workflow (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> create_workflow \
        [ -answer_workflow answer_workflow ] \
        [ -master_workflow master_workflow ] parentObj

    Create a workflow based on the template provided in this method for answering the question for the students. The name of the workflow is derived from the workflow instance and recorded in the formfield "wfName". :log "create_answer_workflow $parentObj"

    Switches:
    -answer_workflow (optional, defaults to "/packages/xowf/lib/online-exam-answer.wf")
    -master_workflow (optional, defaults to "en:Workflow.form")
    Parameters:
    parentObj (required, object)

    Testcases:
    No testcase defined.
    # first delete workflow and data, when it exists
    if {[$parentObj property wfName] ne ""} {
      set wf [:delete_all_answer_data $parentObj]
      if {$wf ne ""} {$wf delete}
    }
    
    #
    # Create a fresh workflow (e.g. instance of the online-exam,
    # inclass-quiz, ...).
    #
    set wfName [$parentObj name].wf
    $parentObj set_property -new 1 wfName $wfName
    
    set wfTitle [$parentObj property _title]
    set questionObjs [:QM question_objs $parentObj]
    
    set wfQuestionNames {}
    set wfQuestionTitles {}
    set attributeNames {}
    foreach form_obj $questionObjs {
      lappend attributeNames  [:FL form_name_based_attribute_stem [$form_obj name]]
    
      lappend wfQuestionNames ../[$form_obj name]
      lappend wfQuestionTitles [$form_obj title]
    }
    set wfID [$parentObj item_id]
    
    set wfDef [subst -nocommands {
      set wfID $wfID
      set wfQuestionNames [list $wfQuestionNames]
      xowf::include $answer_workflow
    }]
    set attributeNames [join $attributeNames ,]
    
    #:log "create workflow by filling out form '$master_workflow'"
    set WF [::[$parentObj package_id] instantiate_forms  -parent_id    [$parentObj parent_id]  -forms $master_workflow  -default_lang [$parentObj lang]]
    
    set fc {}
    lappend fc  "@table:_item_id,_state,$attributeNames,_last_modified"  "@table_properties:view_field=_item_id"  @cr_fields:hidden
    
    set wf [$WF create_form_page_instance  -name                $wfName  -nls_language        [$parentObj nls_language]  -publish_status      ready  -parent_id           [$parentObj item_id]  -package_id          [$parentObj package_id]  -default_variables   [list title $wfTitle]  -instance_attributes [list workflow_definition $wfDef  form_constraints $fc]]
    $wf save_new
    #ns_log notice "create_answer_workflow $wf DONE [$wf pretty_link] IA <[$wf instance_attributes]>"
    #ns_log notice "create_answer_workflow parent $parentObj IA <[$parentObj instance_attributes]>"
    
    set time_window [$parentObj property time_window]
    if {$time_window ne ""} {
      :time_window_setup $parentObj -time_window $time_window
    }
  • delete_all_answer_data (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> delete_all_answer_data \
        obj

    Delete all instances of the answer workflow

    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    set wf [:get_answer_wf $obj]
    if {$wf ne ""} {
      set items [:get_wf_instances -initialize false $wf]
      foreach i [$items children] {
        $i www-delete
      }
    }
    #
    # Delete as well the manual gradings for this exam.
    #
    #$obj set_property -new 1 manual_gradings {}
    :AM set_exam_results -obj $obj manual_gradings {}
    
    return $wf
  • delete_scheduled_atjobs (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> delete_scheduled_atjobs \
        obj

    Delete previously scheduled atjobs ns_log notice "#### delete_scheduled_atjobs"

    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    set item_id [$obj item_id]
    set atjob_form_id [::xowf::atjob form_id -parent_id $item_id -package_id [ad_conn package_id]]
    
    set to_delete [xo::dc list get_children {
      select item_id from xowiki_form_instance_item_index
      where parent_id = :item_id
      and page_template = :atjob_form_id
    }]
    
    foreach id $to_delete {
      ns_log notice "#### acs::dc call content_item proc delete -item_id $id"
      acs::dc call content_item delete -item_id $id
    }
  • exam_results (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> exam_results \
        [ -manual_gradings manual_gradings ] \
        [ -gradingScheme gradingScheme ] [ -only_grades on|off ] \
        [ -reply ] [ -format format ] [ -orderby orderby ] results_dict

    Return results either as HTML table, as HTML chart or as csv. When "reply" is set. the result is returned directly to the browser (for downloading). When "gradingScheme" is empty, this method returns the following fields: participant, question, achieved_points, achievable points, comment When the "gradingScheme" is specified the results are per-participant. In this cases, when the "gradingScheme" is "....::none", the fields are participant, achieved, percentage otherwise the grade and rounding of achieved points and percentage are exported based on the rules of the grading scheme. participant, achieved, percentage, grade When additionally "only_grades" is specified, just participant and grad are returned/exported.

    Switches:
    -manual_gradings (optional)
    -gradingScheme (optional)
    needed for reporting grades, can be empty
    -only_grades (optional, boolean, defaults to "false")
    -reply (optional, defaults to "false")
    when false, csv will be returned as text, when true, it will be returned as response to the browser.
    -format (optional, defaults to "csv")
    -orderby (optional, defaults to "participant,desc")
    Parameters:
    results_dict (required)
    the results to format as csv, every key in the dict represents a user_id.
    Returns:
    csv as value or as response to the client

    Testcases:
    No testcase defined.
    set result ""
    if {$gradingScheme eq ""} {
      set t [:result_table per_question  -manual_gradings $manual_gradings  $results_dict]
    } else {
      set t [:result_table per_participant  -gradingScheme $gradingScheme  -only_grades $only_grades  -manual_gradings $manual_gradings  $results_dict]
    }
    
    lassign [split $orderby ,] att order
    
    $t orderby  -order [expr {$order eq "asc" ? "increasing" : "decreasing"}]  -type [ad_decode $att achieved real achievable real grade integer dictionary]  $att
    
    #
    # XLS export requires OOXML
    #
    # See https://fossil.sowaswie.de/ooxml/index
    #
    if {$format eq "xls" &&
        [::namespace which ::ooxml::xl_write] eq ""} {
      set format csv
    }
    
    if {$reply} {
      switch $format {
        html    {
          ns_return 200 "text/html; charset=utf-8" [$t asHTML]
          ad_script_abort
        }
        xls {
          set s [::ooxml::xl_write new]
          set sheet [$s worksheet {1}]
          set decimal [lc_get "decimal_point"]
          set doublestyle [$s style -numfmt [$s numberformat -decimal -format "#${decimal}##"]]
          set stringstyle [$s style -numfmt [$s numberformat -string]]
          set datestyle [$s style -numfmt [$s numberformat -date]]
          set cellformat {}
    
          #iterate cols of table
          $s row $sheet
          set displayColumns [lmap column [${t}::__columns children] {
            if {[$column exists no_csv]} continue
            if {[$column istype ::xo::Table::BulkAction]} continue
            if {[$column istype ::xo::Table::HiddenField]} continue
            set column
          }]
          foreach column $displayColumns {
            if {[$column name] in {"achieved" "achievable" "percentage"}} {
              lappend cellformat double
            } else {
              lappend cellformat string
            }
            set label [$column label]
            if {[regexp {^#([a-zA-Z0-9_:-]+\.[a-zA-Z0-9_:-]+)#$} $label _ message_key]} {
              set label [_ $message_key]
            }
            set value [string map {\" \\\" \n \r} $label]
            $s cell $sheet $value
          }
          #iterate row content
          foreach row [$t children] {
            $s row $sheet
            set i 0
            foreach column $displayColumns {
              set value [string map {\" \\\" \n \r} [$row set [$column set name]]]
              set format [lindex $cellformat $i]
              $s cell $sheet $value -style [set [set format]style]
              incr i
            }
          }
          $s write results.xlsx
          ad_script_abort
        }
        default {set result [$t write_csv]}
      }
    } else {
      switch $format {
        chart   {set result [:grading_table [$t set __grade_dict]]}
        html    {set result [$t asHTML]}
        default {set result [$t format_csv]}
      }
    }
    $t destroy
    return $result
  • export_answer (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> export_answer \
        [ -combined_form_info combined_form_info ] -html html  \
        -recutil recutil  [ -submission submission ]

    Export the provided question and answer in GNU rectuil format. ns_log notice "answers: [$submission serialize]"

    Switches:
    -combined_form_info (optional)
    -html (required)
    -recutil (required, object)
    -submission (optional, object)

    Testcases:
    No testcase defined.
    if {[$submission exists __form_fields]} {
      set form_fields [$submission set __form_fields]
    } else {
      #
      # We do not have the newest version of xowiki, so locate the
      # objs the hard way based on the naming convention.
      #
      set form_field_objs [lmap f [::xowiki::formfield::FormField info instances -closure] {
        if {![string match *_ [$f name]]} {continue}
        set f
      }]
      foreach form_field_obj $form_field_objs {
        dict set form_fields [$form_field_obj name] $form_field_obj
      }
      ns_log notice "export_answers: old style form_fields: $form_fields"
    }
    
    set export_dict ""
    set user [$submission set creation_user]
    if {![info exists ::__running_ids]} {
      set ::__running_ids ""
    }
    if {![dict exists $::__running_ids $user]} {
      dict set ::__running_ids $user [incr ::__running_id]
    }
    
    set seeds [$submission property seeds]
    set instance_attributes [$submission set instance_attributes]
    set answer_attributes [lmap a $instance_attributes {
      if {![string match *_ $a]} {continue}
      set a
    }]
    
    #ns_log notice "export_answers: combined_form_info: $combined_form_info"
    #set title_infos [dict get $combined_form_info title_infos]
    
    #
    # Get the question dict, which is a mapping between question
    # names and form_obj_ids.
    #
    set question_dict [:FL name_to_question_obj_dict  [dict get $combined_form_info question_objs]]
    # ns_log notice "export_answers: question_dict: $question_dict"
    
    set form_constraints [lsort -unique [dict get $combined_form_info form_constraints]]
    set fc_dict [:fc_to_dict $form_constraints]
    #ns_log notice "... form_constraints ([llength $form_constraints]) $form_constraints"
    #ns_log notice ".... dict $fc_dict"
    #
    # Every answer_attribute contains the answer to a test_item
    # (which potentially sub answers).
    #
    foreach a $answer_attributes {
      #ns_log notice "answers <[dict get $instance_attributes $a]>"
      foreach {alternative_id answer} [dict get $instance_attributes $a] {
        set alt_value [lindex [split $alternative_id .] 1]
        set form_obj [dict get $question_dict $a]
    
        #set ff [dict get $form_fields $a]
        #ns_log notice "answer $a: [dict get $instance_attributes $a] [$ff serialize]"
        #ns_log notice "answer $a: form_obj [$form_obj serialize]"
        set form_obj_ia [$form_obj instance_attributes]
        #ns_log notice "answer $a: [dict get $instance_attributes $a] [dict keys [dict get $form_obj_ia question]]"
        #ns_log notice "INTERACTION [dict get [dict get $form_obj_ia question] question.interaction]"
        set intro [dict get [dict get [dict get $form_obj_ia question] question.interaction] question.interaction.text]
        #ns_log notice "TEXT $intro"
        #set question_title [question_manager question_property $form_obj title]
        #set question_minutes [question_manager question_property $form_obj minutes]
        #ns_log notice "answer $a: [dict get $instance_attributes $a] [dict keys [dict get $form_obj_ia question]]"
    
        #dict set export_dict name $a
        dict set export_dict name $alternative_id
        dict set export_dict user_id $user
        dict set export_dict running_id [dict get $::__running_ids $user]
        dict set export_dict question_obj $form_obj
        dict set export_dict question_title [$form_obj title]
        dict set export_dict question_intro [ns_striphtml $intro]
        dict set export_dict question_minutes [dict get $fc_dict $a test_item_minutes]
        dict set export_dict question_points [dict get $fc_dict $a test_item_points]
        dict set export_dict question_text [ns_striphtml [:get_label_from_options $alt_value [dict get $fc_dict $a options]]]
        #dict set export_dict options [dict get $fc_dict $a options]
        dict set export_dict answer $answer
    
        ns_log notice "answer $a: DICT $export_dict"
        #ns_log notice "avail $a: [dict get $fc_dict $a]"
        $recutil ins $export_dict
      }
    }
  • get_IPs (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> get_IPs \
        revision_sets

    Get the IP addresses for the given revision set. Should be actually only one. The revision_set must not be empty.

    Parameters:
    revision_sets (required)

    Testcases:
    No testcase defined.
    set IPs ""
    foreach revision_set $revision_sets {
      set ip [ns_set get $revision_set creation_ip]
      if {$ip ne ""} {
        dict set IPs [ns_set get $revision_set creation_ip] 1
      }
    }
    return [dict keys $IPs]
  • get_answer_attributes (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> get_answer_attributes \
        [ -state state ] [ -extra_attributes extra_attributes ] wf

    Extracts wf instances as answers (e.g., extracting their answer-specific attributes)

    Switches:
    -state (optional)
    retrieve only instances in this state
    -extra_attributes (optional)
    return these attributes additionally as key/value pairs per tuple
    Parameters:
    wf (required, object)
    the workflow
    Returns:
    a list of dicts

    Testcases:
    No testcase defined.
    set results {}
    set items [:get_wf_instances $wf]
    foreach i [$items children] {
      if {$state ne "" && [$i state] ne $state} {
        continue
      }
    
      set answerAttributes [:FL answer_attributes [$i instance_attributes]]
      foreach extra $extra_attributes {
        lappend answerAttributes $extra [$i property $extra]
      }
      #ns_log notice "get_answer_attributes $i: <$answerAttributes> ALL [$i instance_attributes]"
      lappend results [list item $i answerAttributes $answerAttributes state [$i state]]
    }
    return $results
  • get_answer_wf (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> get_answer_wf obj

    return the workflow denoted by the property wfName in obj

    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    return [::[$obj package_id] instantiate_forms  -parent_id    [$obj item_id]  -default_lang [$obj lang]  -forms        [$obj property wfName]]
  • get_duration (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> get_duration \
        [ -exam_published_time exam_published_time ] revision_sets

    Get the duration from a set of revisions and return a dict containing "from", "fromClock","to", "toClock", "seconds", and "duration".

    Switches:
    -exam_published_time (optional)
    Parameters:
    revision_sets (required)

    Testcases:
    No testcase defined.
    set first [lindex $revision_sets 0]
    set last [lindex $revision_sets end]
    set fromClock [clock scan [::xo::db::tcl_date [ns_set get $first creation_date] tz]]
    set toClock [clock scan [::xo::db::tcl_date [ns_set get $last last_modified] tz]]
    dict set r fromClock $fromClock
    dict set r toClock $toClock
    dict set r from [clock format $fromClock -format "%H:%M:%S"]
    dict set r to [clock format $toClock -format "%H:%M:%S"]
    set timeDiff [expr {$toClock - $fromClock}]
    dict set r duration "[expr {$timeDiff/60}]m [expr {$timeDiff%60}]s"
    dict set r seconds $timeDiff
    if {$exam_published_time ne ""} {
      set examPublishedClock [clock scan [::xo::db::tcl_date $exam_published_time tz]]
      dict set r examPublishedClock $examPublishedClock
      dict set r examPublished [clock format $examPublishedClock -format "%H:%M:%S"]
      set epTimeDiff [expr {$toClock - $examPublishedClock}]
      dict set r examPublishedDuration "[expr {$epTimeDiff/60}]m [expr {$epTimeDiff%60}]s"
      #ns_log notice "EP examPublishedDuration [dict get $r examPublishedDuration]"  "EP [dict get $r examPublished] $exam_published_time"
      dict set r examPublishedSeconds $epTimeDiff
    }
    return $r
  • get_exam_results (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> get_exam_results \
        -obj obj  property [ default ]

    Retrieve a property value from the exam statistics result page. This page is an instance of the exam statistics workflow stored as a child of the exam object.

    Switches:
    -obj (required, object)
    the exam object
    Parameters:
    property (required)
    the property name
    default (optional)
    default value when property is not found

    Testcases:
    No testcase defined.
    set p [$obj childpage -name en:result -form inclass-exam-statistics.wf]
    set instance_attributes [$p instance_attributes]
    if {[dict exists $instance_attributes $property]} {
      #ns_log notice "get_exam_results <$property> returns value from "  "results page: [dict get $instance_attributes $property]"
      return [dict get $instance_attributes $property]
    }
    #ns_log notice "get_exam_results <$property> returns default"
    return $default
  • get_wf_instances (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> get_wf_instances \
        [ -initialize initialize ] [ -orderby orderby ] \
        [ -creation_user creation_user ] [ -item_id item_id ] \
        [ -state state ] wf

    get_wf_instances: return the workflow instances

    Switches:
    -initialize (optional, defaults to "false")
    -orderby (optional)
    -creation_user (optional, integer)
    -item_id (optional, integer)
    -state (optional)
    Parameters:
    wf (required, object)

    Testcases:
    No testcase defined.
    :assert_assessment_container $wf
    set extra_where_clause ""
    foreach var {creation_user item_id state} {
      if {[info exists $var]} {
        append extra_where_clause "AND $var = [ns_dbquotevalue [set $var]] "
      }
    }
    
    return [::xowiki::FormPage get_form_entries  -base_item_ids             [$wf item_id]  -form_fields               ""  -always_queried_attributes "*"  -initialize                $initialize  -orderby                   $orderby  -extra_where_clause        $extra_where_clause  -publish_status            all  -package_id                [$wf package_id]]
  • grading_dialog_setup (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> grading_dialog_setup \
        examWf

    Define the modal dialog and everything necessary for reusing this dialog for multiple occasions. This method registers the pop-up and dismiss handlers for JavaScript and returns the HTML markup of the modal dialog.

    Parameters:
    examWf (required)
    Returns:
    HTML block for the modal dialog

    Testcases:
    No testcase defined.
    set url [$examWf pretty_link -query m=grade-single-item]
    
    # jquery-ui is just needed for draggable()
    ::template::add_body_script -src urn:ad:js:jquery-ui
    
    ::template::add_body_script -script [subst -novariables {
    
      function thumbnail_files_setup(element) {
        // to be called on the elements of class ".thumbnail-file"
        element.querySelectorAll('.thumbnail-file-text a.delete')
            .forEach(el => el.addEventListener('click', event => {
              // Get the "href" of the a.delete element
              // and call this actions in the background
              var href = event.currentTarget.getAttribute('href');
              if (!href) {
                console.log(".thumbnail-file does not have a proper delete link");
                return
              }
              var fileIcon = event.currentTarget.parentElement.parentElement;
              var request = new XMLHttpRequest();
              request.open('GET', href, true);
              request.onload = function() {
                if (this.status >= 200 && this.status < 400) {
                  // Success!
                  fileIcon.parentNode.removeChild(fileIcon);
                } else {
                  console.log('AJAX request returned bad return code: ' + this.status);
                }
              };
              request.send();
              event.preventDefault();
            }));
      };
    
      $(document).ready(function(){
        document.querySelectorAll('.thumbnail-file').forEach(el => thumbnail_files_setup(el));
    
        $('.modal-dialog').draggable();
        $('.modal .confirm').on('click', function(ev) {
          //
          // Popdown: "submit" button of grading dialog was pressed.
          //
          var id = ev.currentTarget.dataset.id;
          var gradingBox  = document.getElementById(id);
          var pointsInput = document.querySelector('#grading-points');
          var helpBlock   = document.querySelector('#grading-points-help-block');
          var comment     = document.querySelector('#grading-comment').value;
          var points      = pointsInput.value;
          var pointsFormGroup = pointsInput.parentElement.parentElement;
          var percentage  = "";
          let hiddenCSSclass = '[::template::CSS class d-none]';
    
          if (points != "") {
            //
            // Number validation
            //
            if (parseFloat(points) > parseFloat(pointsInput.max) || parseFloat(points) < parseFloat(pointsInput.min)){
              if (parseFloat(points) > parseFloat(pointsInput.max)) {
                helpBlock.textContent = '[_ xowf.Value_max] ' + pointsInput.max;
              } else {
                helpBlock.textContent = '[_ xowf.Value_min] ' + pointsInput.min;
              }
              pointsFormGroup.classList.add('has-error');
              helpBlock.classList.remove(hiddenCSSclass);
              ev.preventDefault();
              return false;
            } else {
              pointsFormGroup.classList.remove('has-error');
              helpBlock.classList.add(hiddenCSSclass);
            }
            var achievable = gradingBox.dataset.achievable;
            if (achievable != "") {
              percentage = "(" + (points*100.0/achievable).toFixed(2) + "%)";
            }
    
          } else {
            pointsFormGroup.classList.remove('has-error');
            helpBlock.classList.add(hiddenCSSclass);
          }
    
          document.querySelector('#' + id + ' .points').textContent = points;
          document.querySelector('#' + id + ' .percentage').textContent = percentage;
          document.querySelector('#' + id + ' .comment').textContent = comment;
          gradingBox.dataset.achieved = points;
          gradingBox.dataset.comment = comment;
          if (comment == "") {
            document.querySelector('#' + id + ' .feedback-label').classList.add(hiddenCSSclass);
          } else {
            document.querySelector('#' + id + ' .feedback-label').classList.remove(hiddenCSSclass);
          }
    
          // Copy the content of the thumbnail files wrapper from the dialog
          // to the main document and register the event handler.
          let thumbnailFilesWrapper =  document.querySelector('#' + id + ' .thumbnail-files-wrapper');
          if (!thumbnailFilesWrapper) {
            thumbnailFilesWrapper = document.createElement('div');
            thumbnailFilesWrapper.className = 'thumbnail-files-wrapper';
            document.querySelector('#' + id).appendChild(thumbnailFilesWrapper);
          }
          thumbnailFilesWrapper.innerHTML = document.querySelector('#thumbnail-files-wrapper').innerHTML;
          //document.querySelector('#' + id + ' .thumbnail-files-wrapper').innerHTML =
          //    document.querySelector('#thumbnail-files-wrapper').innerHTML;
          gradingBox.querySelectorAll('.thumbnail-file').forEach(el => thumbnail_files_setup(el));
    
          var user_id = gradingBox.dataset.user_id;
          var examGradingBox = document.getElementById('runtime-panel-' + user_id);
    
          var data = new FormData();
          data.append('question_name', gradingBox.dataset.question_name);
          data.append('user_id', user_id);
          data.append('achieved', points);
          data.append('comment', comment);
          data.append('grading_scheme', examGradingBox.dataset.grading_scheme);
          data.append('achieved_points', examGradingBox.dataset.achieved_points);
    
          var xhttp = new XMLHttpRequest();
          xhttp.open('POST', '[set url]', true);
          xhttp.onload = function () {
            if (this.readyState == 4) {
              if (this.status == 200) {
                var text = this.responseText;
                var span = document.querySelector('#runtime-panel-' + user_id + ' .achieved-points');
                span.textContent = text;
              } else {
                console.log('sent NOT ok');
              }
            }
          };
          xhttp.send(data);
    
          return true;
        });
    
        $('.modal-dialog form').keypress(function(e){
          if(e.keyCode == 13) {
            e.preventDefault();
            return false;
          }
        });
    
        $('#grading-modal').on('shown.bs.modal', function (ev) {
          //
          // Popup of grading dialog.
          // Copy values from data attributes to input fields.
          //
          var gradingBox = ev.relatedTarget.parentElement;
          document.getElementById('grading-question-title').textContent = gradingBox.dataset.title;
          document.getElementById('grading-participant').textContent = gradingBox.dataset.full_name;
    
          var pointsInput = document.getElementById('grading-points');
          pointsInput.value = gradingBox.dataset.achieved;
          pointsInput.max = gradingBox.dataset.achievable;
          document.getElementById('grading-comment').value = gradingBox.dataset.comment;
          //document.getElementById('drop-zone').dataset.link = gradingBox.dataset.link;
    
          var filesUpload = document.getElementById('js-upload-files');
          filesUpload.dataset.file_name_prefix = gradingBox.dataset.question_name;
          filesUpload.dataset.url = gradingBox.dataset.link;
          filesUpload.dataset.disposition = "FileIconified";
          //console.log("... URL "    + filesUpload.dataset.url);
    
          var feedBackFiles = gradingBox.getElementsByClassName("thumbnail-files-wrapper")\[0\];
          //
          // For legacy composite items, there is no "thumbnail-files-wrapper"
          //
          // console.log(feedBackFiles);
          document.getElementById('thumbnail-files-wrapper').innerHTML =
              (feedBackFiles ? feedBackFiles.innerHTML : "");
    
          document.querySelectorAll('#grading-modal .thumbnail-file').forEach(el => thumbnail_files_setup(el));
    
          // Tell confirm button to which grading box it belongs
          var confirmButton = document.querySelector('#grading-modal-confirm');
          confirmButton.dataset.id = gradingBox.id;
        });
      });
    }]
    
    set uploader_link [::[$examWf package_id] make_link $examWf file-upload]
    set dropZone [::xowiki::BootstrapNavbarDropzone new  -href $uploader_link  -label #xowf.Feedback_files_dnd#  -text "Text for SUBMIT label"  -file_name_prefix ""  -disposition File]
    set dropZoneHTML [$dropZone asHTML]
    #ns_log notice "dropZoneHTML=$dropZoneHTML"
    
    return [::xowiki::bootstrap::modal_dialog  -id grading-modal  -title "#xowf.Grading#: <span id='grading-participant'></span>"  -subtitle "#xowf.question#: <span id='grading-question-title'></span>"  -body [subst [ns_trim -delimiter | {
                  |<form class="form-horizontal" role="form" action='#' method="post">
                  |  <div class="form-group">
                  |    <label for="grading-points" class="control-label col-sm-2">#xowf.Points#:</label>
                  |    <div class="col-sm-9">
                  |      <input class="form-control" id="grading-points" placeholder="#xowf.Points#"
                  |             type="number" step="0.1" min="0">
                  |      <span id="grading-points-help-block" class="help-block hidden"></span>
                  |    </div>
                  |  </div>
                  |  <div class="form-group">
                  |    <label for="grading-comment" class="control-label col-sm-2">#xowf.feedback#:</label>
                  |    <div class="col-sm-9">
                  |      <textarea lines="2" class="form-control" id="grading-comment"
                  |                placeholder="..."></textarea>
                  |    </div>
                  |  </div>
                  |</form>
                  |<div class="control-label">#xowf.Feedback_files#:</div>
                  |<div id="thumbnail-files-wrapper"></div>
                  |<ul class="dropZone">$dropZoneHTML</ul>
                }]]  ]
  • grading_scheme (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> grading_scheme \
        -examWf examWf  [ -grading grading ] \
        [ -total_points total_points ]

    Return the grading scheme object based on the provided short name. In case the grading scheme belongs to the predefined grading schemes, the object can be directly loaded. When the name refers to a user-defined grading object, this might have to be loaded. We could consider some hints about the usefulness of the chosen grading scheme, E.g., when an exam has 40 points or less, rounding has the potential effect that a high percentage of the grade is just due to rounding. So, in such cases a non-rounding scheme should be preferred.

    Switches:
    -examWf (required, object)
    -grading (optional)
    -total_points (optional, defaults to "100")
    Returns:
    fully qualified grading scheme object

    Testcases:
    No testcase defined.
    #
    # When not grading is provided, this muse be a legacy question.
    #
    if {$grading eq ""} {
      #set grading [expr {$total_points < 40 ? "round-none" : "round-points"}]
      set grading "none"
      ns_log notice "--- legacy grading scheme -> none"
    }
    
    set grading_scheme ::xowf::test_item::grading::$grading
    if {![nsf::is object $grading_scheme]} {
      #
      # Maybe we have to load this grading scheme...
      #
      #ns_log notice "grading_scheme_name load loaded yet: '$grading'"
      #::xo::show_stack
      ::xowf::test_item::grading::load_grading_schemes  -package_id [$examWf package_id]  -parent_id [$examWf parent_id]
      ns_log notice "--- grading schemes loaded"
    }
    if {![nsf::is object $grading_scheme]} {
      set grading_scheme ::xowf::test_item::grading::round-points
      ns_log notice "--- fallback to default grading scheme object"
    }
    #ns_log notice "USE grading_scheme $grading_scheme"
    return $grading_scheme
  • grading_table (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> grading_table \
        [ -csv csv ] grade_dict

    Produce HTML markup based on a dict with grades as keys and counts as values.

    Switches:
    -csv (optional)
    Parameters:
    grade_dict (required)

    Testcases:
    No testcase defined.
    set gradingTable {<div class="grading-info"><div class="table-responsive"><table class="table grading">}
    append gradingTable  "<thead><th class='text-right col-md-1'>#xowf.Grade#</th><th class='col-md-1 text-right'>#</th></thead>"  "<tbody>\n"
    set nrGrades 0
    foreach v [dict values $grade_dict] { incr nrGrades $v}
    set grades [lsort [dict keys $grade_dict]]
    foreach k $grades {
      set count [dict get $grade_dict $k]
      set countPercentage [format %.2f [expr {$count *100.0 / $nrGrades}]]
      append gradingTable  <tr>  [subst {<td class="text-right">$k</td><td class="text-right">$count</td>}]  [subst {<td><div class="progress"><div class="progress-bar"
            style="width:$countPercentage%">$countPercentage%</div></td}]  </tr>\n
    }
    append gradingTable "</tbody></table></div>\n"
    if {$csv ne "" } {
      append gradingTable "<pre>$csv</pre></div>\n"
    }
    
    if {[template::head::can_resolve_urn urn:ad:js:highcharts]} {
      #
      # The highcharts package is available
      #
      template::add_body_script -src urn:ad:js:highcharts
      set graphID pie-[incr ::__xotcl_highcharts_pie]
      append gradingTable "<div id='$graphID'></div>\n"
      set data ""
      foreach k $grades {
        set count [dict get $grade_dict $k]
        set countPercentage [format %.2f [expr {$count *100.0 / $nrGrades}]]
        lappend data [subst {{name:'$k', y: $countPercentage}}]
      }
      set gradeLabel [_ xowf.Grade]
      template::add_body_script -script [subst [ns_trim {
        Highcharts.chart('$graphID', {
          chart: {type: 'pie'},
          plotOptions: {pie: {size: 200}, series: {dataLabels: {enabled: true, format: '$gradeLabel {point.name}: {point.y:.1f}%'} }},
          title: {text: ''},
          credits: {enabled: true },
          series: \[{name: 'Percentage', data: \[ [join $data ,] \]}\]
        });
      }]]
    }
    return $gradingTable
  • last_time_in_state (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> last_time_in_state \
        -state state  revision_sets

    Loops through revision sets and retrieves the latest date where state is equal the specified value.

    Switches:
    -state (required)
    Parameters:
    revision_sets (required)
    a list of ns_sets containing revision data. List is assumed to be sorted in descending creation_date order (as retrieved by get_revision_sets)
    Returns:
    a date

    Testcases:
    No testcase defined.
    set result ""
    foreach ps $revision_sets {
      if {$state eq [ns_set get $ps state]} {
        set result [ns_set get $ps last_modified]
      }
    }
    return $result
  • last_time_switched_to_state (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> last_time_switched_to_state \
        -state state  [ -before before ] revision_sets

    Loops through revision sets and retrieves the latest date where state is equal the specified value.

    Switches:
    -state (required)
    -before (optional)
    Parameters:
    revision_sets (required)
    a list of ns_sets containing revision data. List is assumed to be sorted in descending creation_date order (as retrieved by get_revision_sets)
    Returns:
    a date

    Testcases:
    No testcase defined.
    set result ""
    set last_state ""
    foreach ps $revision_sets {
      if {$before ne ""} {
        set currentClock [clock scan [::xo::db::tcl_date [ns_set get $ps last_modified] tz]]
        if {$currentClock > $before} {
          break
        }
      }
      if {$last_state ne $state && $state eq [ns_set get $ps state]} {
        set result [ns_set get $ps last_modified]
      }
      set last_state [ns_set get $ps state]
    }
    return $result
  • marked_results (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> marked_results \
        [ -obj obj ] [ -wf wf ] form_info

    Return for every participant the individual results for an exam

    Switches:
    -obj (optional, object)
    -wf (optional, object)
    Parameters:
    form_info (required)

    Testcases:
    No testcase defined.
    set form_field_objs [:answer_form_field_objs -wf $wf $form_info]
    
    set items [:get_wf_instances $wf]
    set results ""
    foreach i [$items children] {
      xo::cc eval_as_user -user_id [$i creation_user] {
        set participantResult [:participant_result -obj $obj $i $form_info $form_field_objs]
      }
      append results $participantResult \n
    }
    
    #ns_log notice "=== marked_results of [llength [$items children]] items => $results"
    return $results
  • participants_table (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> participants_table \
        [ -package_id package_id ] -items items  \
        [ -view_all_method view_all_method ] [ -state state ] wf

    This method returns an HTML table containing a row for every participant with Name and short summary information. This table provides as well an interface for sending messages to this student.

    Switches:
    -package_id (optional, integer)
    -items (required, object)
    -view_all_method (optional, defaults to "print-answers")
    -state (optional, defaults to "done")
    Parameters:
    wf (required, object)

    Testcases:
    No testcase defined.
    set form_field_objs {}
    lappend form_field_objs  [$wf create_raw_form_field  -name _online-exam-userName  -spec text,label=#xowf.participant#]  [$wf create_raw_form_field  -name _online-exam-fullName  -spec label,label=#acs-subsite.Name#,disableOutputEscaping=true]  [$wf create_raw_form_field  -name _state  -spec text,label=#xowf.Status#]  [$wf create_raw_form_field  -name _online-exam-seconds  -spec number,label=#xowf.Seconds#]  [$wf create_raw_form_field  -name _creation_date  -spec date,label=#xowiki.Page-last_modified#]
    
    #
    # Take "orderby" from the query parameter. If not set, order by
    # the first field.
    #
    set orderby [::$package_id query_parameter orderby:token ""]
    if {$orderby eq "" && [llength $form_field_objs] > 0} {
      set orderby [[lindex $form_field_objs 0] name],asc
    }
    
    #
    # Create table widget.
    #
    set table_widget [::xowiki::TableWidget create_from_form_fields  -package_id $package_id  -form_field_objs $form_field_objs  -type_map {_online-exam-seconds integer}  -orderby $orderby]
    #
    # Extend properties of individual answers and add notification
    # dialogs.
    #
    set dialogs ""
    set user_list {}
    foreach p [$items children] {
    
      #foreach ff_obj $answer_form_field_objs {
      #  $ff_obj object $p
      #  set property [$ff_obj name]
      #  $ff_obj value [$p property $property]
      #}
    
      #
      # Provide a notification dialog only before the student has
      # submitted her exam and the exam is published.
      #
      if {[$p state] ne "done" && [$wf state] eq "published"} {
        set dialog_info [::xowiki::includelet::personal-notification-messages  modal_message_dialog -to_user_id [$p creation_user]]
        append dialogs [dict get $dialog_info dialog] \n
        set notification_dialog_button [dict get $dialog_info link]
        $p set online-exam-fullName "$notification_dialog_button [$p set online-exam-fullName]</a>"
        lappend user_list [$p creation_user]
      }
    
      #
      # Extend every answer with corresponding precomputed extra
      # "_online-exam-*" values to ease rendering:
      #
      set duration [:get_duration [$p get_revision_sets]]
      $p set_property -new 1 _online-exam-seconds [dict get $duration seconds]
    }
    
    ::xowiki::includelet::personal-notification-messages  modal_message_dialog_register_submit  -url [$wf pretty_link -query m=send-participant-message]
    
    set bulk_notification_HTML ""
    
    if {$state eq "done"} {
      set uc {tcl {[$p state] ne "done"}}
    } else {
      set uc {tcl {false}}
    
      if {[llength $user_list] > 0} {
        #
        # Provide bulk notification message dialog to send message to all users
        #
        set dialog_info [::xowiki::includelet::personal-notification-messages  modal_message_dialog -to_user_id $user_list]
        append dialogs [dict get $dialog_info dialog] \n
        set notification_dialog_button [dict get $dialog_info link]
        set bulk_notification_HTML "<div class='bulk-personal-notification-message'>$notification_dialog_button #xowiki.Send_message_to# [llength $user_list] #xowf.Participants#</a></div>"
      }
    }
    #
    # Render table widget with extended properties.
    #
    set HTML [$table_widget render_page_items_as_table  -package_id $package_id  -items $items  -form_field_objs $form_field_objs  -csv true  -uc $uc  -view_field _online-exam-userName  -view_filter_link [$wf pretty_link -query m=$view_all_method]  {*}[expr {[info exists generate] ? [list -generate $generate] : ""}]  -return_url [ad_return_url]  -return_url_att local_return_url  ]
    $table_widget destroy
    return $dialogs$HTML$bulk_notification_HTML
  • prevent_multiple_tabs (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> prevent_multiple_tabs \
        [ -cookie_name cookie_name ]

    Prevent answering the same survey from multiple, concurrently open tabs.

    Switches:
    -cookie_name (optional, defaults to "multiple_tabs")

    Testcases:
    No testcase defined.
    template::add_body_script -script [subst {
      var cookieLine = document.cookie.split('; ').find(row => row.startsWith('$cookie_name='));
      var cookieValue = (cookieLine === undefined) ? 1 : parseInt(cookieLine.split('=')\[1\]) + 1;
      // console.log("cookie $cookie_name " + cookieValue);
      if (cookieValue > 1) {
        alert('Already open!');
        window.open("about:blank""_self").close();
      }
      document.cookie = "$cookie_name=" + cookieValue;
      // console.log("START finished -> " + document.cookie);
    
      window.onunload = function () {
        var cookieLine = document.cookie.split('; ').find(row => row.startsWith('$cookie_name='));
        var cookieValue = (cookieLine === undefined) ? 0 : parseInt(cookieLine.split('=')\[1\]) - 1;
        document.cookie = "$cookie_name=" + cookieValue;
        // console.log("UNLOAD finished -> " + document.cookie);
      };
    }]
  • recutil_create (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> recutil_create \
        [ -exam_id exam_id ] [ -fn fn ] [ -clear ]

    Create recfile

    Switches:
    -exam_id (optional, integer)
    -fn (optional, defaults to "answers.rec")
    -clear (optional)
    See Also:

    Testcases:
    No testcase defined.
    set export_dir $::acs::rootdir/log/exam-exports/$exam_id/
    if {![file isdirectory $export_dir]} {
      file mkdir $export_dir
    }
    if {$clear && [file exists $export_dir$fn]} {
      file delete -- $export_dir$fn
    }
    #
    # If we have no recutils, create for the time being a stub
    #
    if {![nsf::is class ::xo::recutil]} {
      ns_log warning "no recutil class available"
      set r [::xotcl::Object new -proc ins args {;}]
      return $r
    }
    return [::xo::recutil new -file $export_dir$fn]
  • render_answers (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> render_answers \
        [ -as_student on|off ] \
        [ -filter_submission_id filter_submission_id ] \
        [ -creation_user creation_user ] [ -revision_id revision_id ] \
        [ -filter_form_ids filter_form_ids ] [ -export on|off ] \
        [ -orderby orderby ] [ -grading grading ] \
        [ -with_grading_table on|off ] examWf

    Return the answers in HTML format in a somewhat printer friendly way, e.g. as the exam protocol.

    Switches:
    -as_student (optional, boolean, defaults to "false")
    -filter_submission_id (optional, integer, accept empty)
    -creation_user (optional, integer, accept empty)
    -revision_id (optional, integer, accept empty)
    -filter_form_ids (optional, integer)
    -export (optional, boolean, defaults to "false")
    -orderby (optional, defaults to "online-exam-userName")
    -grading (optional)
    -with_grading_table (optional, boolean, defaults to "false")
    Parameters:
    examWf (required, object)
    Returns:
    dict containing "do_stream" and "HTML" ns_log notice "RENDER ANSWERS 0"

    Testcases:
    No testcase defined.
    set combined_form_info [:QM combined_question_form $examWf]
    set autograde   [dict get $combined_form_info autograde]
    set totalPoints [:QM total_points  -max_items [$examWf property max_items ""]  $combined_form_info]
    
    set withSignature [$examWf property signature 0]
    set examTitle [$examWf title]
    set ctx [::xowf::Context require $examWf]
    set results ""
    
    set wf [:get_answer_wf $examWf]
    if {$wf eq ""} {
      return [list do_stream 0 HTML ""]
    }
    
    if {$filter_form_ids ne "" && $filter_form_ids ni [dict get $combined_form_info question_objs]} {
      ns_log warning "inclass-exam: ignore invalid form_obj '$filter_form_ids';"  "valid [dict get $combined_form_info question_objs]"
      set filter_form_ids ""
    }
    ns_log notice "--- grading '$grading'"
    set grading_scheme [:grading_scheme -examWf $examWf -grading $grading -total_points $totalPoints]
    #ns_log notice "--- grading_scheme $grading_scheme from grading '$grading'"
    
    set :grade_dict {}
    set :grade_csv ""
    
    set items [:submissions  -creation_user $creation_user  -filter_submission_id $filter_submission_id  -revision_id $revision_id  -wf $wf]
    #
    # In case we have many items to render (which might take a
    # while), use streaming mode.
    #
    set do_stream [expr {[llength [$items children]] > 100}]
    
    set HTML [:render_print_button]
    
    if {!$as_student} {
      #
      # When rendering for teachers, we offer the possibility for to
      # sort, filter and export submissions.
      #
      append HTML [:render_filter_bar  -examWf $examWf  -filter_form_ids $filter_form_ids  -revision_id $revision_id  -filter_submission_id $filter_submission_id  -orderby $orderby]
    }
    
    ::xo::cc set_parameter template_file view-plain-master
    ::xo::cc set_parameter MenuBar 0
    
    if {[llength $filter_form_ids] > 0} {
      #
      # Filter by questions. For the time being, we allow only a
      # single question, ... and we take the first ones.
      #
      append HTML "<h2>#xowf.question#: [ns_quotehtml [[lindex $filter_form_ids 0] title]]</h2>\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 "<h2>#xowf.online-exam-review-protocol# - $heading</h2>\n"
      set runtime_panel_view "student"
    
    } else {
      #
      # Provide the full protocol (or a subset of it)
      #
      append HTML "<h2>#xowf.online-exam-protocol#</h2>\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 {
        <div class=''main-content>
        <div class='xowiki-content' style='padding-left:15px;'>
        <h1>[ns_quotehtml $examTitle]</h1>
        [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 <p>[:grading_table -csv ${:grade_csv} ${:grade_dict}]</p>
      #
      # The following lines are convenient for debugging
      #
      #set manual_gradings [$examWf property manual_gradings]
      #set manual_gradings [:get_exam_results -obj $examWf manual_gradings]
      #append HTML <pre>$manual_gradings</pre>
      #append HTML <pre>[:exam_results -manual_gradings $manual_gradings $results]</pre>
    }
    
    if {$create_zip_file} {
      $zipFile ns_returnZipFile [$zipFile cget -name].zip
      $zipFile destroy
      ad_script_abort
    }
    
    #
    # If we have already some file submission we are showing a link
    # for bulk-downloading the submissions
    #
    if {$file_submission_exists} {
      #
      # Avoid empty entries for query parameters
      #
      if {[llength $filter_form_ids] > 0} {
        set fos $filter_form_ids
      }
      foreach value {revision_id filter_submission_id} var {rid id} {
        if {[set $value] ne ""} {
          set $var [set $value]
        }
      }
      set href [$examWf pretty_link -query [export_vars {
        {m print-answers} {create-file-submission-zip-file 1}
        fos rid id
      }]]
      append HTML [ns_trim -delimiter | [subst {
        |<a href='[ns_quotehtml $href]'>
        |[::xowiki::bootstrap::icon -name download -CSSclass download-submissions]
        |#xowf.Download_file_submissions#</a>
      }]]
    }
    #ns_log notice "RENDER ANSWERS 5"
    
    #
    # Store statistics only in autograding cases, and only, when it
    # was a full evaluation of the exam. This has the advantage
    # that we do no have to partially update the statistics. These
    # are somewhat overly conservative assumptions for now, which
    # might be partially relaxed in the future.
    #
    if {$with_grading_table
        && !$as_student && $filter_submission_id eq "" && $creation_user eq "" && $revision_id eq ""
      } {
      set statistics {}
      set ia [$examWf instance_attributes]
      if {$autograde} {
        foreach var {__stats_success __stats_count} key {success count} {
          if {[$examWf exists $var]} {
            dict set statistics $key [$examWf set $var]
            $examWf unset $var
          }
        }
        :AM set_exam_results -obj $examWf statistics $statistics
      }
      :AM set_exam_results -obj $examWf results $results
    }
    
    return [list do_stream $do_stream HTML $HTML]
  • render_answers_with_edit_history (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> render_answers_with_edit_history \
        examWf

    Analyze the student submissions an find situations, where input is "cleared" between revisions and return the HTML rendering. TODO: we should resolve this, move the exam protocol rendering (www-print-answers) also into the answer manager and make it configurable to provide this as an alternate item renderer. The current result is provided for all submission,s, but in general, this could be as well made available per question or per-student.

    Parameters:
    examWf (required, object)
    Returns:
    HTML

    Testcases:
    No testcase defined.
    set wf [:get_answer_wf $examWf]
    if {$wf eq ""} {
      return ""
    }
    
    set submissions [:submissions -wf $wf]
    set HTML [:render_submissions=edit_history -examWf $examWf -submissions $submissions]
    
    return $HTML
  • results_table (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> results_table \
        [ -package_id package_id ] -items items  \
        [ -view_all_method view_all_method ] [ -with_answers on|off ] \
        [ -state state ] [ -grading_scheme grading_scheme ] wf

    Render the results in format of a table and return HTML. Currently mostly deactivated (but potentially called by online-exam.wf and topic-assignment.wf).

    Switches:
    -package_id (optional, integer)
    -items (required, object)
    -view_all_method (optional, defaults to "print-answers")
    -with_answers (optional, boolean, defaults to "true")
    -state (optional, defaults to "done")
    -grading_scheme (optional, defaults to "::xowf::test_item::grading::none")
    Parameters:
    wf (required, object)

    Testcases:
    No testcase defined.
    #set form_info [:combined_question_form -with_numbers $wf]
    set form_info [:QM combined_question_form $wf]
    set answer_form_field_objs [:answer_form_field_objs -wf $wf $form_info]
    set autograde [dict get $form_info autograde]
    
    #if {$autograde && [llength $answer_form_field_objs] > 10} {
    #  set with_answers 0
    #}
    
    set form_field_objs {}
    lappend form_field_objs  [$wf create_raw_form_field  -name _online-exam-userName  -spec text,label=#xowf.participant#]
    
    if {$with_answers} {
      #
      # Create for every answer field a matching grading field
      #
      set ff_dict {}
      foreach answer_field_obj $answer_form_field_objs {
        #ns_log notice "LABEL [$answer_field_obj name] <[$answer_field_obj label]>"
        $answer_field_obj label [string trimright [$answer_field_obj name] _]
        $answer_field_obj mixin ::xowf::test_item::td_pretty_value
    
        set grading_field_obj [$wf create_raw_form_field  -name [$answer_field_obj name].score  -spec number,label=#xowf.Grading-Score#]
        lappend form_field_objs  $answer_field_obj  $grading_field_obj
        dict set ff_dict [$answer_field_obj name] $answer_field_obj
        dict set ff_dict [$grading_field_obj name] $grading_field_obj
      }
    }
    
    # if {0 && $autograde} {
    #   lappend form_field_objs  #       [$wf create_raw_form_field  #          -name _online-exam-total-score  #          -spec number,label=#xowf.Total-Score#]  #       [$wf create_raw_form_field  #            -name _online-exam-grade  #            -spec number,label=#xowf.Grade#]
    # }
    
    lappend form_field_objs  [$wf create_raw_form_field  -name _online-exam-seconds  -spec number,label=#xowf.Seconds#]  [$wf create_raw_form_field  -name _creation_date  -spec date,label=#xowiki.Page-last_modified#]
    
    #
    # Check, if any of the answer form field objects is
    # randomized. If so, it is necessary to recreate these eagerly,
    # since the full object structure might be personalized.
    #
    set randomized_fields {}
    foreach ff_obj $answer_form_field_objs {
      if {[$ff_obj exists shuffle_kind] && [$ff_obj shuffle_kind] ne "none"} {
        lappend randomized_fields $ff_obj
      }
    }
    
    #
    # Take "orderby" from the query parameter. If not set, order by
    # the first field.
    #
    set orderby [::$package_id query_parameter orderby:token ""]
    if {$orderby eq "" && [llength $form_field_objs] > 0} {
      set orderby [[lindex $form_field_objs 0] name],asc
    }
    
    #
    # Create table widget.
    #
    set table_widget [::xowiki::TableWidget create_from_form_fields  -package_id $package_id  -form_field_objs $form_field_objs  -orderby $orderby]
    #
    # Extend properties of every answer with corresponding ".score"
    # values.
    #
    foreach p [$items children] {
      #
      # If we have randomized fields, we have to
      # recreate/reinitialize these to get proper correction
      # markings for this user. It might be possible to optimize
      # this, when only a few fields are randomized.
      #
      if {[llength $randomized_fields] > 0} {
        #ns_log notice "WORK ON [$p creation_user] "
        :answer_form_field_objs -clear -wf $wf $form_info
        $wf form_field_flush_cache
        xo::cc eval_as_user -user_id [$p creation_user] {
          set answer_form_field_objs [:answer_form_field_objs -wf $wf $form_info]
        }
      }
    
      set total_score 0
      set total_points 0
      foreach ff_obj $answer_form_field_objs {
        $ff_obj object $p
        set property [$ff_obj name]
        $ff_obj value [$p property $property]
    
        $ff_obj set_feedback 3
    
        #ns_log notice "[$p creation_user] [$ff_obj name] [$p property $property] -> [$ff_obj set evaluated_answer_result]"
        set r [expr {[$ff_obj exists grading_score] ? [$ff_obj set grading_score] : ""}]
        #
        # In case, we have a grading score, which is not starred, we
        # can compute points from this.
        #
        if {$r ne "" && ![regexp {[*]$} $r]} {
          #
          # Add exercise score weighted to the total score to
          # compute points.
          #
          if {[$ff_obj exists test_item_points]} {
            #ns_log notice "[$ff_obj name]: grading_score <$r>, test_item_points <[$ff_obj set test_item_points]>"
    
            set minutes [$ff_obj set test_item_points]
            set total_score [expr {$total_score + ($minutes * [$ff_obj set grading_score])}]
            set total_points [expr {$total_points + $minutes}]
          }
          #ns_log notice "==== [$ff_obj name] grading_score => $r"
        } else {
          set r [expr {[$ff_obj set evaluated_answer_result] eq "correct" ? 100.0 : 0.0}]*
          #ns_log notice [$ff_obj serialize]
        }
        $p set_property -new 1 $property.score $r
      }
    
      set duration [:get_duration [$p get_revision_sets]]
      $p set_property -new 1 _online-exam-seconds [dict get $duration seconds]
    
      # if {0 && $autograde && $total_points > 0} {
      #   set final_score [expr {$total_score/$total_points}]
      #   $p set_property -new 1 _online-exam-total-score $final_score
      #
      #   set d [list achievedPoints $total_score achievablePoints $total_points totalPoints $total_points]
      #   set grade [$grading_scheme grade -achieved_points $d]
      #   dict incr grade_count $grade
      #   $p set_property -new 1 _online-exam-grade $grade
      # }
    }
    
    if {$state eq "done"} {
      set uc {tcl {[$p state] ne "done"}}
    } else {
      set uc {tcl {false}}
    }
    
    #
    # Render table widget with extended properties.
    #
    set HTML [$table_widget render_page_items_as_table  -package_id $package_id  -items $items  -form_field_objs $form_field_objs  -csv true  -uc $uc  -view_field _online-exam-userName  -view_filter_link [$wf pretty_link -query m=$view_all_method]  {*}[expr {[info exists generate] ? [list -generate $generate] : ""}]  -return_url [ad_return_url]  -return_url_att local_return_url  ]
    $table_widget destroy
    
    if {0 && $autograde} {
      set gradingTable {<div class="table-responsive"><table class="table">}
      append gradingTable  "<thead><th class='text-right col-md-1'>#xowf.Grade#</th><th class='col-md-1 text-right'>#</th></thead>"  "<tbody>\n"
      set nrGrades 0
      foreach v [dict values $grade_count] { incr nrGrades $v}
      foreach k [lsort [dict keys $grade_count]] {
        set count [dict get $grade_count $k]
        set countPercentage [expr {$count*100.0/$nrGrades}]
        append gradingTable  <tr>  [subst {<td class="text-right">$k</td><td class="text-right">$count</td>}]  [subst {<td><div class="progress"><div class="progress-bar"
              style="width:$countPercentage%">$countPercentage%</div></td}]  </tr>\n
      }
      append gradingTable "</tbody></table></div>\n"
      append HTML <p>$gradingTable</p>
    }
    return $HTML
  • revisions_up_to (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> revisions_up_to \
        revision_sets revision_id

    Return the revisions of the provided revision set up the provided revision_id. If this revision_id does not exist, return the full set.

    Parameters:
    revision_sets (required)
    revision_id (required)

    Testcases:
    No testcase defined.
    set result ""
    set stop 0
    return [lmap s $revision_sets {
      if {$stopbreak
      set stop [expr {[ns_set get $s revision_id] eq $revision_id}]
      set s
    }]
  • runtime_panel (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> runtime_panel \
        [ -revision_id revision_id ] [ -view view ] \
        [ -grading_info grading_info ] answerObj

    Return statistics for the provided object in the form of HTML: - minimal statistics: when view default - statistics with clickable revisions: when view = revision_overview - per-revision statistics: when view = revision_overview and revision_id is provided

    Switches:
    -revision_id (optional)
    -view (optional, defaults to "default")
    -grading_info (optional)
    Parameters:
    answerObj (required, object)
    Returns:
    HTML block

    Testcases:
    No testcase defined.
    set revision_sets [$answerObj get_revision_sets]
    set parent_revision_sets [[$answerObj parent_id] get_revision_sets]
    set item_id [$answerObj item_id]
    set live_revision_id [xo::dc get_value -prepare integer live_revision_id {
      select live_revision from cr_items where item_id = :item_id
    }]
    set current_question [expr {[dict get [$answerObj instance_attributes] position] + 1}]
    set page_info "#xowf.question#: $current_question"
    
    if {$view eq "default"} {
      set url [ad_return_url]&id=$item_id
      set revisionDetails "#xowf.nr_changes#: <a href='[ns_quotehtml $url]'>[llength $revision_sets]</a><br>"
    } elseif {$view eq "student"} {
      set revisionDetails ""
    } elseif {$view eq "revision_overview"} {
      set displayed_revision_info ""
      set live_revision_info ""
      set make_live_info ""
    
      set baseUrl [ns_conn url]
      set filtered_revision_sets [:revisions_up_to $revision_sets $revision_id]
      set c 0
    
      foreach s $revision_sets {
        set rid [ns_set get $s revision_id]
        incr c
        if {$rid == $live_revision_id} {
          set liveCSSclass "live"
          set live_revision_info "#xowf.Live_revision#: $c"
        } else {
          set liveCSSclass "other"
        }
        set revision_url $baseUrl?[::xo::update_query [ns_conn query] rid $rid]
        if {$rid == [$answerObj revision_id]} {
          set suffix "*"
          set displayed_revision_info "#xowf.Displayed_revision#: $c"
    
          if {$rid ne $live_revision_id} {
            set query [::xo::update_query [ns_conn query] m make-live-revision]
            set query [::xo::update_query $query revision_id $rid]
            set query [::xo::update_query $query local_return_url [ad_return_url]]
            set live_revision_link $baseUrl?$query
            set make_live_info [subst {
              <a class="button" href="[ns_quotehtml $live_revision_link]">#xowf.Make_live_revision#</a>
            }]
            lappend revision_list "<span class='current'>$c</span>"
          } else {
            lappend revision_list "<span class='$liveCSSclass'>$c</span>"
          }
        } else {
          lappend revision_list [subst {
            <a class="$liveCSSclass" title="#xowf.Goto_this_revision#" href="[ns_quotehtml $revision_url]">$c</a>
          }]
        }
      }
      set revision_sets $filtered_revision_sets
      set revisionDetails [subst {#xowiki.revisions#: [join $revision_list {, }]
        <div class="revision-details right">$displayed_revision_info<br>$live_revision_info<br>
        $make_live_info
        </div>
        <br>
      }]
    }
    if {$revision_id eq ""} {
      set revision_sets [:revisions_up_to $revision_sets $live_revision_id]
    }
    set toClock [clock scan [::xo::db::tcl_date [ns_set get [lindex $revision_sets end] last_modified] tz]]
    set last_published [:last_time_switched_to_state $parent_revision_sets -state published -before $toClock]
    #ns_log notice "LAST PUBLISHED $last_published"
    set duration [:get_duration -exam_published_time $last_published $revision_sets]
    
    set state [$answerObj state]
    if {$state eq "done"} {
      set submission_info "#xowf.submitted#"
    } else {
      set submission_info "#xowf.not_submitted# ($page_info)"
    }
    
    if {[dict exists $duration examPublished]} {
      set publishedInfo "#xowf.Exam_published#: <span class='data'>[dict get $duration examPublished]</span><br>"
      set extraDurationInfo " - #xowf.since_published#: [dict get $duration examPublishedDuration]"
    } else {
      set publishedInfo ""
      set extraDurationInfo ""
    }
    if {$view eq "student"} {
      set IPinfo ""
      set statusInfo ""
      set extraDurationInfo ""
      set publishedInfo ""
    } else {
      set IPinfo [subst {IP: <span class="data">[:get_IPs $revision_sets]</span>}]
      set statusInfo "#xowf.Status#: <span class='data'>$submission_info</span><br>"
    }
    
    if {$grading_info ne ""} {
      set achievedPointsInfo [subst {
        #xowf.Achieved_points#: <span class='data achieved-points'>$grading_info</span><br>
      }]
    } else {
      set achievedPointsInfo ""
    }
    set HTML [subst {
      $publishedInfo
      $revisionDetails
      $statusInfo
      #xowf.Duration#: <span class="data">[dict get $duration from] - [dict get $duration to]
      ([dict get $duration duration]$extraDurationInfo)</span><br>
      $achievedPointsInfo
      $IPinfo
    }]
    return $HTML
  • set_exam_results (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> set_exam_results \
        -obj obj  property value

    ns_log notice "SES '$property' bytes [string length $value]"

    Switches:
    -obj (required, object)
    Parameters:
    property (required)
    value (required)

    Testcases:
    No testcase defined.
    set p [$obj childpage -name en:result -form inclass-exam-statistics.wf]
    set instance_attributes [$p instance_attributes]
    dict set instance_attributes $property $value
    $p update_attribute_from_slot [$p find_slot instance_attributes] ${instance_attributes}
    #
    # cleanup of legacy values
    #
    set instance_attributes [$obj instance_attributes]
    foreach property_name [list $property __$property] {
      if {[dict exists $instance_attributes $property_name]} {
        ns_log notice "SES set_exam_results:"  "clearing values from earlier releases for '$property_name'"  "was <[dict get $instance_attributes $property_name]>"
        dict unset instance_attributes $property_name
        $obj set instance_attributes $instance_attributes
        #ns_log notice "FINAL IA <$instance_attributes> for item_id [$obj item_id]"  "revision_id [$obj revision_id]"
        $obj update_attribute_from_slot [$obj find_slot instance_attributes] $instance_attributes
        ::xo::xotcl_object_cache flush [$obj item_id]
        ::xo::xotcl_object_cache flush [$obj revision_id]
      }
    }
  • state_periods (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> state_periods \
        -state state  revision_sets

    Return for the provided revision_sets the time ranges the workflow was in the provided state.

    Switches:
    -state (required)
    Parameters:
    revision_sets (required)

    Testcases:
    No testcase defined.
    set periods ""
    set from ""
    set last_from ""
    set until ""
    foreach ps $revision_sets {
      set current_state [ns_set get $ps state]
      if {$state eq $current_state} {
        if {$until ne ""} {
          lappend periods [:pretty_period $last_from $until]
        }
        set from [ns_set get $ps creation_date]
        set until ""
      } elseif {$until eq "" && $current_state ne $state && $from ne ""} {
        set until [ns_set get $ps last_modified]
        set last_from $from
        set from ""
      }
    }
    if {$until ne ""} {
      lappend periods [:pretty_period $last_from $until]
    } elseif {$from ne ""} {
      lappend periods [:pretty_period $from ""]
    }
    #ns_log  notice "state_periods $state <$from> <$last_from> <$until> <$periods>"
    return $periods
  • student_submissions_exist (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> student_submissions_exist \
        wf

    Returns 1 if there are student submissions. The method returns already true, when a student has started to work on this exam. This method could be optimized if necessary via caching the wf_instances or a more specific database query.

    Parameters:
    wf (required, object)

    Testcases:
    No testcase defined.
    set items [:get_wf_instances $wf]
    foreach i [$items children] {
      if {[$i property try_out_mode] ne "1"} {
        #ns_log notice "==================== student_submissions_exist 1"
        return 1
      }
    }
    #ns_log notice "==================== student_submissions_exist 0"
    return 0
  • time_window_setup (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> time_window_setup \
        -time_window time_window  parentObj

    Check the provided time_window values, adjust it if necessary, and make sure, according atjobs are provided. This method was made public, since there configuration window update in inclass-exam.wf requires this for the update via update_attribute_from_slot. Probably, we should move the core of this function to this file, and make it protected again.

    Switches:
    -time_window (required)
    Parameters:
    parentObj (required, object)

    Testcases:
    No testcase defined.
    set dtstart [dict get $time_window time_window.dtstart]
    set dtend [dict get $time_window time_window.dtend]
    
    #
    # Delete previously scheduled atjobs. This needs to happen in
    # any case, because people may have removed the time window
    # altogether.
    #
    ns_log notice "#### deleting old scheduled atjob"
    :delete_scheduled_atjobs $parentObj
    
    if {$dtstart ne ""} {
      set total_minutes [question_manager total_minutes_for_exam -manager $parentObj]
      ns_log notice "#### create_workflows: atjobs for time_window <$time_window> total-mins $total_minutes"
      set start_clock [clock scan $dtstart -format %Y-%m-%dT%H:%M]
    
      if {$dtend eq ""} {
        #
        # No end given. Set it to start + exam time + 5 minutes.
        # The value of "total_minutes" might contain fractions of a
        # minute, so make sure that the end_clock is an integer as
        # needed by "clock format",
        set end_clock [expr {int($start_clock + ($total_minutes + 5) * 60)}]
        set new_dtend [clock format $end_clock -format %H:%M]
        ns_log notice "#### no dtend given. set it from $dtend to $new_dtend"
    
      } else {
        set end_date    [clock format $start_clock -format %Y-%m-%d]T$dtend
        set end_clock   [clock scan $end_date      -format %Y-%m-%dT%H:%M]
        if {($end_clock - $start_clock) < ($total_minutes * 60)} {
          #
          # The specified end time is too early. Set it to start +
          # exam time + 5 minutes.
          #
          set end_clock [expr {int($start_clock + ($total_minutes + 5)*60)}]
          set new_dtend [clock format $end_clock -format %H:%M]
          ns_log notice "#### dtend is too early. Move it from $dtend to $new_dtend"
    
        } else {
          set new_dtend $dtend
        }
      }
    
      if {$new_dtend ne $dtend} {
        ns_log notice "#### create_workflows: must change dtend from <$dtend> to <$new_dtend>"
        set ia [$parentObj instance_attributes]
        dict set time_window time_window.dtend $new_dtend
        dict set ia time_window $time_window
        #ns_log notice "SAVE updated ia <${:instance_attributes}>"
        $parentObj update_attribute_from_slot [$parentObj find_slot instance_attributes] $ia
      }
    
      #
      # Schedule new atjobs
      #
      ns_log notice "#### scheduling atjobs"
      $parentObj schedule_action  -time [clock format $start_clock -format "%Y-%m-%d %H:%M:%S"]  -action publish
      $parentObj schedule_action  -time [clock format $end_clock -format "%Y-%m-%d %H:%M:%S"]  -action unpublish
    }
  • waiting_room_message (scripted, public)

     <instance of xowf::test_item::Answer_manager[i]> waiting_room_message \
        obj

    Renders the waiting room message, including the JavaScript reacting to actions from the backend.

    Parameters:
    obj (required, object)

    Testcases:
    No testcase defined.
    set message [::xowiki::bootstrap::card  -title #xowf.Waiting_Room#  -body [subst {
                       <p>[_ xowf.waiting_for_exam [list title "[$obj title]"]]
                       <p><adp:icon name='clock'>  <span id='waiting-msg'></span></p>
                       #xowf.waiting_redirect#
                     }]]
    
    set url [$obj pretty_link -query m=poll-open]
    template::add_body_script -script [subst -nocommands {
      (function poll() {
        setTimeout(function() {
          var xhttp = new XMLHttpRequest();
          xhttp.open("GET", '$url', true);
          xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
              var data = JSON.parse(xhttp.response);
              console.log(data);
              console.log(data["action"]);
              console.log(data["msg"]);
              if (data["action"] == "msg") {
                var el = document.querySelector('#waiting-msg');
                el.innerHTML = data["msg"];
                poll();
              } else if (data["action"] == "redirect") {
                window.location.href =  data["url"];
              } else {
                console.log("something else");
              }
            }
          };
          xhttp.send();
        }, 1000);
      })();
    }]
    return $message