topic-assignment.wf

Delivered as */*

[ hide source ] | [ make this the default ]

File Contents

# -*- Tcl -*-
########################################################################
# Topic-assignment workflow, designed similar to online-exam
# ==========================================================
#
# Defining topic-selection-survey: This workflow lets a lecturer
# choose from a some ordering exercises used to collect preferences of
# participants.  The lecturer selects lists with topics via drag and
# drop. The lecturer can perform a test run of the created survey, and
# can get the results via a result table.
#
# Publishing and closing survey: When a lecturer is satisfied with the
# survey, the survey can be published. In this step, all answers of
# the testing phase are deleted. In the process of publishing, the
# link to start the topic selection survey is offered to the user.
# When the survey is published, the lecturer can see the incoming
# answers in the report by refreshing the page. When the survey is done,
# it is unpublished. The workflow offers the lecturer to see a summary
# of the results in form of a table (an to download the results via
# csv), or the lecturer can produce a printer friendly version of the
# answers.
#
# An admin might wish to add the following entries to the folder to
# ease creation of questions and workflows
#
#   {config -use test-items}
#
# The policy has to allow the following methods on FormPages:
#
#  - "answer" (for students),
#  - "proctor" (for students),
#  - "view-my-exam" (for students),
#  - "edit" (for students),
#  - "poll" (for lecturers),
#  - "print-answers" (for lecturers),
#  - "print-answer-table" (for lecturers),
#  - "print-participants" (for lecturers),
#  - "delete" (for lecturers),
#  - "qrcode" (for lecturers)
#
# Gustaf Neumann, 2021
########################################################################
set :autoname 1   ;# to avoid editable name field
set :policy ::xowf::test_item::test-item-policy-publish
set :debug 0
set :live_updates 1

Action select -next_state created -label "Create Topic Assignment Survey"
Action publish -state_safe true -next_state published -label "Publish Survey"
Action unpublish -state_safe true -next_state done -label "Close Survey"
Action republish -next_state published -label "Reopen Topic Assignment Survey" \
    -title #xowf.online-exam-title-republish#
Action groupformation -next_state buildgroups -label "Group Formation"
Action todone -next_state done -label "Survey Overview"

Action restart -next_state initial -label "Restart"
Action assign -next_state done -label "Assign Students to Groups"

State parameter {
    {extra_css {/resources/xowf/test-item.css}}
    {view_method edit}
}
State initial \
    -actions {select} \
    -form en:select-topics.form

State created \
    -actions {publish restart} \
    -form_loader load_form \
    -form "Survey not published"

State published \
    -actions {unpublish} \
    -form_loader load_form \
    -form "Survey is published"

State done \
    -actions {republish groupformation} \
    -in_role swa {
      -actions {republish groupformation restart}
    } \
    -form_loader load_form \
    -form "Topic Assignment Survey is closed"

State buildgroups \
    -actions {assign todone} \
    -form_loader group_parameter_form \
    -form "Group assignment parameters"


########################################################################
# Activate action select: After the lecturer has selected the
# exercises, the answer workflow is created.
#
select proc activate {obj} {
  set question [$obj property question]

  if {[$obj property with_team_preferences 0]} {
    set extra_form en:select-group-members.form
    if {$extra_form ni $question} {
      $obj set_property question [lappend question $extra_form]
    }
  } elseif {[llength $question] > 1} {
    #
    # There was a change in the configuration, with_team_preferences
    # was probably deactivated.
    #    
    $obj set_property question [lindex $question 0]
  }
  xowf::test_item::answer_manager create_workflow \
      -answer_workflow /packages/xowf/lib/topic-assignment-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
}

########################################################################
# When the user restarts an exam, make sure that already scheduled
# atjobs are removed.
#
restart proc activate {obj} {
  xowf::test_item::answer_manager delete_scheduled_atjobs $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]
  $obj 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} {
  $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 "<h3>$title</h3>"
  set menu ""
  set synchronized [$obj property synchronized 0]
  set with_team_preferences [$obj property with_team_preferences 0]  
  set question_objs     [dict get $combined_form_info question_objs]
  set nrQuestions       [llength $question_objs]

  append text [subst {<p>
    [expr {$synchronized ? "" : "Non-"}]Synchronized Survey<br>
    Team preferences [expr {$with_team_preferences ? "actviated" : "not activated"}]
    </p>}]
  set revision_sets     [$obj get_revision_sets]
  set published_periods [xowf::test_item::answer_manager state_periods $revision_sets -state published]
  set max_items_msg     ""

  append text [subst {
    <p>
    [expr {[llength $published_periods] > 0 ? "<br>Survey published: [join $published_periods {, }]<br>" : ""}]
    </p>
  }]

  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 test-run 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_answers $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 pLink3 [$obj pretty_link -query m=print-participants]

      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 $pLink3]'>#xowf.Participants#</a>, " \
          "<a href='[ns_quotehtml $pLink1]'>#xowf.online-exam-protocol#</a>, " \
          "<a href='[ns_quotehtml $pLink2]'>#xowf.online-exam-results-table#</a>\]"
    }
  }

  switch $state {
    "created" -
    "done" -
    "published" {
      #
      # In inclass cases, never show all questions on screen, since
      # the lecturer might have the screen on the projector.
      #
      template::add_script -src urn:ad:js:bootstrap3
      set fullQuestionForm [subst {
        <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="collapse" data-target="#questions">Topics <span class="caret"></span></button>
        <div id="questions" class="collapse">
        $fullQuestionForm
        </div>
      }]
    }
  }

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

  set www_method [xo::cc query_parameter m:token]
  if {$www_method ni {edit view}} {
    set marked ""
  } else {
    set answerStatus ""
    set marked ""
    if {$state in {published done} } {
      if {$state eq "done"} {
        [$ctx object] setCSSDefaults
        set marked [xowf::test_item::answer_manager marked_results -obj $obj -wf $wf $combined_form_info]
        set marked ""  ;# not needed right now
      }
      set answerStatus [xowf::test_item::answer_manager answers_panel  \
                            -heading "Submitted Preferences" \
                            -submission_msg "Preferences provided by students" \
                            -polling=[expr {${:live_updates} && $state ni {initial created done}}] \
                            -manager_obj $obj \
                            -target_state done \
                            -wf $wf]
    }

    set qrCode ""
    set countdownHTML ""
    if {$state eq "published"} {
      set src [$obj pretty_link -query m=qrcode]
      set qrCode [subst {<div><img class="img-thumbnail qrcode" src="[ns_quotehtml $src]" ></div>}]
      set target_time [xowf::test_item::question_manager exam_target_time \
                           -manager $obj -base_time [$obj last_modified]]
      set countdownHTML [xowf::test_item::answer_manager countdown_timer \
                             -target_time $target_time -id "countdown"]
    }

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

    append text [subst {
      <div class='container-fluid'><div class='row'>
      <div class="col-sm-12">$answerStatus</div>
      <div class="col-sm-9 quiz-preview">$fullQuestionForm</div>
      <div class="col-sm-3">$qrCode</div>
      <div class="col-sm-6">$countdownHTML</div>
      </div></div>
    }]
  }

  set footer "$menu $extraAction"

  if {$state eq "done"} {
    lappend full_fc \
        {maxgroupsize:number,form_item_wrapper_CSSclass=form-inline,min=0,default=3,label=Max Students per group} \
        {mingroupsize:number,form_item_wrapper_CSSclass=form-inline,min=0,default=,label=Min Students per group} \
        {allowemptygroups:boolean,horizontal=true,default=t,label=Allow empty groups}

    set groupformation "<p><hr><h4>Group formation parameters</h4> \
        @maxgroupsize@ @mingroupsize@ @allowemptygroups@"
  } else {
    set groupformation ""
  }

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

:proc group_parameter_form {ctx title} {
  set obj [$ctx object]
  set state [$obj property _state]
  set wf [xowf::test_item::answer_manager get_answer_wf $obj]

  set answers [xowf::test_item::answer_manager get_answers -extra_attributes {_creation_user members} $wf]
  set nrgroups [llength [lindex $answers 0 3 1]]
  set nrParticipants [llength $answers]
  set report "nrParticipants=$nrParticipants\nnrGroups=$nrgroups\n"
  foreach p {maxgroupsize mingroupsize allowemptygroups} {
    append report "$p: [$obj property $p]\n"
  }
  append report \n

  set combined_form_info [::xowf::test_item::question_manager combined_question_form -with_numbers $obj]
  set question_objs [dict get $combined_form_info question_objs]
  set labels {}
  #
  # The first question_obj has to be to topics
  #
  set topics_form [lindex $question_objs 0]
  if {$topics_form ne ""} {
    set q [dict get [$topics_form instance_attributes] question]
    foreach {key text} [dict get $q question.interaction question.interaction.answer] {
      if {[regexp {[.]([^.]+)$} $key _ nr]} {
        #append report "$nr: $text\n"
        dict set labels g$nr $text
      }
    }
  }
  set select_group_members_form [lindex $question_objs 1]
  if {$select_group_members_form ne ""} {
    #ns_log notice "[$select_group_members_form serialize]"
    ns_log notice "ANSWERS $answers"
  }

  set team_preferences ""
  foreach d $answers {
    set atts [dict get $d answerAttributes]
    set i [dict get $d item]
    dict set team_preferences [$i creation_user] [regsub -all \n [dict get $atts members] " "]
  }
  foreach s [dict keys $team_preferences] {
    set filtered_prefs {}
    foreach pref [dict get $team_preferences $s] {
      if {$pref == $s} {
        continue
      }
      if {[dict exists $team_preferences $pref]} {
        set others_prefs [dict get $team_preferences $pref]
        if {$s in $others_prefs} {
          ns_log notice "reciprocal pref $s -> $pref and $pref -> $s"
          lappend filtered_prefs $pref
        } else {
          ns_log notice "preference is not reciprocal"
        }
      } else {
        ns_log notice "no preference for $pref recorded"
      }
    }
    ns_log notice "set team preferences for $s to $filtered_prefs"
    dict set team_preferences $s $filtered_prefs
  }
  
  set students {}
  foreach d $answers {
    set atts [dict get $d answerAttributes]
    set i [dict get $d item]
    set user_id [$i creation_user]
    set user [acs_user::get_element -user_id $user_id -element username]
    set name [::xo::get_user_name $user_id]
    set preferences [lmap x [lindex $atts 1] {incr x}]
    set team [dict get $team_preferences $user_id]
    lappend students [list -user "$user $name" -preferences $preferences -user_id $user_id -team $team]
  }
  append report "-students \{\n  " [join $students "\n  ""\n\}\n"
  
  set cmd [list ::xowf::StudentAssignment new -nrgroups $nrgroups]
  foreach p {maxgroupsize mingroupsize} {
    set value [$obj property $p]
    if {$value ne ""} {
      lappend cmd -$p $value
    }
  }
  if {[$obj property allowemptygroups 0]} {
    lappend cmd -allowemptygroups
  }
  lappend cmd -students $students
  #append report "\n$cmd\n"

  if {0} {
    set model [ {*}$cmd ]
    set r [$model run]
    #ns_log notice "================= label <$labels>"
    append report [$model report -grouplabels $labels $r]
    $model destroy
  }

  #append report [dict get $r annotated] \n
  #append report [dict get $r best] \n

  #
  # Avoid interpretation as includelet, when the report contains
  # double curley braces.
  #
  regsub -all "(\{\{)" $report {\\\1} report

  
  if {0} {
    StudentAssignment create a000 \
        -maxgroupsize 3 \
        -mingroupsize 2 \
        -nrgroups 5 \
        -allowemptygroups \
        -students {
            {-user {neumann@wu-wien.ac.at} -preferences {4 3 2 5 1}}
            {-user {gustaf.neumann@wu.ac.at} -preferences {5 1 2 3 4}}
            {-user {u1} -preferences {4 3 2 5 1}}
            {-user {u2} -preferences {5 1 2 3 4}}
        }
  }
  
  set f [::xowiki::Form new \
             -destroy_on_cleanup \
             -set name en:question \
             -form [subst {<form><h4>$title</h4><pre>$report</pre></form> text/html}] \
             -text {} \
             -anon_instances t \
             -form_constraints {
               @cr_fields:hidden _description:omit _page_order:omit
             } \
            ]
}

########################################################################
#
# 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
    $ctx forward group_parameter_form $container %proc $ctx
  }

  ${container}::Property return_url -default "" -allow_query_parameter true

  if {${:state} eq "done"} {
    set done_actions {republish groupformation}
    set combined_form_info [::xowf::test_item::question_manager combined_question_form [self]]

    set swa_done_actions [concat $done_actions restart]

    ${container}::done actions $done_actions
    ${container}::done in_role swa [subst {
      -actions {$swa_done_actions}
    }]
  }

  #
  # 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 tabular form.
  #
  :proc www-print-answer-table {} {
    set HTML ""
    set withAnswerColumns [${:package_id} query_parameter with_answers:boolean 0]

    set wf [xowf::test_item::answer_manager get_answer_wf [self]]
    if {$wf ne ""} {
      #set form_info [::xowf::test_item::question_manager combined_question_form -with_numbers [self]]

      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 \
                    -state * \
                    -with_answers $withAnswerColumns \
                    [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-participants"
  #
  # Print participants in a tabular form.
  #
  :proc www-print-participants {} {
    set HTML ""
    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 participants_table \
                    -package_id ${:package_id} \
                    -items $items2 \
                    -state * \
                    [self]]
      $items2 destroy
    }
    if {$HTML eq ""} {
      set HTML "#xowiki.no_data#"
    } else {
      set HTML "<h3>#xowf.Participants#</h3>$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"

    :www-view $HTML

  }

  ########################################################################
  # web-callable method "print-answers"
  #
  # Print the answers in a somewhat printer friendly way.
  #
  :proc www-print-answers {} {
    set as_student    [:query_parameter as_student:boolean 0]
    set filter_id     [:query_parameter id:int32 ""]
    set creation_user [:query_parameter creation_user:int32 ""]
    set revision_id   [:query_parameter rid:int32 ""]
    set export        [:query_parameter export:boolean 0]

    set combined_form_info [::xowf::test_item::question_manager combined_question_form [self]]
    set totalPoints [::xowf::test_item::question_manager total_points \
                         -max_items [:property max_items ""] \
                         $combined_form_info]
    #
    # The management of the grading scheme has to be extended. For the
    # time being, we have a single grading scheme with the option to
    # round to full points or not. When an exam has less than 40
    # points, we do not round, since this rounding could provide more
    # than 1 percent of the result. This should be made configurable
    # (also in www-print-answer-table, which is not used right now).
    #
    set grading_scheme ::xowf::test_item::grading::wi1

    if {$totalPoints < 40} {
      append grading_scheme _noround
    }
    set grade_dict {}
    set grade_csv ""

    #
    # Provide quick mapping from the mangled attribute name to the question obj.
    #
    set nameToQuestionObj [xowf::test_item::renaming_form_loader \
                               name_to_question_obj_dict \
                               [dict get $combined_form_info question_objs]]

    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 \
                     {*}[expr {$creation_user ne "" ? "-creation_user $creation_user" : ""}] \
                     {*}[expr {$filter_id ne "" ? "-item_id $filter_id" : ""}] \
                     $wf]
      set withSignature [expr {[dict exists ${:instance_attributes} signature]
                               ? [dict get ${:instance_attributes} signature]
                               : 0 }]
      set examTitle ${:title}
      set do_stream [expr {[llength [$items children]] > 100}]

      #
      # Add a simple print button for the unaware that makes it easy
      # to print the exam protocol to PDF and use e.g. a pdf-tool to
      # annotate free text answers.
      #
      set HTML {
        <button id="print-button">
          <span class='glyphicon glyphicon-print' aria-hidden='true'></span> print
        </button>
      }

      template::add_event_listener \
          -id print-button \
          -event click \
          -preventdefault=false \
          -script "window.print();"
      append HTML [template::collect_body_scripts]

      ::xo::cc set_parameter template_file view-plain-master
      ::xo::cc set_parameter MenuBar 0
      template::head::add_link -rel stylesheet -href /resources/xowf/test-item.css

      if {$as_student} {
        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"
      } else {
        append HTML "<h2>#xowf.online-exam-protocol#</h2>\n"
      }

      if {$do_stream} {
        # ns_log notice STREAM-[info level]-$::template::parse_level
        uplevel #$::template::parse_level [subst {set title "${:title}"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 ${:title}]</h1>
          [lang::util::localize $HTML]
        }]
        set HTML ""
      }

      if {$revision_id ne ""} {
        set r [::xowiki::FormPage get_instance_from_db -revision_id $revision_id]
        if {[$r item_id] ni [lmap i [$items children] {$i item_id}]} {
          error "invalid revision id '$revision_id' provided"
        }
        $items destroy
        set items [::xo::OrderedComposite new -destroy_on_cleanup]
        $items add $r
      }

      if {$export} {
        set recutil [xowf::test_item::answer_manager recutil_create \
                         -clear \
                         -exam_id [$wf parent_id] \
                         -fn [expr {$filter_id eq "" ? "all.rec" : "$filter_id.rec"}]
                    ]
      }

      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]
        set state [$i state]

        #if {$state ne "done"} {
        #  ns_log notice "online-exam: submission of $userName is not finished (state $state)"
        #  #continue
        #}

        set revisions [$i get_revision_sets]
        if {[llength $revisions] <=1 } {
          # just an initial revision
          ns_log notice "online-exam: submission of $userName is empty. Ignoring."
          continue
        }

        #
        # The call to "render_content" calls actually the
        # "summary_form" of online/inclass-exam-answer.wf when the submit
        # instance is in state "done". We set the __feedback_mode to
        # get the auto-correction included.
        #
        foreach f [::xowiki::formfield::FormField info instances -closure] {
          #ns_log notice "FF could DESTROY $f [$f name]"
          if {[string match *_ [$f name]]} {
            #ns_log notice "FF DESTROY $f [$f name]"
            $f destroy
          }
        }
        $wf form_field_flush_cache

        set achieved_points {}
        xo::cc eval_as_user -user_id [$i creation_user] {
          $i set __feedback_mode 2
          set question_form [$i render_content]

          if {$export} {
            xowf::test_item::answer_manager export_answer \
                -user_answers $i \
                -html $question_form \
                -combined_form_info $combined_form_info \
                -recutil $recutil
          }
          if {$withSignature} {
            set answerAttributes [xowf::test_item::renaming_form_loader \
                                      answer_attributes [$i instance_attributes]]
          }
        }

        if {$withSignature} {
          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"]

        set view [expr {$as_student
                        ? "student"
                        : $filter_id ne ""
                        ? "revision_overview"
                        : "default"}]
        set gradingInfo [$grading_scheme print -achieved_points $achieved_points]
        set grandingPanel [expr {[dict exists  $gradingInfo panel] ? [dict get $gradingInfo panel] : ""}]
        set runtime_panel [xowf::test_item::answer_manager runtime_panel \
                               -revision_id $revision_id \
                               -view $view \
                               -grading_info $grandingPanel \
                               $i]
        set heading "$userName · $fullName · $pretty_date"
        append HTML [subst {
          <div class='single_exam'>
          <div class='runtime-data'>
          [expr {$as_student ? "" : "<h2>$heading</h2>"}]
          $runtime_panel
          </div>
          $signatureString
          $question_form
          </div>
        }]

        if {$do_stream} {
          ns_write [lang::util::localize $HTML]
          set HTML " "
        }
      }
      if {$export} {
        $recutil destroy
      }
    }

    if {$HTML eq ""} {
      append HTML "#xowiki.no_data#"
    }

    if {!$as_student} {
      set return_url [: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

    if {$do_stream} {
      ns_write [lang::util::localize $HTML]
      set HTML ""
       [$wf package_id] set __continuation ad_progress_bar_end
      return ""
    } else {
      :www-view $HTML
    }
  }

  ########################################################################
  # web-callable method "answer"
  #
  :proc www-answer {} {
    #
    # Create or use an answering workflow for the current exam. This
    # is a convenience routine to shorten the published URL.
    #
    # 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]]
      set proctoring [:property proctoring]
      if {$proctoring ne "" && $proctoring} {
        set po [:property proctoring_options]
        set cLink [export_vars -base [:pretty_link] {
          {m proctor} {link "[:pretty_link -query m=proctor-answer&proctoring_options=$po]"}
        }]
        ::${:package_id} returnredirect $cLink
      } else {
        $wf www-create-or-use -parent_id [:item_id]
      }
    }
  }
  :proc www-proctor-answer {} {
    #
    # Start answering an exam in proctored mode
    #
    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]
    }
  }

  :proc www-qrcode {} {
    #
    # Produce a QR code with an answer link
    #
    set aLink [:pretty_link -absolute true -query m=answer]
    set fn /tmp/qr-${:item_id}.png
    exec qrencode -o $fn -l h $aLink
    ns_returnfile 200 image/png $fn
    ad_script_abort
  }

  ########################################################################
  # AJAX call "poll"
  #
  :proc www-poll {} {
    #
    # Return statistics about working and finished exams.
    #
    set wf [xowf::test_item::answer_manager get_answer_wf [self]]
    set answers [xowf::test_item::answer_manager get_answers $wf]
    set answered [xowf::test_item::answer_manager get_answers -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
  }

  ########################################################################
  # AJAX call "send-participant-message"
  #
  :proc www-send-participant-message {} {
    #
    # Send a message to a participant
    #
    ::xowiki::includelet::personal-notification-messages message_add \
        -notification_id ${:item_id} \
        -to_user_id [${:package_id} query_parameter user_id:int32 ""] \
        -payload [list msg [ns_queryget msg] from [xo::cc user_id] urgency [ns_queryget urgency]]

    ns_return 200 text/plain ok
    ad_script_abort
  }
}

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