online-exam-answer.wf

Delivered as */*

[ hide source ] | [ make this the default ]

File Contents

# -*- Tcl -*-
#
# Workflow template for answering online exams. The workflow is
# typically controlled from a parent workflow that a teacher can use
# to create the exam, to try it out and to publish it
# (online-exam.wf).
#
# This workflow is based on the test-item infrastructure using
# the "renaming_form_loader" and "question_manager".
#

set :autoname 1   ;# to avoid editable name field
set :policy ::xowf::test_item::test-item-policy-answer
set :debug 0

########################################################################
#
# Properties
#
# position: the current page in the exam
# return_url: when the exam is finished, the user proceeds to this url
# try_out_mode: a teacher can try the exam in this mode
# ip: IP address of the user, kept in the instance attribute for auditing
#
########################################################################

Property position -default 0
Property return_url -default "" -allow_query_parameter true
Property try_out_mode -default 0 -allow_query_parameter true

########################################################################
#
# Action definitions
#
########################################################################

Action allocate -proc activate {obj} {
  # Called, when we try to create or use a workflow instance
  # via a workflow definition ($obj is a workflow definition)
  set parent_id [$obj parent_id]
  set name [ns_md5 $parent_id-[::xo::cc set untrusted_user_id]]
  set parent_obj [::xo::db::CrClass get_instance_from_db -item_id $parent_id]
  :payload [list title [$parent_obj title] name $name]
}

Action initialize -proc activate {obj} {
  # called, after workflow instance was created

  # make sure to create the parent (the controlling workflow)
  set parent_obj [::xo::db::CrClass get_instance_from_db -item_id [$obj parent_id]]
  set parent_state [$parent_obj state]

  #
  # Don't allow one to enter values when the state of the parent
  # workflow is not published (the teacher has not published the exam,
  # or closed it already). Only allow usage in the try-out-mode.
  #
  if {$parent_state ne "published" && [$obj property try_out_mode 0] == 0} {
    set current_state [$obj property _state]
    set locking_state [expr {$current_state eq "initial" ? "initial" : "done"}]
    set locking_msg(initial) "#xowf.online-exam-not-published#"
    set locking_msg(done) "#xowf.online-exam-finished#"

    util_user_message -message $locking_msg($locking_state)
    #
    # Force the user in the done state. Alternatively, we could
    # handle this in the provide a different form or push the user to some other state.
    #
    [:wf_context] set_current_state $locking_state

  } else {
    #:msg "not LOCKED"
  }
}

Action instproc goto_page {position} {
  :set_property position $position
}
Action instproc set_page {obj increment} {
  set parent_obj [::xo::db::CrClass  get_instance_from_db -item_id [$obj parent_id]]
  set pages [::xowf::test_item::question_manager question_names $parent_obj]
  set position [:property position 0]
  incr position $increment
  if {$position < 0} {
    set position 0
  } elseif {$position >= [llength $pages]} {
    set position [expr {[llength $pages] - 1}]
  }
  :goto_page $position
}

Action prevQuestion \
    -next_state working \
    -label #xowf.previous_question# \
    -title #xowf.previous_question_title# \
    -proc activate {obj} {:set_page $obj -1}

Action nextQuestion \
    -next_state working \
    -label #xowf.next_question# \
    -title #xowf.next_question_title# \
    -proc activate {obj} {:set_page $obj 1}

Action review \
    -next_state done \
    -label #xowf.online-exam-review# \
    -proc activate {obj} {
      [[$obj wf_context ] wf_container] addSignature $obj
    }

Action save \
    -label #xowf.online-exam-save#

Action logout \
    -label #xowf.online-exam-submit# \
    -proc activate {obj} {
      [[$obj wf_context ] wf_container] addSignature $obj
      set try_out_mode [$obj property try_out_mode 0]
      set return_url [$obj property return_url .]
      #:msg "tryout $try_out_mode return_url $return_url"
      if {$try_out_mode} {
        ad_returnredirect $return_url
        ad_script_abort
      } else {
        ::xo::cc set_parameter return_url /register/logout?return_url=$return_url
      }
    }

Action start \
    -next_state working \
    -label #xowf.online-exam-start# \
    -proc activate {obj} {
      $obj set_property position 0
    }

Action start_again \
    -label #xowf.first_question# \
    -title #xowf.first_question_title# \
    -next_state working -proc activate {obj} {
      $obj set_property position 0
    }

########################################################################
#
# State definitions
#
########################################################################

State parameter {
  {view_method edit}
  {extra_js {
    urn:ad:js:jquery
    ../file:seal.js?m=download
  }}
  {extra_css {
    /resources/xowf/test-item.css
  }}
}

State working \
    -form_loader working_form_loader

State initial \
    -actions {start logout} \
    -form "../en:exam-start"

State done \
    -form "../en:exam-done" \
    -form_loader summary_form


########################################################################
#
# Helper methods for the workflow container
#
########################################################################

#
# Field-renaming form loader
#
proc working_form_loader {ctx form_name} {
  set obj [$ctx object]
  set item_nr [$obj property position]

  set parent_id [$obj parent_id]
  #:msg "working_form_loader [$obj instance_attributes]"
  set parent_obj [::xo::db::CrClass get_instance_from_db -item_id $parent_id]
  set parent_state [$parent_obj state]

  #
  # In case shuffling is required, fetch via the shuffled position.
  #
  set shuffle_id [expr {[$parent_obj property shuffle_items 0] ? [$obj creation_user] : -1}]
  set position [::xowf::test_item::question_manager shuffled_index \
                    -shuffle_id $shuffle_id \
                    $parent_obj $item_nr]
  #
  # Load the form.
  #
  set form_obj [::xowf::test_item::question_manager nth_question_obj $parent_obj $position]

  #
  # Update IP address each time the form is loaded.
  #
  if {[$obj state] in {"initial" "working"}} {
    $obj set_property ip [expr {[ns_conn isconnected] ? [ad_conn peeraddr] : "nowhere"}]
  }

  #
  # Update the title of the page
  #
  :set_title $obj -position $position -item_nr $item_nr -for_question -with_minutes

  return $form_obj
}

#
# Set "title" with question/user/IP information. Note that the
# "set_title" method is as well responsible for calling the rename
# function via question_manager.
#
:proc set_title {
  obj
  -position:integer
  -item_nr:integer
  {-for_question:switch false}
  {-with_minutes:switch false}
} {
  set parent_obj [::xo::db::CrClass get_instance_from_db -item_id [$obj parent_id]]
  if {$for_question && [$obj state] eq "working"} {
    set form_info [::xowf::test_item::question_manager nth_question_form \
                       -with_numbers \
                       -with_title \
                       -with_minutes=$with_minutes \
                       -position $position \
                       -item_nr $item_nr \
                       $parent_obj]
    set title_info [lindex [dict get $form_info title_infos] 0]
    set titleString [dict get $title_info full_title]
    set title [list [string trim $titleString]]
  }
  lappend title \
      [$parent_obj title] \
      "IP: [$obj property ip]"
  #ns_log notice "SETTING $obj title [join $title { · }]"
  $obj title [join $title " · "]

  #:msg set_title-set_parameter-MenuBar-[$obj state]
  ::xo::cc set_parameter MenuBar 0
  ::xo::cc set_parameter template_file view-plain-master
}

#
# Form loader for summary (shows all submission data of a user)
#
# This form loader is also called indirectly by www-print-answers of
# oneline-exam.wf
#
:proc summary_form {ctx form_title} {
  set obj [$ctx object]
  set parent_obj [::xo::db::CrClass  get_instance_from_db -item_id [$obj parent_id]]
  #:msg "summary_form_loader $form_title [$obj instance_attributes]"

  set shuffle_id [expr {[$parent_obj property shuffle_items 0] ? [$obj creation_user] : -1}]
  set form_info [::xowf::test_item::question_manager combined_question_form \
                     -with_numbers \
                     -with_title \
                     -with_minutes \
                     -shuffle_id $shuffle_id \
                     $parent_obj]

  set summary_form [dict get $form_info form]
  set summary_fc [dict get $form_info disabled_form_constraints]
  regsub -all {</?form[^>]*>} $summary_form {} summary_form

  :set_title $obj

  return [::xowiki::Form new \
              -destroy_on_cleanup \
              -name en:summary \
              -title $form_title \
              -form [list <form><div>$summary_form</div></form> text/html] \
              -text {} \
              -anon_instances t \
              -form_constraints $summary_fc]
}

:proc addSignature {obj} {
  set answerAttributes [xowf::test_item::renaming_form_loader \
                            answer_attributes [$obj instance_attributes]]
  set sha256 [ns_md string -digest sha256 $answerAttributes]
  $obj set_property -new true signature $sha256
  return $sha256
}


########################################################################
#
# Object specific operations
#
########################################################################

:object-specific {
  #
  # Ensure default value is updated for each instance individually.
  #
  set ctx [:wf_context]
  set container [$ctx wf_container]
  ${container}::Property ip -default [expr {[ns_conn isconnected] ? [ad_conn peeraddr] : "nowhere"}]

  set ctx [:wf_context]
  set container [$ctx wf_container]
  if {$ctx ne $container} {
    $ctx forward working_form_loader $container %proc $ctx
    $ctx forward summary_form $container %proc $ctx
  }
  set :policy ::xowf::test_item::test-item-policy1

  if {${:state} in {working done}} {
    set parent_obj [::xo::db::CrClass  get_instance_from_db -item_id [:parent_id]]
    set question_names [::xowf::test_item::question_manager question_names $parent_obj]

    #
    # Use the current_position in the sense of the nth question of the
    # user, which is not necessarily the nth question in the list of
    # questions due to shuffling.
    #
    set current_position [:property position]

    set actions {}
    if {$current_position > 0 && ${:state} eq "working"} {
      lappend actions prevQuestion
    }
    set count 0
    foreach question $question_names {
      incr count
      ${container}::Action create ${container}::q.$count \
          -label "$count" \
          -next_state working \
          -extra_css_class [expr {$current_position == $count - 1 ? "current" : ""}] \
          -proc activate {obj} \
          [list :goto_page [expr {$count -1}]]
      lappend actions q.$count
    }
    if { ${:state} eq "working"
         && [::xowf::test_item::question_manager more_ahead -position $current_position $parent_obj]
       } {
      lappend actions nextQuestion
    }
    if {${:state} eq "working" } {
      lappend actions save review
    } else {
      lappend actions logout
    }
    ${container}::${:state} actions $actions
  }
}

#
# Local variables:
#    mode: tcl
#    tcl-indent-level: 2
#    indent-tabs-mode: nil
# End: