# -*- Tcl -*-
########################################################################
# Inclass-Exam workflow, designed similar to online-exam
# ======================================================
#
# Defining exams: This workflow lets a lecturer choose from a
# predefined set of exam questions, which are typically open text,
# short text, single or multiple choice questions. The lecturer
# selects test questions via drag and drop. The lecturer can perform a
# test run of the created exam, and can get the results via a result
# table.
#
# Publishing and closing exams: When a lecturer 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 lecturer can see the incoming answers in the report by refreshing
# the page. When the exam 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 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 "Inclass Exam" -form en:inclass-exam.wf}
#
# Alternatively, one can use the programmatic setup for the menubar
# via config=test-items in case a site wants to change all setups in
# all instances for menubars by updating a single file.
#
# {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),
# - "exam-summary" (for lecturers),
# - "delete" (for lecturers),
# - "qrcode" (for lecturers)
#
# Gustaf Neumann, Feb 2012-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 #xowf.online-exam-select# \
-title #xowf.online-exam-title-select#
Action publish -next_state published -state_safe true -label #xowf.online-exam-publish# \
-title #xowf.online-exam-title-publish#
Action unpublish -next_state done -state_safe true -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#
Action open_submission_review -next_state submission_review -label #xowf.open_submission_review# \
-title #xowf.open_submission_review_title#
Action close_submission_review -next_state done -label #xowf.close_submission_review# \
-title #xowf.close_submission_review_title#
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.inclass-exam-draft_exam#"
State published -actions {unpublish} -form_loader load_form -view_method edit \
-form "#xowf.inclass-exam-open#"
State done -actions {republish open_submission_review} \
-in_role swa {
-actions {republish open_submission_review restart}
} \
-form_loader load_form -view_method edit \
-form "#xowf.inclass-exam-closed#"
State submission_review -actions {close_submission_review} -form_loader load_form -view_method edit \
-form "#xowf.inclass-exam-review#"
########################################################################
# Activate action select: After the lecturer 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/inclass-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
}
########################################################################
# 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
}
########################################################################
# When the user opens the submission review, offer a link.
#
open_submission_review proc activate {obj} {
set aLink [$obj pretty_link -query m=view-my-exam]
$obj util_user_message -html -message \
"[$obj name] exam review is available as [ns_quotehtml $aLink]"
}
########################################################################
# 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 [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} {
$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 proctoring [$obj property proctoring 0]
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 text [::xowf::test_item::question_manager exam_info_block \
-combined_form_info $combined_form_info \
$obj]
set detail_link [$obj pretty_link -query m=exam-summary]
append text "
"
set wf [xowf::test_item::answer_manager get_answer_wf $obj]
if {$wf eq ""} {
:msg "cannot get current workflow for [$obj name]"
set tLink "."
set aLink "."
} 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 {$proctoring} {
#
# send link via "m=proctor"
#
set tLink [export_vars -base [$obj pretty_link] {
{m proctor} {link "$tLink&p.proctor=1"}
}]
#
# We could send answer link ("aLink") as well this way, but we
# want to keep the link short, therefore, we handle the proctor
# link inside the www-answer method.
#
}
set answers [xowf::test_item::answer_manager get_answers $wf]
#
# Per default, the entries are disabled. When there are answers,
# these will be enabled.
#
set link_disabled [expr {[llength $answers] == 0 ? "link-disabled" : ""}]
set md [subst {
listing {obj $wf m list label #xowf.online-exam-exam_instances# glyph list}
participants {obj $obj m print-participants label #xowf.Participants# glyph user}
protocol {obj $obj m print-answers label #xowf.online-exam-protocol# glyph list-alt}
results {obj $obj m print-answer-table label #xowf.online-exam-results-table# glyph th-list}
}]
if {![acs_user::site_wide_admin_p -user_id [::xo::cc user_id]]} {
dict unset md listing
}
dict unset md results
set menu ""
dict for {name d} $md {
set href [[dict get $d obj] pretty_link -query m=[dict get $d m]]
append menu "" \
" [dict get $d label]\n"
}
}
switch $state {
"created" -
"done" -
"submission_review" -
"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 {
$fullQuestionForm
}]
}
}
set extraAction ""
switch $state {
"created" {
append extraAction " " \
"#xowf.online-exam-try_out# " \
"#xowf.testrun#"
}
"published" {
append extraAction " " \
"#xowf.online-exam-can_answer# " \
"$aLink"
}
}
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 submission_review} || [llength $answers] > 0 } {
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 "#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 \
-extra_text $menu \
-wf $wf]
}
set qrCode ""
set countdownHTML ""
if {$state eq "published"} {
set src [$obj pretty_link -query m=qrcode]
set qrCode [subst {}]
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 {
$answerStatus
$qrCode
$fullQuestionForm
$countdownHTML
}]
}
set footer " $extraAction"
#-form [subst { text/html}]
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
if {${:state} eq "done"} {
set done_actions republish
set combined_form_info [::xowf::test_item::question_manager combined_question_form [self]]
#
# We could allow open_submission_review only when autograde is
# possible, but apparently, it makes as well sense for other
# open text answers
#
lappend done_actions open_submission_review
#if {[dict get $combined_form_info autograde]} {
# lappend done_actions open_submission_review
#}
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 "
#xowf.online-exam-results-table#
$HTML"
}
set return_url [[$wf package_id] query_parameter local_return_url:localurl [:pretty_link]]
append HTML "
\n"
}
if {$do_stream} {
ns_write [lang::util::localize $HTML]
set HTML ""
${: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]
}
}
}
########################################################################
# web-callable function "qrcode", acts as responder
#
: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
}
########################################################################
# web-callable method "proctoring-display"
#
:proc www-proctoring-display {} {
#
# Display the proctoring files collected for this exam using the
# UI by the proctoring-support package.
#
# By this is also possible to delete the proctoring files, either
# for the whole exam or for the single participant.
#
${:package_id} return_page -adp /packages/proctoring-support/lib/proctoring-display -variables {
{object_id ${:item_id}}
}
}
########################################################################
# web-callable function "proctor-answer"
#
: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]
}
}
########################################################################
# web-callable function "proctor"
#
:proc www-proctor {} {
#
# Redirect the exam to an iframe for implementing proctoring. The
# basic idea is that the web application turns on the camera and
# keeps the iframe while the user is iterating through the exam.
# The line "
You are being proctored!
" is just a
# placeholder and has to be replaced with real code.
#
set link [:query_parameter link:localurl ""]
::xo::cc set_parameter template_file view-plain-master
::xo::cc set_parameter MenuBar 0
set proctoring_template /packages/proctoring-support/lib/proctored-page
if {[file exists [acs_root_dir]${proctoring_template}.adp]} {
set object_id ${:item_id}
set object_url $link
set examination_statement_p [expr {![string match *p.try_out_mode=1* $link]}]
set proctoring_options [:property proctoring_options "d c a s"]
foreach \
proctoring_parm {d c a s} \
flag {desktop_p camera_p audio_p examination_statement_p} {
set $flag [expr {$proctoring_parm in $proctoring_options}]
}
set preview_p [expr {$desktop_p || $camera_p || $audio_p}]
#
# Set the max interval between screen captures to 30s (default is 60s)
#
set max_ms_interval 30000
${:package_id} return_page -adp $proctoring_template -variables {
object_id
object_url
preview_p
desktop_p
camera_p
audio_p
max_ms_interval
examination_statement_p
{check_active_p false}
}
} else {
#
# Minimal fallback in case the proctoring-support is not installed
#
return [:www-view [subst {
You ([xo::cc user_id]) are being proctored in exam ${:object_id}!
}]]
}
}
########################################################################
# web-callable function "proctor-image", acts as responder
#
:proc www-proctor-image {} {
#
# View a proctored image
#
set type [${:package_id} query_parameter type:ascii ""]
set ts [${:package_id} query_parameter ts:integer ""]
set ext [${:package_id} query_parameter e:wordchar ""]
set user_id [${:package_id} query_parameter user_id:int32 ""]
set proctoring_dir [proctoring::folder \
-object_id ${:item_id} \
-user_id $user_id]
set png_path $proctoring_dir/$type-$ts.$ext
#ns_log notice "image: $png_path ... [ad_file exists $$png_path]"
ns_returnfile 200 [ns_guesstype $ts.$ext] $png_path
ad_script_abort
}
########################################################################
# web-callable function "blank-inputs"
#
:proc www-blank-inputs {} {
#
# Analyze the student submissions an find situations, where input
# is "cleared" between revisions.
#
template::head::add_link -rel stylesheet -href /resources/xowf/test-item.css
set HTML [xowf::test_item::answer_manager render_answers_with_edit_history [self]]
if {$HTML eq ""} {
set HTML "#xowiki.no_data#"
}
set return_url [:query_parameter local_return_url:localurl [:pretty_link]]
append HTML "