Index: openacs-4/packages/xowf/lib/online-exam.wf
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/lib/online-exam.wf,v
diff -u -r1.6 -r1.7
--- openacs-4/packages/xowf/lib/online-exam.wf 6 Jun 2018 12:42:44 -0000 1.6
+++ openacs-4/packages/xowf/lib/online-exam.wf 3 Sep 2024 15:37:54 -0000 1.7
@@ -1,57 +1,113 @@
# -*- Tcl -*-
########################################################################
-# Online-Exam workflow, designed similar to mobile-clicker
+# Online-Exam workflow
+# ====================
#
-# This workflow lets a teacher choose from a predefined set of exam
-# questions, which are typically open text questions. The user
-# selects one ore several exam question via drag and drop The teacher
-# can test the exam by entering test answers. The results are provided
-# in form of a table.
+# 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.
#
-# When the teacher has finished testing of 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 he can produce a printer friendly version
-# of the answers.
+# 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
########################################################################
-my set autoname 1
-my set debug 1
-my set masterWorkflow //xowf/de:workflow.wf
+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 {Erstelle Prüfung}
-Action publish -next_state published -label {Schalte Prüfung frei}
-Action unpublish -next_state done -label {Schließe Prüfung}
-Action republish -next_state published -label {Schalte Prüfung nochmals frei}
-Action restart -next_state initial
+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#}
+}
-State initial -actions {select} -form en:select_question.form -view_method edit
+
+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 "Prüfungsentwurf (Prüfung nicht freigeschaltet)"
+ -form "#xowf.online-exam-draft_exam#"
State published -actions {unpublish} -form_loader load_form -view_method edit \
- -form "Prüfung ist freigeschaltet"
+ -form "#xowf.online-exam-open#"
State done -actions {republish restart} -form_loader load_form -view_method edit \
- -form "Die Prüfung ist geschlossen."
+ -form "#xowf.online-exam-closed#"
########################################################################
# Activate action select: After the teacher has selected the
# exercises, the answer workflow is created.
#
select proc activate {obj} {
- [[:wf_context] wf_container] create_answer_workflow $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} {
- [[:wf_context] wf_container] delete_all_answer_data $obj
+ xowf::test_item::answer_manager delete_all_answer_data $obj
:publish_link $obj
}
@@ -71,292 +127,301 @@
}
########################################################################
-# create_answer_workflow: create a workflow based on the template
-# provided in this method for answering the question for the
-# students. The name of the workflow is derived from the wokflow
-# instance and recorded in the formfield "wfName".
+# publish_link: make the user participation link available for the
+# target group
#
-my proc create_answer_workflow {obj} {
- my log "create_answer_workflow $obj"
+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
+}
- # first delete workflow and data, when it exists
- if {[$obj property wfName] ne ""} {
- set wf [my delete_all_answer_data $obj]
- if {$wf ne ""} {$wf delete}
- }
+########################################################################
+# 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
+}
- # create a fresh workflow
- set wfName [$obj name].wf
- $obj set_property -new 1 wfName $wfName
+########################################################################
+# 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 wfMaster [my set masterWorkflow]
- set wfTitle [$obj property _title]
- set questionObjs [[:wf_context] get_questions]
- set wfQuestionNames {}
- set wfQuestionTitles {}
- set attributeNames {}
- foreach q $questionObjs {
- set counter 0
- set prefix [lindex [split [$q name] :] end]-a
- dom parse -simple -html [$q property form] doc
- $doc documentElement root
- if {$root ne ""} {
- foreach node [$root selectNodes "//textarea|//input"] {
- set newName $prefix[incr counter]
- lappend attributeNames $newName
+ 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#\]"
}
- lappend wfQuestionNames ../[$q name]
- lappend wfQuestionTitles [$q title]
}
- set wfID [$obj item_id]
- set wfDef [subst -nocommands {
- set wfID $wfID
- set wfTitle "$wfTitle"
- set wfQuestionNames [list $wfQuestionNames]
- set wfQuestionTitles [list $wfQuestionTitles]
- xowf::include /packages/xowf/lib/online-exam-answer.wf [list wfID wfTitle wfQuestionNames wfQuestionTitles]
- }]
- set attributeNames [join $attributeNames ,]
+ set extraAction ""
+ switch $state {
+ "created" {
+ append extraAction " " \
+ "#xowf.online-exam-try_out# " \
+ "#xowf.testrun#"
+ }
+ "published" {
+ append extraAction " " \
+ "#xowf.online-exam-can_answer# " \
+ "$aLink"
+ }
+ }
- set WF [::xowiki::Weblog instantiate_forms \
- -parent_id [$obj parent_id] -package_id [$obj package_id] \
- -default_lang [$obj lang] \
- -forms $wfMaster]
- set f [$WF create_form_page_instance \
- -name $wfName \
- -nls_language [$obj nls_language] \
- -publish_status ready \
- -parent_id [$obj item_id] \
- -package_id [$obj package_id] \
- -default_variables [list title $wfTitle] \
- -instance_attributes [list workflow_definition $wfDef \
- form_constraints "@table:_name,_state,$attributeNames @cr_fields:hidden"]]
- $f save_new
- my log "create_answer_workflow $obj DONE [$f pretty_link]"
-}
+ 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 ""
+ }
-########################################################################
-# get_answer_wf: return the workflow denoted by the property wfName in obj
-#
-my proc get_answer_wf {obj} {
- return [::xowiki::Weblog instantiate_forms \
- -parent_id [$obj item_id] -package_id [$obj package_id] \
- -default_lang [$obj lang] \
- -forms [$obj property wfName]]
-}
+ append text "$answerStats\n"
+ append report "$menu $extraAction"
-########################################################################
-# get_wf_instances: return the workflow instances
-#
-my proc get_wf_instances {{-initialize false} wf} {
- return [::xowiki::FormPage get_form_entries \
- -base_item_ids [$wf item_id] -form_fields "" \
- -always_queried_attributes "*" -initialize $initialize \
- -publish_status all -package_id [$wf package_id]]
-}
+ # Remove wrapping forms
+ regsub -all {?form[^>]*>} $fullQuestionForm {} fullQuestionForm
-########################################################################
-# delete_all_answer_data: delete all instances of the answer workflow
-#
-my proc delete_all_answer_data {obj} {
- set wf [my get_answer_wf $obj]
- if {$wf ne ""} {
- set items [my get_wf_instances -initialize false $wf]
- foreach i [$items children] { $i delete }
- }
- return $wf
+ set f [::xowiki::Form new \
+ -destroy_on_cleanup \
+ -set name en:question \
+ -form [subst { text/html}] \
+ -text {} \
+ -anon_instances t \
+ -form_constraints $full_fc \
+ ]
}
########################################################################
-# publish_link: make the user participation link available for the
-# target group
#
-Action instproc publish_link {obj} {
- set aLink "[$obj pretty_link]?m=answer"
- util_user_message -html -message "[$obj name] is available as [ns_quotehtml $aLink]"
- # TODO: make it happen
-}
-
-########################################################################
-# unpublish_link: remove the user participation link for the target
-# group
+# Object specific operations
#
-Action instproc unpublish_link {obj} {
- util_user_message -html -message "[$obj name] is closed"
- # TODO: make it happen
-}
+########################################################################
:object-specific {
- ########################################################################
- # Extern callable methods
- ########################################################################
+ 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
+
########################################################################
- # delete: delete the workflow instance and all its associated data
+ # web-callable method "delete"
#
+ # Delete the workflow instance and all its associated data.
+ #
:proc www-delete {} {
- set ctx [::xowf::Context require [self]]
- [$ctx wf_container] delete_all_answer_data [self]
+ ::xo::cc set_query_parameter return_url [:property return_url]
+ xowf::test_item::answer_manager delete_all_answer_data [self]
next
}
- :proc -deprecated delete {} {
- :www-delete
- }
-
########################################################################
- # print-answers: print the answers in a somewhat printer friendly way
+ # web-callable method "print-answer-table"
#
- :proc www-print-answers {} {
+ # Print the answers in a somewhat printer friendly way.
+ #
+ :proc www-print-answer-table {} {
set HTML ""
set ctx [::xowf::Context require [self]]
- set wf [[$ctx wf_container] get_answer_wf [self]]
+ set wf [xowf::test_item::answer_manager get_answer_wf [self]]
if {$wf ne ""} {
- set items [[$ctx wf_container] get_wf_instances $wf]
- foreach i [$items children] {
- 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"]
- set uid [$i property _creation_user]
- set text "
"
- set question_form [$i render_content]
- set answer $question_form
- set title [$i title]
- array set ia [$i set instance_attributes]
- regsub -all {
\n"
- :proc -deprecated print-answers {} {
- :www-print-answers
+ xo::Page requireCSS /resources/xowf/test-item.css
+ :www-view $HTML
}
########################################################################
- # answer: answer the exam; this is a convenience routine to shorten
- # the published URL; make sure, that no-one trys to start the answer
- # workflow in a state different to "published"
+ # web-callable method "print-answers"
#
- :proc www-answer {} {
- if {[my property _state] ne "published"} {
- util_user_message -html -message "Cannot start answer workflow in this state"
- } else {
- set ctx [::xowf::Context require [self]]
- set wf [[$ctx wf_container] get_answer_wf [self]]
- $wf www-create-or-use -parent_id [my item_id]
- }
- }
-
- :proc -deprecated answer {args} {
- ad_log warning "????? who is calling me?"
- :www-answer {*}$args
- }
-
- ########################################################################
+ # Print the answers in a somewhat printer friendly way.
#
- # Helper methods for the workflow context
- #
- ########################################################################
+ :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 ""]
- set ctx [:wf_context]
+ 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]
- ########################################################################
- # form loader: create dynamically a form containing the disabled
- # questions and the survey results (the results can be refreshed)
- #
- $ctx proc load_form {title} {
- set state [my property _state]
+ 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
+ }
- set questions [my get_questions]
- set counter 0
- set fullQuestionForm ""
- foreach q $questions {
- append fullQuestionForm \
- "
Frage [incr counter]
\n" \
- [$q property form]
- }
+ #
+ # 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]
- # disable fields, remove wrapping form
- regsub -all {]*>} $fullQuestionForm {} fullQuestionForm
+ 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 "
\n"
+ }
+ } else {
+ set signatureString ""
+ }
- set text "
$title
"
- set obj ${:object}
+ 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"]
- set wf [[:wf_container] get_answer_wf $obj]
- if {$wf eq ""} {
- my msg "cannot get current workflow for [$obj name]"
- set lLink "."
- set tLink "."
- set aLink "."
- set pLink "."
- set menu ""
- } else {
- set wf_pretty_link [$wf pretty_link]
- set tLink "$wf_pretty_link?m=create-new&p.return_url=[::xo::cc url]&p.try_out_mode=1"
- set lLink "$wf_pretty_link?m=list"
- set aLink "[$obj pretty_link]?m=answer"
- set pLink "[$obj pretty_link]?m=print-answers"
- #util_user_message -html -message "$survey is available as $pLink"
- set menu "\[refresh,\
- listing,\
- print\]"
+ append HTML "\n
" \
+ "
$userName · $fullName · $pretty_date · IP [$i property ip]
" \
+ $signatureString \
+ $question_form \
+ "
\n"
+ }
}
- set extraAction ""
- switch [my property _state] {
- "created" {set extraAction " Do you want to try out the exam?"}
- "published" {set extraAction " Students can now answer via $aLink"}
+ if {$HTML eq ""} {
+ set HTML "#xowiki.no_data#"
+ } else {
+ set HTML "
#xowf.online-exam-protocol#
$HTML"
}
- append text "$menu $extraAction\n"
+ set return_url [[$wf package_id] query_parameter local_return_url:localurl [:pretty_link]]
+ append HTML "