# -*- 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# \ -proc activate {obj} {:set_page $obj -1} Action nextQuestion \ -next_state working \ -label #xowf.next_question# \ -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# \ -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 {]*>} $summary_form {} summary_form :set_title $obj return [::xowiki::Form new \ -destroy_on_cleanup \ -name en:summary \ -title $form_title \ -form [list
$summary_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: