online-exam.wf

Delivered as */*

[ hide source ] | [ make this the default ]

File Contents

# -*- Tcl -*-
########################################################################
# Online-Exam workflow
# ====================
#
# Defining exams: This workflow lets a teacher choose from a
# predefined set of exam questions, which are typically open text,
# short text, single or multiple choice questions.  The teacher
# selects test questions via drag and drop. The teacher can perform a
# test run of the created exam, and can get the results via a result
# table.
#
# Publishing and closing exams: When a teacher is satisfied with the
# exam, the exam can be published. In this step, all answers of the
# testing phase are deleted. In the process of publishing, the link to
# start the exam is offered to the user.  When the exam is published,
# the teacher can see the incoming answers in the report by refreshing
# the page. When the exam is done, it is unpublished. The workflow
# offers the teacher to see a summary of the results in form of a
# table (an to download the results via csv), or the teacher can
# produce a printer friendly version of the answers.
#
# An admin might with to add the following entries to the folder to ease
# creation of exercises and exams
#
#   {clear_menu -menu New}
#
#   {entry -name New.Item.TextInteraction -form en:edit-interaction.wf -query p.item_type=Text}
#   {entry -name New.Item.ShortTextInteraction -form en:edit-interaction.wf -query p.item_type=ShortText}
#   {entry -name New.Item.SCInteraction -form en:edit-interaction.wf -query p.item_type=SC}
#   {entry -name New.Item.MCInteraction -form en:edit-interaction.wf -query p.item_type=MC}
#   {entry -name New.Item.ReorderInteraction -form en:edit-interaction.wf -query p.item_type=Reorder}
#   {entry -name New.Item.UploadInteraction -form en:edit-interaction.wf -query p.item_type=Upload}
#
#   {entry -name New.App.Exam -label "Online Exam" -form en:online-exam.wf}
#
# The policy has to allow the following methods on FormPages:
#
#  - "answer" (for students),
#  - "edit" (for students),
#  - "poll" (for teachers),
#  - "print-answers" (for teachers),
#  - "print-answer-table" (for teachers),
#  - "delete" (for teachers),
#
# Gustaf Neumann, Feb 2012
########################################################################
set :autoname 1   ;# to avoid editable name field
set :policy ::xowf::test_item::test-item-policy-publish
set :debug 0
set :live_updates 1

set :fc_repository {
  {countdown_audio_alarm:boolean,horizontal=true,default=t,label=#xowf.Countdown_audio_alarm#,help_text=#xowf.Countdown_audio_alarm_help_text#}
  {shuffle_items:boolean,horizontal=true,label=#xowf.randomized_items#,help_text=#xowf.randomized_items_help_text#}
  {max_items:number,min=1,label=#xowf.Max_items#,help_text=#xowf.Max_items_help_text#}
  {allow_paste:boolean,horizontal=true,default=t,label=#xowf.Allow_paste#,help_text=#xowf.Allow_paste_help_text#}
  {allow_spellcheck:boolean,horizontal=true,default=t,label=#xowf.Allow_spellcheck#,help_text=#xowf.Allow_spellcheck_help_text#}
  {allow_translation:boolean,horizontal=true,default=f,label=#xowf.Allow_translation#,help_text=#xowf.Allow_translation_help_text#}
  {show_minutes:boolean,horizontal=true,default=t,label=#xowf.Show_minutes#,help_text=#xowf.Show_minutes_help_text#}
  {show_points:boolean,horizontal=true,default=t,label=#xowf.Show_points#,help_text=#xowf.Show_points_help_text#}
  {show_ip:boolean,horizontal=true,default=t,label=#xowf.Show_IP#,help_text=#xowf.Show_IP_help_text#}
  {time_budget:range,default=100,min=100,max=300,step=5,with_output=t,form_item_wrapper_CSSclass=form-inline,output_suffix=%,label=#xowf.Time_budget#,help_text=#xowf.Time_budget_help_text#}
  {synchronized:boolean,horizontal=true,default=f,label=#xowf.Synchronized#,help_text=#xowf.Synchronized_help_text#}
  {time_window:time_span,label=#xowf.Exam_time_window#,help_text=#xowf.Exam_time_window_help_text#}
  {proctoring:boolean,horizontal=true,default=f,label=#xowf.Proctoring#,help_text=#xowf.Proctoring_help_text#}
  {proctoring_options:checkbox,horizontal=true,options={Desktop d} {Camera c} {Audio a} {Statement s},default=d c a s,label=#xowf.Proctoring_options#,help_text=#xowf.Proctoring_options_help_text#,swa?:disabled=1}
  {proctoring_record:boolean,horizontal=true,default=t,label=#xowf.Proctoring_record#,help_text=#xowf.Proctoring_record_help_text#}
  {signature:boolean,horizontal=true,default=f,label=#xowf.Signature#,help_text=#xowf.Signature_help_text#}
  {grading:grading_scheme,required,default=none,label=#xowf.Grading_scheme#,help_text=#xowf.Grading_scheme_help_text#}
}


Action select -next_state created -label #xowf.online-exam-select# \
    -title #xowf.online-exam-title-select#
Action publish -next_state published -label #xowf.online-exam-publish# \
    -title #xowf.online-exam-title-publish#
Action unpublish -next_state done -label #xowf.online-exam-unpublish#
Action republish -next_state published -label #xowf.online-exam-republish# \
    -title #xowf.online-exam-title-republish#
Action restart -next_state initial -label #xowf.restart# \
    -title #xowf.online-exam-title-restart#

State parameter {
  {extra_css {/resources/xowf/test-item.css}}
}
State initial -actions {select} -form en:select_question.form -view_method edit
State created -actions {publish restart} -form_loader load_form -view_method edit \
    -form "#xowf.online-exam-draft_exam#"
State published -actions {unpublish} -form_loader load_form -view_method edit \
    -form "#xowf.online-exam-open#"
State done -actions {republish restart} -form_loader load_form -view_method edit \
    -form "#xowf.online-exam-closed#"

########################################################################
# Activate action select: After the teacher has selected the
# exercises, the answer workflow is created.
#
select proc activate {obj} {
  xowf::test_item::answer_manager create_workflow \
      -answer_workflow /packages/xowf/lib/online-exam-answer.wf \
      $obj
}

########################################################################
# Activate action publish: delete all responses for the workflow and
# publish user participation link.
#
publish proc activate {obj} {
  xowf::test_item::answer_manager delete_all_answer_data $obj
  :publish_link $obj
}

########################################################################
# Activate action republish: publish user participation link.
#
republish proc activate {obj} {
  :publish_link $obj
}

########################################################################
# When the user un-publishes an exam, just the user participation
# link should be removed for the users
#
unpublish proc activate {obj} {
  :unpublish_link $obj
}

########################################################################
# publish_link: make the user participation link available for the
# target group
#
Action instproc publish_link {obj} {
  set aLink [$obj pretty_link -query m=answer]
  util_user_message -html \
      -message "[$obj name] is available as <a target='_blank' href='[ns_quotehtml $aLink]'>[ns_quotehtml $aLink]</a>"
  # TODO: make it happen in the LMS
}

########################################################################
# unpublish_link: remove the user participation link for the target
# group
#
Action instproc unpublish_link {obj} {
  util_user_message -html -message "[$obj name] is closed</a>"
  # TODO: make it happen in the LMS
}

########################################################################
# form loader: create dynamically a form containing the disabled
# questions as a preview and the survey results (the results can be
# refreshed).
#
:proc load_form {ctx title} {
  set obj [$ctx object]
  set state [$obj property _state]

  set combined_form_info [::xowf::test_item::question_manager combined_question_form -with_numbers $obj]
  set fullQuestionForm [dict get $combined_form_info form]
  set full_fc [dict get $combined_form_info disabled_form_constraints]

  #:log  fullQuestionForm=$fullQuestionForm
  set text "<h2>$title</h2>"
  set menu ""

  set wf [xowf::test_item::answer_manager get_answer_wf $obj]
  if {$wf eq ""} {
    :msg "cannot get current workflow for [$obj name]"
    set lLink "."
    set tLink "."
    set aLink "."
    set pLink "."
  } else {
    #
    # Always compute the testrun and answer link.
    #
    set wf_pretty_link [$wf pretty_link]
    set tLink [export_vars -base $wf_pretty_link {
      {m create-new} {p.return_url "[::xo::cc url]"} {p.try_out_mode 1} {title "[$obj title]"}
    }]
    set aLink [$obj pretty_link -query m=answer]
    #
    # If there are answers, include the full menu.
    #
    set answers [xowf::test_item::answer_manager get_answer_attributes $wf]
    if {[llength $answers] > 0} {

      set lLink "$wf_pretty_link?m=list"
      set pLink1 [$obj pretty_link -query m=print-answers]
      set pLink2 [$obj pretty_link -query m=print-answer-table]

      set menu "\["
      if {[acs_user::site_wide_admin_p -user_id [::xo::cc user_id]]} {
        append menu "<a href='[ns_quotehtml $lLink]'>#xowf.online-exam-exam_instances#</a>, "
      }
      append menu \
          "<a href='[ns_quotehtml $pLink1]'>#xowf.online-exam-protocol#</a>, " \
          "<a href='[ns_quotehtml $pLink2]'>#xowf.online-exam-results-table#</a>\]"
    }
  }

  set extraAction ""
  switch $state {
    "created"   {
      append extraAction "<br>" \
          "#xowf.online-exam-try_out# " \
          "<a class='btn btn-default' href='[ns_quotehtml $tLink]'>#xowf.testrun#</a>"
    }
    "published" {
      append extraAction "<br>" \
          "#xowf.online-exam-can_answer# " \
          "<a href='$aLink'>$aLink</a>"
    }
  }

  if {$state in {published done}} {
    if {$state eq "done"} {
      set marked [xowf::test_item::answer_manager marked_results -obj $obj -wf $wf $combined_form_info]
    }
    set answerStats [xowf::test_item::answer_manager answers_panel \
                         -heading "#xowf.online-exam-submitted_exams_heading#" \
                         -submission_msg "#xowf.online-exam-submitted_exams_msg#" \
                         -polling=[expr {${:live_updates} && $state ni {initial created done}}] \
                         -manager_obj $obj \
                         -target_state done \
                         -wf $wf]
  } else {
    set answerStats ""
  }

  append text "$answerStats\n"
  append report "$menu $extraAction"

  # Remove wrapping forms
  regsub -all {</?form[^>]*>} $fullQuestionForm {} fullQuestionForm

  set f [::xowiki::Form new \
             -destroy_on_cleanup \
             -set name en:question \
             -form [subst {<form>$text<div class='exam-preview'>$fullQuestionForm</div>$report</form> text/html}] \
             -text {} \
             -anon_instances t \
             -form_constraints $full_fc \
            ]
}

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

:object-specific {

  set ctx [:wf_context]
  set container [$ctx wf_container]
  if {$ctx ne $container} {
    $ctx forward load_form $container %proc $ctx
  }

  ${container}::Property return_url -default "" -allow_query_parameter true
  #
  # Unset the actual query return_url, since we want to use it via
  # property.  In some cases, we have to set it explicitly from the
  # property, e.g. in www-delete.
  #
  ::xo::cc unset_query_parameter return_url

  ########################################################################
  # web-callable method "delete"
  #
  # Delete the workflow instance and all its associated data.
  #
  :proc www-delete {} {
    ::xo::cc set_query_parameter return_url [:property return_url]
    xowf::test_item::answer_manager delete_all_answer_data [self]
    next
  }

  ########################################################################
  # web-callable method "print-answer-table"
  #
  # Print the answers in a somewhat printer friendly way.
  #
  :proc www-print-answer-table {} {
    set HTML ""
    set ctx [::xowf::Context require [self]]
    set wf [xowf::test_item::answer_manager get_answer_wf [self]]
    if {$wf ne ""} {
      set items [xowf::test_item::answer_manager get_wf_instances $wf]
      set items2 [$items deep_copy]
      foreach i [$items2 children] {
        $i set online-exam-userName [acs_user::get_element -user_id [$i creation_user] -element username]
        $i set online-exam-fullName [::xo::get_user_name [$i creation_user]]
      }
      set HTML [::xowf::test_item::answer_manager results_table \
                    -package_id ${:package_id} \
                    -items $items2 \
                    [self]]
      $items2 destroy
    }
    if {$HTML eq ""} {
      set HTML "#xowiki.no_data#"
    } else {
      set HTML "<h1>#xowf.online-exam-results-table#</h1>$HTML"
    }
    set return_url [[$wf package_id] query_parameter local_return_url:localurl [:pretty_link]]
    append HTML "<hr><p><a class='btn btn-default' href='$return_url'>#xowiki.back#</a></p>\n"

    xo::Page requireCSS /resources/xowf/test-item.css
    :www-view $HTML
  }

  ########################################################################
  # web-callable method "print-answers"
  #
  # Print the answers in a somewhat printer friendly way.
  #
  :proc www-print-answers {} {
    set HTML ""
    set ctx [::xowf::Context require [self]]
    set wf [xowf::test_item::answer_manager get_answer_wf [self]]
    if {$wf ne ""} {
      set items [xowf::test_item::answer_manager get_wf_instances $wf]
      set withSignature [expr {[dict exists ${:instance_attributes} signature]
                               ? [dict get ${:instance_attributes} signature]
                               : 0 }]
      set examTitle ${:title}
      set filter_submission_id [[$wf package_id] query_parameter id:integer ""]

      foreach i [$items children] {
        $i set online-exam-userName [acs_user::get_element -user_id [$i creation_user] -element username]
        $i set online-exam-fullName [::xo::get_user_name [$i creation_user]]
      }
      $items orderby online-exam-userName
      foreach i [$items children] {
        set userName [$i set online-exam-userName]
        set fullName [$i set online-exam-fullName]

        if {[$i state] ne "done"} {
          ns_log notice "online-exam: submission of $userName is not finished (state [$i state])"
          continue
        }
        if {$filter_submission_id ne "" && [$i item_id] ne $filter_submission_id} {
          continue
        }

        #
        # The call to "render_content" calls actually the
        # "summary_form" of online-exam-answer.wf when the submit
        # instance is in state "done". We set the __feedback_mode to
        # get the auto-correction included.
        #
        $i set __feedback_mode 2
        set question_form [$i render_content]

        if {$withSignature} {
          set answerAttributes [xowf::test_item::renaming_form_loader \
                                    answer_attributes [$i instance_attributes]]
          set sha256 [ns_md string -digest sha256 $answerAttributes]
          set signatureString "<div class='signature'>online-exam-actual_signature: $sha256</div>\n"
          set submissionSignature [$i property signature ""]
          if {$submissionSignature ne ""} {
            append signatureString "<div>#xowf.online-exam-submission_signature#: $submissionSignature<div>\n"
          }
        } else {
          set signatureString ""
        }

        set time [::xo::db::tcl_date [$i property _last_modified] tz_var]
        set pretty_date [clock format [clock scan $time] -format "%Y-%m-%d %T"]

        append HTML "\n<div class='single_exam'>" \
            "<h2>$userName · $fullName · $pretty_date · IP [$i property ip]</h2>" \
            $signatureString \
            $question_form \
            "</div>\n"
      }
    }

    if {$HTML eq ""} {
      set HTML "#xowiki.no_data#"
    } else {
      set HTML "<h1>#xowf.online-exam-protocol#</h1>$HTML"
    }
    set return_url [[$wf package_id] query_parameter local_return_url:localurl [:pretty_link]]
    append HTML "<hr><p><a  class='btn btn-default' href='$return_url'>#xowiki.back#</a></p>\n"
    ::xo::cc set_parameter template_file view-plain-master
    ::xo::cc set_parameter MenuBar 0
    xo::Page requireCSS /resources/xowf/test-item.css
    :www-view $HTML
  }

  ########################################################################
  # web-callable method "answer"
  #
  # Create or use an answering workflow for the current exam. This is
  # a convenience routine to shorten the published URL.
  #
  :proc www-answer {} {
    #
    # Make sure that no-one tries to start the answer workflow in a
    # state different to "published".
    #
    if {[:property _state] ne "published"} {
      util_user_message -html -message "Cannot start answer workflow in this state"
    } else {
      set wf [xowf::test_item::answer_manager get_answer_wf [self]]
      $wf www-create-or-use -parent_id [:item_id]
    }
  }

  ########################################################################
  # AJAX call "poll"
  #
  # Return statistics about working and finished exams.
  #
  :proc www-poll {} {
    set wf [xowf::test_item::answer_manager get_answer_wf [self]]
    set answers [xowf::test_item::answer_manager get_answer_attributes $wf]
    set answered [xowf::test_item::answer_manager get_answer_attributes -state done $wf]
    ns_return 200 text/plain [llength $answered]/[llength $answers]
    #ns_log notice "MASTER POLL [self] ${:name}, returned [llength $answered]/[llength $answers]"
    ad_script_abort
  }
}

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