# -*- 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 [ns_quotehtml $aLink]"
# 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"
# 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 "
$title
"
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 "#xowf.online-exam-exam_instances#, "
}
append menu \
"#xowf.online-exam-protocol#, " \
"#xowf.online-exam-results-table#\]"
}
}
set extraAction ""
switch $state {
"created" {
append extraAction "
" \
"#xowf.online-exam-try_out# " \
"#xowf.testrun#"
}
"published" {
append extraAction "
" \
"#xowf.online-exam-can_answer# " \
"$aLink"
}
}
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 { 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 "#xowf.online-exam-results-table#
$HTML"
}
set return_url [[$wf package_id] query_parameter local_return_url:localurl [:pretty_link]]
append HTML "
#xowiki.back#
\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 "online-exam-actual_signature: $sha256
\n"
set submissionSignature [$i property signature ""]
if {$submissionSignature ne ""} {
append signatureString "#xowf.online-exam-submission_signature#: $submissionSignature
\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
" \
"
$userName · $fullName · $pretty_date · IP [$i property ip]
" \
$signatureString \
$question_form \
"\n"
}
}
if {$HTML eq ""} {
set HTML "#xowiki.no_data#"
} else {
set HTML "
#xowf.online-exam-protocol#
$HTML"
}
set return_url [[$wf package_id] query_parameter local_return_url:localurl [:pretty_link]]
append HTML "
#xowiki.back#
\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: