Index: openacs-4/packages/xowf/xowf.info =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/xowf.info,v diff -u -r1.12.2.5 -r1.12.2.6 --- openacs-4/packages/xowf/xowf.info 4 Nov 2019 10:24:39 -0000 1.12.2.5 +++ openacs-4/packages/xowf/xowf.info 25 Nov 2019 14:51:46 -0000 1.12.2.6 @@ -10,15 +10,15 @@ t xowf - + Gustaf Neumann XoWiki Content Flow - an XoWiki based workflow system implementing state-based behavior of wiki pages and forms 2017-08-06 WU Vienna 2 - - + + Index: openacs-4/packages/xowf/catalog/xowf.de_DE.ISO-8859-1.xml =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/catalog/xowf.de_DE.ISO-8859-1.xml,v diff -u -r1.2.2.1 -r1.2.2.2 --- openacs-4/packages/xowf/catalog/xowf.de_DE.ISO-8859-1.xml 15 Oct 2019 21:13:24 -0000 1.2.2.1 +++ openacs-4/packages/xowf/catalog/xowf.de_DE.ISO-8859-1.xml 25 Nov 2019 14:51:46 -0000 1.2.2.2 @@ -19,13 +19,16 @@ Zeilen Neuer Workflow Antwort - Antwortzeiten + Antworten + Abgegebene Antworten + Antwortzeilen Antwortspalten Richig wenn... Pr�fungsentwurf (Pr�fung nicht freigeschaltet) Pr�fung ist freigeschaltet Pr�fung ist geschlossen Frage + Fragen Erste Frage N�chste Frage Vorherige Frage @@ -41,11 +44,39 @@ Schalte Pr�fung nochmals frei Aktualisieren Drucken + Liste der Bearbeitungen - Wollen Sie einen Probelauf druchf�hren? + Wollen Sie einen Probelauf durchf�hren? Studierende k�nnen die Fragen mit folgendem Link beantworten Probelauf Die Pr�fung ist von der Aufsicht nicht freigegeben! Die Pr�fungszeit ist beendet + Pr�fungsprotokoll + Abgegebene Pr�fungen + Studierende haben eine Pr�fung abgegeben + Teilnehmer haben diese Frage beantwortet + Bitte beantworten Sie + Ergebnisse + Ergebnisse von + Quiz beenden + Antwort speichern + Entwurf (Quiz ist nicht freigegeben) + Abgabe + Gebe Quiz frei + Warte auf n�chste Frage ... + Der Quiz kann derzeit nicht beantwortet werden + Sie haben Frage %number% beantwortet + Zufallsauswahl + Nie + Pro Benutzer + Immer + + Kurztextfrage + Single-Choice-Frage + Multiple-Choice-Frage + Textfrage + Teilfrage + Zeige maximal + Index: openacs-4/packages/xowf/catalog/xowf.en_US.ISO-8859-1.xml =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/catalog/xowf.en_US.ISO-8859-1.xml,v diff -u -r1.2.2.2 -r1.2.2.3 --- openacs-4/packages/xowf/catalog/xowf.en_US.ISO-8859-1.xml 4 Nov 2019 10:24:39 -0000 1.2.2.2 +++ openacs-4/packages/xowf/catalog/xowf.en_US.ISO-8859-1.xml 25 Nov 2019 14:51:46 -0000 1.2.2.3 @@ -20,6 +20,8 @@ Lines New Workflow Answer + Answers + Submitted Answers Answer Lines Answer Columns Correct When @@ -28,10 +30,19 @@ Exam closed Antwort Question + Questions First Question Next Question Previous Question Restart + Refresh + Print + Testrun + Please answer + participants answered this question + Results + Results_of + Create Exam Review Exam Final submission @@ -41,13 +52,43 @@ Publish Exam Close Exam Publish Exam again - Refresh - Print + Publish exam again, existing answers are NOT deleted. + Publish exam (already existing answers will be deleted) + Go back to start where questions can be selected (existing anwers will be deleted). + Create a fresh Exam (will delete all answers to this exam) + Listing of Filled-out Exams Do you want to try out the exam? Students can now answer via - Testrun + The Exam has not been published The Exam is finished + Exam Protocol + Submitted exams + Persons have submitted exam + The quiz has not been published + The quiz is open + The quiz is closed + Draft (quiz not published) + Create quiz + Publish quiz + Publish question + Close quiz + Publish quiz again + Save Answer" + Submit + Waiting for next question ... + The quiz is currently not available for answering + You have already answered question %number% + Short Text Question + Single Choice Question + Multiple Choice Question + Text Question + Sub-Question + Show Max + + None + Per user + Always Fisheye: Tag 1.1 refers to a dead (removed) revision in file `openacs-4/packages/xowf/lib/inclass-quiz-answer.wf'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 1.1 refers to a dead (removed) revision in file `openacs-4/packages/xowf/lib/inclass-quiz.wf'. Fisheye: No comparison available. Pass `N' to diff? Index: openacs-4/packages/xowf/lib/online-exam-answer.wf =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/lib/online-exam-answer.wf,v diff -u -r1.2.2.9 -r1.2.2.10 --- openacs-4/packages/xowf/lib/online-exam-answer.wf 6 Nov 2019 18:49:21 -0000 1.2.2.9 +++ openacs-4/packages/xowf/lib/online-exam-answer.wf 25 Nov 2019 14:51:46 -0000 1.2.2.10 @@ -5,91 +5,55 @@ # to create the exam, to try it out and to publish it # (online-exam.wf). # -# This workflow is similar to the classical "iterate.wf" but is more -# configurable (the answer forms are passed via template -# variables). The workflow uses a form-loader which renames the input -# fields to avoid potential name clashes. +# This workflow is based on the test-item infrastrstructure using +# the "renaming_form_loader" and "question_manager". # -# Template variables: -# @wfTitle@ -# @wfQuestionNames@ -# @wfQuestionTitles@ -# @wfID@ +set :autoname 1 ;# to avoid editable name field +set :policy ::xowf::test_item::test-item-policy-answser +set :debug 0 -set :autoname 1 -set :debug 1 - -set pages [list @wfQuestionNames@] -set titles [list @wfQuestionTitles@] - ######################################################################## # # Properties # -# pages: the form pages used as in/out forms # position: the current page in the exam # return_url: when the exam is finished, the user proceeds to this url # try_out_mode: a teacher can try the exam in this mode -# current_form: used internally to keep the current form # ip: IP address of the user, kept in the instance attribute for auditing # ######################################################################## -Property pages -default $pages -Property titles -default $titles -Property position -default 0 -allow_query_parameter true +Property position -default 0 Property return_url -default "" -allow_query_parameter true Property try_out_mode -default 0 -allow_query_parameter true -Property current_form -default "" -Condition more_ahead \ - -expr {[$obj property position] < [llength [$obj property pages]]-1} - -Condition more_before \ - -expr {[$obj property position] > 0} - -set page_count 1 -set page_actions {} -foreach page $pages { - Action $page_count \ - -next_state working \ - -label "$page_count" \ - -proc activate {obj} [list :goto_page [expr {$page_count -1}]] - lappend page_actions $page_count - incr page_count -} - ######################################################################## # # Action definitions # ######################################################################## Action allocate -proc activate {obj} { - #:msg "allocate $obj" # Called, when we try to create or use a workflow instance # via a workflow definition ($obj is a workflow definition) $obj set_property -new 1 name ___[::xo::cc set untrusted_user_id] + set parent_obj [::xo::db::CrClass get_instance_from_db -item_id [$obj parent_id]] + :payload [list title [$parent_obj title]] } Action initialize -proc activate {obj} { # called, after workflow instance was created - $obj set_property -new 1 _title "@wfTitle@" - set parent_id [$obj parent_id] - #set package_id [$obj package_id] - # make sure to create the parent (the controlling workflow) - set parent_obj [::xo::db::CrClass get_instance_from_db -item_id $parent_id] + set parent_obj [::xo::db::CrClass get_instance_from_db -item_id [$obj parent_id]] set parent_state [$parent_obj state] # - # Don't allow one to enter values when the state of the master - # workflow is not published (e.g. trial mode, or closed) or when - # teacher is not in the try-out-mode. + # Don't allow one to enter values when the state of the parent + # workflow is not published (the teacher has not published the exam, + # or closed it already). Only allow usage in the try-out-mode. # - if {$parent_state ne "published" && [$obj property try_out_mode 0] == 0} { #:msg "LOCKED" set current_state [$obj property _state] @@ -100,43 +64,40 @@ util_user_message -message $lockin_msg($lockin_state) # # Force the user in the done state. Alternatively, we could - # provide a different form or push the user to some other state. + # handle this in the provide a different form or push the user to some other state. # - set ctx [:wf_context] - $ctx set_current_state $lockin_state + [:wf_context] set_current_state $lockin_state } else { #:msg "not LOCKED" } } Action instproc goto_page {position} { - set pages [:property pages] :set_property position $position - :set_property current_form [lindex $pages $position] } -Action instproc set_page {increment} { - set pages [:property pages] +Action instproc set_page {obj increment} { + set parent_obj [::xo::db::CrClass get_instance_from_db -item_id [$obj parent_id]] + set pages [::xowf::test_item::question_manager question_names $parent_obj] set position [:property position 0] incr position $increment if {$position < 0} { set position 0 } elseif {$position >= [llength $pages]} { set position [expr {[llength $pages] - 1}] } - :set_property position $position - :set_property current_form [lindex $pages $position] + :goto_page $position } Action prevQuestion \ -next_state working \ -label #xowf.previous_question# \ - -proc activate {obj} {:set_page -1} + -proc activate {obj} {:set_page $obj -1} Action nextQuestion \ -next_state working \ -label #xowf.next_question# \ - -proc activate {obj} {:set_page 1} + -proc activate {obj} {:set_page $obj 1} Action review \ -next_state done \ @@ -169,14 +130,12 @@ -label #xowf.online-exam-start# \ -proc activate {obj} { $obj set_property position 0 - $obj set_property current_form [lindex [$obj property pages] 0] } Action start_again \ -label #xowf.first_question# \ -next_state working -proc activate {obj} { $obj set_property position 0 - $obj set_property current_form [lindex [$obj property pages] 0] } ######################################################################## @@ -193,28 +152,17 @@ }} {extra_css { /resources/xowf/test-item.css - ../file:seal.js?m=download }} - {form_loader get_question_form_object} } -State working -working set page_actions $page_actions -working proc actions {} { - set actions "" - if {[more_before]} {lappend actions prevQuestion} - set actions [concat $actions ${:page_actions}] - if {[more_ahead]} {lappend actions nextQuestion} - lappend actions save review -} +State working \ + -form_loader working_form_loader State initial \ -actions {start logout} \ -form "../en:exam-start" -set done_actions [concat $page_actions {start_again logout}] State done \ - -actions $done_actions \ -form "../en:exam-done" \ -form_loader summary_form @@ -228,13 +176,19 @@ # # Field-renaming form loader # -proc get_question_form_object {ctx form_name} { +proc working_form_loader {ctx form_name} { set obj [$ctx object] + set position [$obj property position] + + set parent_id [$obj parent_id] + #:msg "waiting_form_loader $form_title [$obj instance_attributes]" + set parent_obj [::xo::db::CrClass get_instance_from_db -item_id $parent_id] + set parent_state [$parent_obj state] # # Load the form. # - set form_obj [::xowf::test_item::renaming_form_loader get_form_object $ctx $form_name] + set form_obj [::xowf::test_item::question_manager nth_question_obj $parent_obj $position] # # Update IP address each time the form is loaded. @@ -246,56 +200,36 @@ # # Update the title of the page # - :set_title $obj -question true -minutes [:minutes_string $form_obj] + set minutes [::xowf::test_item::question_manager minutes_string $form_obj] + :set_title $obj -question true -minutes $minutes + return $form_obj } # -# Set "title" with question and user information. +# Set "title" with question/user/IP information. # :proc set_title {obj {-question:boolean true} {-minutes ""}} { - set t [list] - set state [$obj state] - set position [$obj property position] - if {$question && $state eq "working"} { - set titleString "[_ xowf.question] [expr {$position + 1}]: [lindex [$obj property titles] $position]" - if {$minutes ne ""} { - append titleString " $minutes" - } - lappend title $titleString + set parent_obj [::xo::db::CrClass get_instance_from_db -item_id [$obj parent_id]] + if {$question && [$obj state] eq "working"} { + set form_info [::xowf::test_item::question_manager nth_question_form \ + -with_numbers \ + -position [$obj property position] \ + $parent_obj] + set question_title [dict get $form_info title_infos title] + set number [dict get $form_info title_infos number] + set titleString "[_ xowf.question] $number: $question_title $minutes" + set title [list [string trim $titleString]] } lappend title \ - [$obj title] \ + [$parent_obj title] \ "IP: [$obj property ip]" - #ns_log notice "SETTING $obj title [join $title { · }] (Container)" + #ns_log notice "SETTING $obj title [join $title { · }]" $obj title [join $title " · "] } # -# Get an attribute of the original question -# -:proc question_property {form_obj:object attribute {default ""}} { - set question [$form_obj get_property -name question] - #:msg question=$question - if {[dict exists $question question.$attribute]} { - set value [dict get $question question.$attribute] - } else { - set value $default - } - #:msg "question_property-$attribute='$value' {$question}" - return $value -} - -:proc minutes_string {form_obj:object} { - set minutes [:question_property $form_obj minutes] - if {$minutes ne ""} { - set key [expr {$minutes eq "1" ? [_ xowiki.minute] : [_ xowiki.minutes]}] - set minutes "($minutes $key)" - } -} - -# # Form loader for summary (shows all submission data of a user) # # This form loader is also called indirectly by www-print-answers of @@ -305,32 +239,26 @@ set obj [$ctx object] #:msg "summary_form_loader $form_title [$obj instance_attributes]" - set summary_form "" - set fc {} - set counter 0 - foreach form_name [$obj property pages] { - set form_obj [::xowf::test_item::renaming_form_loader get_form_object $ctx $form_name] - set title [lindex [$obj property titles] $counter] - set minutes [:minutes_string $form_obj] - append summary_form \ - "

[_ xowf.question] [incr counter]: $title $minutes

" \n \ - [$form_obj property form] \ - \n
\n - lappend fc {*}[$form_obj property disabled_form_constraints] - } + set parent_obj [::xo::db::CrClass get_instance_from_db -item_id [$obj parent_id]] + set form_info [::xowf::test_item::question_manager combined_question_form \ + -with_numbers \ + -with_title \ + $parent_obj] - :set_title $obj -question 0 - - # get rid of all inner forms + set summary_form [dict get $form_info form] + set summary_fc [dict get $form_info disabled_form_constraints] regsub -all {]*>} $summary_form {} summary_form + :set_title $obj -question false + return [::xowiki::Form new \ + -destroy_on_cleanup \ -name en:summary \ - -title "Summary" \ + -title $form_title \ -form [list
$summary_form
text/html] \ -text {} \ -anon_instances t \ - -form_constraints [lsort -unique $fc]] + -form_constraints $summary_fc] } :proc addSignature {obj} { @@ -357,16 +285,43 @@ set ctx [:wf_context] set container [$ctx wf_container] if {$ctx ne $container} { - $ctx forward get_question_form_object $container %proc $ctx + $ctx forward working_form_loader $container %proc $ctx $ctx forward summary_form $container %proc $ctx } - set working_state_object [$ctx wf_definition_object working] - $working_state_object set form [:property current_form] - # fallback if the current_form isn't set - if {[$working_state_object set form] eq ""} { - $working_state_object set form [lindex [:property pages] 0] - } +set :policy ::xowf::test_item::test-item-policy1 + if {${:state} in {working done}} { + set parent_obj [::xo::db::CrClass get_instance_from_db -item_id [:parent_id]] + set question_names [::xowf::test_item::question_manager question_names $parent_obj] + set current_position [:property position] + + set actions {} + if {$current_position > 0 && ${:state} eq "working"} { + lappend actions prevQuestion + } + set count 0 + foreach question $question_names { + incr count + ${container}::::Action ${container}::$count \ + -label "$count" \ + -next_state working \ + -extra_css_class [expr {$current_position == $count - 1 ? "current" : ""}] \ + -proc activate {obj} \ + [list :goto_page [expr {$count -1}]] + lappend actions $count + } + if { ${:state} eq "working" + && [::xowf::test_item::question_manager more_ahead -position $current_position $parent_obj] + } { + lappend actions nextQuestion + } + if {${:state} eq "working" } { + lappend actions save review + } else { + lappend actions logout + } + ${container}::${:state} actions $actions + } } # 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.2.10 -r1.6.2.11 --- openacs-4/packages/xowf/lib/online-exam.wf 6 Nov 2019 18:49:21 -0000 1.6.2.10 +++ openacs-4/packages/xowf/lib/online-exam.wf 25 Nov 2019 14:51:46 -0000 1.6.2.11 @@ -4,12 +4,12 @@ # ======================================================== # # This workflow lets a teacher choose from a predefined set of exam -# questions, which are typically open text questions. The teacher -# selects one or several exam question via drag and drop. The teacher +# questions, which are typically open text questions. A teacher +# selects one or several exam questions via drag and drop. The teacher # can test the exam by entering test answers. The results are provided # in form of a table. # -# When the teacher is satisfied with the exam, the exam can be +# 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 @@ -19,26 +19,39 @@ # the results via csv), or the teacher can produce a printer friendly # version of the answers. # -# You might with to add the following entries to the folder to ease +# An admin might with to add the following entries to the folder to ease # creation of exercises and exams # # {entry -name New.App.TextInteraction -label "Text Interaction" -form en:TestItemText.form} -# {entry -name New.App.TextEntryInteraction -label "Text Entry Interaction" -form en:TestItemTextEntry.form} +# {entry -name New.App.ShortTextInteraction -label "Short Text Interaction" -form en:TestItemShortText.form} # {entry -name New.App.MCInteraction -label "MC Interaction" -form en:TestItemMC.form} +# {entry -name New.App.SCInteraction -label "SC Interaction" -form en:TestItemSC.form} # {entry -name New.App.Exam -label 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), +# - "delete" (for teachers), +# # Gustaf Neumann, Feb 2012 ######################################################################## -set :autoname 1 -set :debug 1 -#set :masterWorkflow //xowf/de:workflow.wf -set :masterWorkflow en:Workflow.form +set :autoname 1 ;# to avoid editable name field +set :policy ::xowf::test_item::test-item-policy-publish +set :debug 0 +set :live_updates 0 -Action select -next_state created -label #xowf.online-exam-select# -Action publish -next_state published -label #xowf.online-exam-publish# +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# -Action restart -next_state initial -label #xowf.restart# +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}} @@ -56,15 +69,17 @@ # exercises, the answer workflow is created. # select proc activate {obj} { - [[$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} { - [[$obj wf_context] wf_container] delete_all_answer_data $obj + xowf::test_item::answer_manager delete_all_answer_data $obj :publish_link $obj } @@ -84,107 +99,14 @@ } ######################################################################## -# 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 workflow -# instance and recorded in the formfield "wfName". -# -:proc create_answer_workflow {obj} { - #:log "create_answer_workflow $obj" - - # first delete workflow and data, when it exists - if {[$obj property wfName] ne ""} { - set wf [:delete_all_answer_data $obj] - if {$wf ne ""} {$wf delete} - } - - # create a fresh workflow - set wfName [$obj name].wf - $obj set_property -new 1 wfName $wfName - - set wfMaster ${:masterWorkflow} - set wfTitle [$obj property _title] - set questionObjs [[[$obj wf_context] wf_container] get_questions $obj] - set wfQuestionNames {} - set wfQuestionTitles {} - set attributeNames {} - foreach form_obj $questionObjs { - - lappend attributeNames [xowf::test_item::renaming_form_loader \ - form_name_based_attribute_stem [$form_obj name]] - - lappend wfQuestionNames ../[$form_obj name] - lappend wfQuestionTitles [$form_obj 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 ,] - - #:log "create workflow by filling out form '$wfMaster'" - 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,_last_modified @cr_fields:hidden"]] - $f save_new - #:log "create_answer_workflow $obj DONE [$f pretty_link]" -} - -######################################################################## -# get_answer_wf: return the workflow denoted by the property wfName in obj -# -: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]] -} - -######################################################################## -# get_wf_instances: return the workflow instances -# -: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]] -} - -######################################################################## -# delete_all_answer_data: delete all instances of the answer workflow -# -:proc delete_all_answer_data {obj} { - set wf [:get_answer_wf $obj] - if {$wf ne ""} { - set items [:get_wf_instances -initialize false $wf] - foreach i [$items children] { $i delete } - } - return $wf -} - -######################################################################## # 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 + util_user_message -html \ + -message "[$obj name] is available as [ns_quotehtml $aLink]" + # TODO: make it happen in the LMS } ######################################################################## @@ -193,77 +115,26 @@ # Action instproc unpublish_link {obj} { util_user_message -html -message "[$obj name] is closed" - # TODO: make it happen + # TODO: make it happen in the LMS } - - - ######################################################################## -# get_questions: load and initialize the interaction forms -# -:proc get_questions {obj} { - set questions [lmap ref [$obj property question] { - if {![string match "*/*" $ref]} { - set ref [[$obj parent_id] name]/$ref - } - set ref - }] - set questionNames [join $questions |] - set questionForms [::xowiki::Weblog instantiate_forms \ - -package_id [$obj package_id] \ - -default_lang [$obj lang] \ - -forms $questionNames] - if {[llength $questionForms] < 1} { - error "unknown form $questionNames" - } - #:msg "questionNames '$questionNames', questionForms 'questionForms'" - return $questionForms -} - - -######################################################################## # form loader: create dynamically a form containing the disabled # questions as a preview and the survey results (the results can be -# refreshed). This is a simplified version of get_question_form_object -# of online-exam-answer.wf. +# refreshed). # :proc load_form {ctx title} { set obj [$ctx object] set state [$obj property _state] - set questions [:get_questions $obj] - set counter 0 - set fullQuestionForm "" - set full_fc {} - foreach q $questions { - set raw_form [$q property form] - set raw_fc [$q property form_constraints] - set newName answer$counter + 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] - regsub {@answer@} $raw_form "@$newName@" formContent - set fc [lmap f $raw_fc { - if {[string match "answer:*" $f]} { - regsub answer $f $newName f - append f ,disabled=true - } - set f}] - - append fullQuestionForm \ - "

#xowf.question# [incr counter]

\n" \ - $formContent - lappend full_fc {*}$fc - } - set full_fc [lsort -unique $full_fc] - #:log fullQuestionForm=$fullQuestionForm\n$full_fc - - # Remove wrapping forms - regsub -all {]*>} $fullQuestionForm {} fullQuestionForm - #:log fullQuestionForm=$fullQuestionForm set text "

$title

" - set wf [:get_answer_wf $obj] + set wf [xowf::test_item::answer_manager get_answer_wf $obj] if {$wf eq ""} { :msg "cannot get current workflow for [$obj name]" set lLink "." @@ -273,18 +144,20 @@ 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 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 lLink "$wf_pretty_link?m=list" set aLink [$obj pretty_link -query m=answer] set pLink [$obj pretty_link -query m=print-answers] #util_user_message -html -message "$survey is available as $pLink" set menu "\[#xowf.refresh#,\ #xowf.online-exam-exam_instances#,\ - #xowf.print#\]" + #xowf.online-exam-protocol#\]" } set extraAction "" - switch [$obj property _state] { + switch $state { "created" { append extraAction "
" \ "#xowf.online-exam-try_out# " \ @@ -296,15 +169,30 @@ "$aLink" } } - append text "$menu $extraAction\n" + if {$state in {published done}} { + if {$state eq "done"} { + [$ctx object] setCSSDefaults + set marked [xowf::test_item::answer_manager marked_results $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 ne "done"}] \ + -manager_obj $obj \ + -target_state done \ + -wf $wf] + } else { + set answerStats "" + } - set wfName [$obj property wfName] - set report [expr {$wfName ne "" - ? "{{form-stats -parent_id [$obj item_id] -form $wfName}}\n" - : ""}] - append report "
$menu" + append text "$menu $answerStats\n" + append report "$menu $extraAction" + # Remove wrapping forms + regsub -all {]*>} $fullQuestionForm {} fullQuestionForm + set f [::xowiki::Form new \ + -destroy_on_cleanup \ -set name en:question \ -form [subst {
$text
$fullQuestionForm
$report
text/html}] \ -text {} \ @@ -321,30 +209,22 @@ :object-specific { - ######################################################################## - # - # Helper methods for the workflow context - # - ######################################################################## - set ctx [:wf_context] set container [$ctx wf_container] if {$ctx ne $container} { $ctx forward load_form $container %proc $ctx } - ######################################################################## - # Extern callable methods - ######################################################################## + ${container}::Property return_url -default "" -allow_query_parameter true + ::xo::cc unset_query_parameter return_url ######################################################################## # 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] + xowf::test_item::answer_manager delete_all_answer_data [self] next } @@ -356,9 +236,9 @@ :proc www-print-answers {} { 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] + 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 }] @@ -397,47 +277,56 @@ } append HTML "\n
" \ - "

$examTitle - IP [$i property ip]

" \ - "

$userName · [::xo::get_user_name $uid] · $pretty_date

" \ + "

$userName · [::xo::get_user_name $uid] · $pretty_date · IP [$i property ip]

" \ $signatureString \ $question_form \ "
\n" } } - if {$HTML ne ""} { - ns_return 200 text/html [subst { - - - - - - - $HTML - }] + if {$HTML eq ""} { + set HTML "#xowiki.no_data#" } else { - util_user_message -html -message "No answer data available" - ad_returnredirect [::xo::cc url] + set HTML "

#xowf.online-exam-protocol#

$HTML" } - ad_script_abort + ::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" # - # answer the 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". + # 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 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]] $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_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 + } } # Index: openacs-4/packages/xowf/tcl/test-item-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/tcl/test-item-procs.tcl,v diff -u -r1.7.2.9 -r1.7.2.10 --- openacs-4/packages/xowf/tcl/test-item-procs.tcl 6 Nov 2019 18:45:47 -0000 1.7.2.9 +++ openacs-4/packages/xowf/tcl/test-item-procs.tcl 25 Nov 2019 14:51:46 -0000 1.7.2.10 @@ -1,4 +1,11 @@ +::xo::library doc { + Test Item procs - support for different kind of tests and exercises. + + @author Gustaf Neumann +} + :::xo::db::require package xowiki + namespace eval ::xowiki::formfield { ########################################################### # @@ -56,7 +63,7 @@ @param feedback_level "full", "single", or "none" @param grading one of "exact", "partial", or "none" @param nr_choices number of choices - @param question_type "mc", "sc", "ot", or "te" + @param question_type "mc", "sc", "ot", or "st" } # @@ -119,8 +126,8 @@ set can_shuffle false } sc { # we should support as well: minChoices, maxChoices - set interaction_class mc_interaction - set options nr_choices=[:nr_choices],multiple=false + set interaction_class mc_interaction2 + set options multiple=false set auto_correct true set can_shuffle true } @@ -135,8 +142,9 @@ set auto_correct ${:auto_correct} set can_shuffle false } - te { - set interaction_class text_entry_interaction + te - + st { + set interaction_class short_text_interaction #set options nr_choices=[:nr_choices] set auto_correct ${:auto_correct} set can_shuffle true @@ -168,13 +176,16 @@ } if {$can_shuffle} { - set shuffle_options "{None none} {{Per user} peruser} {Always always}" - set shuffleSpec [subst {{shuffle {radio,horizontal=true,options=$shuffle_options,default=none,label=#xowf.Shuffle#}}}] + set shuffle_options "{#xowf.shuffle_none# none} {#xowf.shuffle_peruser# peruser} {#xowf.shuffle_always# always}" + set shuffleSpec [subst { + {shuffle {radio,horizontal=true,form_item_wrapper_CSSclass=form-inline,options=$shuffle_options,default=none,label=#xowf.Shuffle#}} + {show_max {number,form_item_wrapper_CSSclass=form-inline,min=2,label=#xowf.show_max#}} + }] } else { set shuffleSpec "" } :create_components [subst { - {minutes number,min=1,default=2,label=#xowf.Minutes#} + {minutes number,form_item_wrapper_CSSclass=form-inline,min=1,default=2,label=#xowf.Minutes#} $gradingSpec $shuffleSpec {interaction {$interaction_class,$options,feedback_level=${:feedback_level},auto_correct=${:auto_correct},label=}} @@ -201,6 +212,7 @@ mc_interaction instproc set_compound_value {value} { set r [next] + if {!${:multiple}} { # For single choice questions, we have a fake-field for denoting # the correct entry. We have to distribute this to the radio @@ -218,6 +230,7 @@ } mc_interaction instproc initialize {} { + if {${:__state} ne "after_specs"} return # # build choices @@ -250,7 +263,7 @@ #:msg " input_field_names=${:input_field_names}" set mc [:get_named_sub_component_value mc] - ns_log notice "MC <$mc>" + #ns_log notice "MC <$mc>" if {!${:multiple}} { set correct_field_name [:get_named_sub_component_value correct] @@ -400,25 +413,23 @@ :create_components [subst { {text {$widget,label=#xowf.exercise-text#,plugins=OacsFs}} - {lines {number,min=1,default=10,label=#xowf.answer_lines#}} - {columns {number,min=1,max=80,default=60,label=#xowf.answer_columns#}} + {lines {number,form_item_wrapper_CSSclass=form-inline,min=1,default=10,label=#xowf.answer_lines#}} + {columns {number,form_item_wrapper_CSSclass=form-inline,min=1,max=80,default=60,label=#xowf.answer_columns#}} $autoCorrectSpec }] set :__initialized 1 } text_interaction instproc convert_to_internal {} { set intro_text [:get_named_sub_component_value text] - set lines [:get_named_sub_component_value lines] - set columns [:get_named_sub_component_value columns] - if {${:auto_correct}} { - set correct_when [:get_named_sub_component_value correct_when] - set correct_when [::xowiki::formfield::FormField fc_encode correct_when=$correct_when], - #ns_log notice "correct_when <$correct_when>" + dict set fc_dict rows [:get_named_sub_component_value lines] + dict set fc_dict cols [:get_named_sub_component_value columns] + dict set fc_dict disabled_as_div 1 + dict set fc_dict label #xowf.answer# - } else { - set correct_when "" + if {${:auto_correct}} { + dict set fc_dict correct_when [:get_named_sub_component_value correct_when] } append form \ @@ -430,7 +441,7 @@ "\n" append fc \ "@categories:off @cr_fields:hidden\n" \ - "{answer:textarea,disabled_as_div=1,rows=$lines,cols=$columns,$correct_when,label=Answer}" + "{answer:[:dict_to_fc -type textarea $fc_dict]}" #ns_log notice "text_interaction $form\n$fc" ${:object} set_property -new 1 form $form @@ -445,14 +456,14 @@ namespace eval ::xowiki::formfield { ########################################################### # - # ::xowiki::formfield::text_entry_interaction + # ::xowiki::formfield::short_text_interaction # ########################################################### - Class create text_entry_interaction -superclass TestItemField -parameter { + Class create short_text_interaction -superclass TestItemField -parameter { } - text_entry_interaction instproc initialize {} { + short_text_interaction instproc initialize {} { if {${:__state} ne "after_specs"} return # # Create component structure. @@ -461,19 +472,19 @@ ns_log notice "[self] [:info class] auto_correct=${:auto_correct}" :create_components [subst { - {text {$widget,label=#xowf.exercise-text#,plugins=OacsFs}} - {answer {text_entry_field,repeat=1..5,label=}} + {text {$widget,height=100px,label=#xowf.exercise-text#,plugins=OacsFs}} + {answer {short_text_field,repeat=1..5,label=}} }] set :__initialized 1 } - text_entry_interaction instproc convert_to_internal {} { + short_text_interaction instproc convert_to_internal {} { set intro_text [:get_named_sub_component_value text] set answerFields [:get_named_sub_component_value answer] - set shuffle_kind [${:parent_field} get_named_sub_component_value shuffle] set options {} + set render_hints {} set answer {} set count 0 @@ -486,24 +497,33 @@ set af answer[incr count] lappend options [list [dict get $value $fieldName.text] $af] lappend answer [dict get $value $fieldName.correct_when] + lappend render_hints [list \ + words [dict get $value $fieldName.options] \ + lines [dict get $value $fieldName.lines]] } - set options [::xowiki::formfield::FormField fc_encode $options] - set answer [::xowiki::formfield::FormField fc_encode $answer] + dict set fc_dict shuffle_kind [${:parent_field} get_named_sub_component_value shuffle] + dict set fc_dict show_max [${:parent_field} get_named_sub_component_value show_max] + dict set fc_dict disabled_as_div 1 + dict set fc_dict label "" + dict set fc_dict options $options + dict set fc_dict answer $answer + dict set fc_dict render_hints $render_hints append form \ "
\n" \ - "
\n" \ + "
\n" \ "
$intro_text
\n" \ "@answer@" \n \ "
\n" \ "\n" + set fc {} lappend fc \ - "answer:text_fields,disabled_as_div=1,options=$options,shuffle_kind=$shuffle_kind,answer=$answer,label=" \ + answer:[:dict_to_fc -type text_fields $fc_dict] \ @categories:off @cr_fields:hidden - ns_log notice "text_entry_interaction $form\n$fc" + ns_log notice "short_text_interaction $form\n$fc" ${:object} set_property -new 1 form $form ${:object} set_property -new 1 form_constraints $fc set anon_instances true ;# TODO make me configurable @@ -513,27 +533,27 @@ } # - # ::xowiki::formfield::text_entry_field + # ::xowiki::formfield::short_text_field # - Class create text_entry_field -superclass TestItemField -parameter { + Class create short_text_field -superclass TestItemField -parameter { } - text_entry_field instproc initialize {} { + short_text_field instproc initialize {} { if {${:__state} ne "after_specs"} return # # Create component structure. # set widget [test_item set richtextWidget] # - # Get auto_correct from the interaction (passing "auto_correct=" - # via form constrain would requires to extend the RepeatContainer, + # Get "auto_correct" from the interaction (passing "auto_correct=" + # via form constrain would require to extend the RepeatContainer, # otherwise the attribute is rejected). # set p [:info parent] while {1} { if {![$p istype ::xowiki::formfield::FormField]} break - if {![$p istype ::xowiki::formfield::text_entry_interaction]} { + if {![$p istype ::xowiki::formfield::short_text_interaction]} { set p [$p info parent] continue } @@ -547,10 +567,21 @@ } else { set autoCorrectSpec "" } + set render_hints [join { + "{#xowiki.number# number}" + "{#xowiki.single_word# single_word}" + "{#xowiki.multiple_words# multiple_words}" + "{#xowiki.multiple_lines# multiple_lines}" + } " "] + set textEntryConfigSpec [subst { + {options {radio,horizontal=true,form_item_wrapper_CSSclass=form-inline,options=$render_hints,default=single_word,label=#xowf.answer#}} + {lines {number,form_item_wrapper_CSSclass=form-inline,default=1,min=1,label=#xowf.lines#}} + }] + #:msg autoCorrectSpec=$autoCorrectSpec :create_components [subst { - {text {$widget,height=100px,label=Teilaufgabe,plugins=OacsFs}} - $autoCorrectSpec + {text {$widget,height=100px,label=#xowf.sub_question#,plugins=OacsFs}} + $textEntryConfigSpec $autoCorrectSpec }] set :__initialized 1 } @@ -565,9 +596,11 @@ ########################################################### Class create mc_interaction2 -superclass TestItemField -parameter { + {multiple true} } mc_interaction2 instproc initialize {} { + if {${:__state} ne "after_specs"} return # # Create component structure. @@ -576,7 +609,7 @@ #ns_log notice "[self] [:info class] auto_correct=${:auto_correct}" :create_components [subst { - {text {$widget,label=#xowf.exercise-text#,plugins=OacsFs}} + {text {$widget,height=100px,label=#xowf.exercise-text#,plugins=OacsFs}} {answer {mc_field,repeat=1..10,label=}} }] set :__initialized 1 @@ -585,12 +618,11 @@ mc_interaction2 instproc convert_to_internal {} { set intro_text [:get_named_sub_component_value text] - set shuffle_kind [${:parent_field} get_named_sub_component_value shuffle] set answerFields [:get_named_sub_component_value answer] - set count 0 set options {} set correct {} + foreach {fieldName value} $answerFields { # skip template entry if {[lindex [split $fieldName .] end] eq 0} { @@ -601,21 +633,32 @@ set text [dict get $value $fieldName.text] # trim leading

since this causes a newline in the checkbox label regexp {^\s*(

)(.*)$} $text . . text + regexp {^(.*)(

)\s*$} $text . text . lappend options [list $text [incr count]] lappend correct [dict get $value $fieldName.correct] } - set options [::xowiki::formfield::FormField fc_encode $options] - set fc [list answer:checkbox,richtext=1,answer=$correct,shuffle_kind=$shuffle_kind,options=$options] + dict set fc_dict richtext 1 + dict set fc_dict answer $correct + dict set fc_dict options $options + dict set fc_dict shuffle_kind [${:parent_field} get_named_sub_component_value shuffle] + dict set fc_dict show_max [${:parent_field} get_named_sub_component_value show_max] + + set interaction_class [expr {${:multiple} ? "mc_interaction" : "sc_interaction"}] append form \ "
\n" \ - "
\n" \ + "
\n" \ "
$intro_text
\n" \ "@answer@" \n \ "
" \n \ "\n" - lappend fc @categories:off @cr_fields:hidden + set widget [expr {${:multiple} ? "checkbox" : "radio"}] + set fc {} + lappend fc \ + answer:[:dict_to_fc -type $widget $fc_dict] \ + @categories:off @cr_fields:hidden + ns_log notice "mc_interaction2 $form\n$fc" ${:object} set_property -new 1 form $form ${:object} set_property -new 1 form_constraints $fc @@ -645,7 +688,7 @@ } #:msg autoCorrectSpec=$autoCorrectSpec :create_components [subst { - {text {$widget,height=100px,label=Teilaufgabe,plugins=OacsFs}} + {text {$widget,height=50px,label=#xowf.sub_question#,plugins=OacsFs}} {correct {boolean,horizontal=true,label=Korrekt}} }] set :__initialized 1 @@ -681,7 +724,7 @@ append form \ "
\n" \ "
\n" \ - "
$intro_text
\n" \ + "
$intro_text
\n" \ "@answer@" \ "
\n" \ "
\n" @@ -809,6 +852,14 @@ # on the form name, such that multiple of these form names can be # processed together without name clashes. # + # - answer_attributes + # - answer_for_form + # - answers_for_form + # - form_name_based_attribute_stem + # + # - get_form_object + # - rename_attributes + # :object method map_form_constraints {form_constraints oldName newName} { # @@ -840,6 +891,10 @@ :public object method answer_attributes {instance_attributes} { + # + # Return all form-loader specific attributes from + # instance_attributes. + # set result "" foreach key [lsort [dict keys $instance_attributes]] { if {[string match *_ $key]} { @@ -849,15 +904,49 @@ return $result } - :public object method get_form_object {{-set_title:boolean true} ctx form_name} { - #:msg "renaming_form_loader for form_name <$form_name>" - set form_id [$ctx default_load_form_id $form_name] - set obj [$ctx object] - set form_obj [::xo::db::CrClass get_instance_from_db -item_id $form_id] + :public object method answer_for_form {formName instance_attributes} { + # + # Return answer for the provided formName from + # instance_attributes of a single object. + # + set result "" + set stem [:form_name_based_attribute_stem $formName] + set answerAttributes [:answer_attributes $instance_attributes] + ns_log notice "answer_for_form\ninstance_attributes $instance_attributes" + if {[dict exists $answerAttributes $stem]} { + set value [dict get $answerAttributes $stem] + if {$value ne ""} { + lappend result $value + } + } + return $result + } - set form [$form_obj get_property -name form] - set fc [$form_obj get_property -name form_constraints] + :public object method answers_for_form {formName answers} { + # + # Return a list of dicts for the provided formName from the + # answers (as returned from [answer_manager get_answers ...]). + # + set stem [:form_name_based_attribute_stem $formName] + set result "" + foreach answer $answers { + set value answer_for_form + set answerAttributes [dict get $answer answerAttributes] + if {[dict exists $answerAttributes $stem]} { + set value [dict get $answerAttributes $stem] + if {$value ne ""} { + lappend result [list item [dict get $answer item] value $value] + } + } + } + return $result + } + :public object method rename_attributes {form_obj:object} { + + set form [$form_obj get_property -name form] + set fc [$form_obj get_property -name form_constraints] + # # Map "answer" to a generic name in the form "@answer@" and in the # form constraints. @@ -879,15 +968,519 @@ $form_obj set_property -new 1 form_constraints $fc $form_obj set_property -new 1 disabled_form_constraints $disabled_fc + #ns_log notice "RENAMED form $form\n$fc\n$disabled_fc" return $form_obj } + :public object method get_form_object {{-set_title:boolean true} ctx:object form_name} { + #:msg "renaming_form_loader for form_name <$form_name>" + set form_id [$ctx default_load_form_id $form_name] + set obj [$ctx object] + set form_obj [::xo::db::CrClass get_instance_from_db -item_id $form_id] + return [:rename_attributes $form_obj] + } + } } +namespace eval ::xowf::test_item { + + nx::Object create answer_manager { + + # + # Public API: + # + # - create_workflow + # - delete_all_answer_data + # - get_answer_wf + # - get_wf_instances + # - get_answers + # + # - marked_results + # - answers_panel + # + :public object method create_workflow { + {-answer_workflow /packages/xowf/lib/online-exam-answer.wf} + {-master_workflow en:Workflow.form} + parentObj:object + } { + # + # 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 workflow instance and + # recorded in the formfield "wfName". + # + #:log "create_answer_workflow $parentObj" + + # first delete workflow and data, when it exists + if {[$parentObj property wfName] ne ""} { + set wf [:delete_all_answer_data $parentObj] + if {$wf ne ""} {$wf delete} + } + + # + # Create a fresh workflow (e.g. instance of the online-exam, + # inclass-quiz, ...). + # + set wfName [$parentObj name].wf + $parentObj set_property -new 1 wfName $wfName + + set wfTitle [$parentObj property _title] + set questionObjs [::xowf::test_item::question_manager question_objs $parentObj] + + set wfQuestionNames {} + set wfQuestionTitles {} + set attributeNames {} + foreach form_obj $questionObjs { + + lappend attributeNames [xowf::test_item::renaming_form_loader \ + form_name_based_attribute_stem [$form_obj name]] + + lappend wfQuestionNames ../[$form_obj name] + lappend wfQuestionTitles [$form_obj title] + } + set wfID [$parentObj item_id] + + set wfDef [subst -nocommands { + set wfID $wfID + set wfQuestionNames [list $wfQuestionNames] + xowf::include $answer_workflow + }] + set attributeNames [join $attributeNames ,] + + #:log "create workflow by filling out form '$master_workflow'" + set WF [::xowiki::Weblog instantiate_forms \ + -parent_id [$parentObj parent_id] \ + -package_id [$parentObj package_id] \ + -default_lang [$parentObj lang] \ + -forms $master_workflow] + + set wf [$WF create_form_page_instance \ + -name $wfName \ + -nls_language [$parentObj nls_language] \ + -publish_status ready \ + -parent_id [$parentObj item_id] \ + -package_id [$parentObj package_id] \ + -default_variables [list title $wfTitle] \ + -instance_attributes [list workflow_definition $wfDef \ + form_constraints "@table:_name,_state,$attributeNames,_last_modified @cr_fields:hidden"]] + $wf save_new + ns_log notice "create_answer_workflow $wf DONE [$wf pretty_link] IA <[$wf instance_attributes]>" + ns_log notice "create_answer_workflow parent $parentObj IA <[$parentObj instance_attributes]>" + } + + ######################################################################## + + :public object method delete_all_answer_data {obj:object} { + # + # Delete all instances of the answer workflow + # + set wf [:get_answer_wf $obj] + if {$wf ne ""} { + set items [:get_wf_instances -initialize false $wf] + foreach i [$items children] { $i delete } + } + return $wf + } + + + ######################################################################## + + :public object method get_answer_wf {obj:object} { + # + # return the workflow denoted by the property wfName in obj + # + return [::xowiki::Weblog instantiate_forms \ + -parent_id [$obj item_id] \ + -package_id [$obj package_id] \ + -default_lang [$obj lang] \ + -forms [$obj property wfName]] + } + + ######################################################################## + + :public object method get_wf_instances {{-initialize false} wf:object} { + # get_wf_instances: return the workflow instances + 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]] + } + + ######################################################################## + + :public object method get_answers {{-state ""} wf:object} { + set results {} + set items [:get_wf_instances $wf] + foreach i [$items children] { + if {$state ne "" && [$i state] ne $state} { + continue + } + set answerAttributes [xowf::test_item::renaming_form_loader answer_attributes \ + [$i instance_attributes]] + lappend results [list item $i answerAttributes $answerAttributes state [$i state]] + } + return $results + } + + ######################################################################## + + :object method participant_result {obj:object form_info} { + set form_fields [$obj create_form_fields_from_form_constraints \ + -lookup \ + [dict get $form_info disabled_form_constraints]] + $obj form_field_index $form_fields + + set instance_attributes [$obj instance_attributes] + set answer [list item $obj] + foreach f $form_fields { + set att [$f name] + + #ns_log notice "### '$att' exists [dict exists $instance_attributes $att]" + if {[dict exists $instance_attributes $att]} { + set value [dict get $instance_attributes $att] + #ns_log notice "### '$att' value '$value'" + $obj combine_data_and_form_field_default 1 $f $value + $f set_feedback 1 + $f add_statistics -options {word_statistics word_cloud} + # + # Leave the form-field in statistics mode in a state with + # correct anwers. + # + $f make_correct + #ns_log notice "FIELD $f [$f name] [$f info class] -> VALUE [$f set value]" + + lappend answer \ + [list name $att \ + value $value \ + correction [$f set correction] \ + evaluated_answer_result [$f set evaluated_answer_result]] + } + } + return $answer + } + + :public object method marked_results {wf:object form_info} { + set items [:get_wf_instances $wf] + set results "" + foreach i [$items children] { + set participantResult [:participant_result $i $form_info] + append results $participantResult \n + } + return $results + } + + :public object method answers_panel { + {-polling:switch false} + {-heading #xowf.submitted_answers#} + {-submission_msg #xowf.participants_answered_question#} + {-manager_obj:object} + {-target_state} + {-wf:object} + {-current_question ""} + {-extra_text ""} + } { + set answers [xowf::test_item::answer_manager get_answers $wf] + set nrParticipants [llength $answers] + if {$current_question ne ""} { + set answered [xowf::test_item::renaming_form_loader answers_for_form \ + [$current_question name] \ + $answers] + } else { + set answered [xowf::test_item::answer_manager get_answers \ + -state $target_state $wf] + } + set nrAnswered [llength $answered] + + set answerStatus [subst { +
+
$heading
+
+ $nrAnswered/$nrParticipants $submission_msg +
+ $extra_text +
+ }] + + if {$polling} { + # + # auto refresh: when in $parent_obj 'state' or 'position' changes, + # do automatically a reload of the current page. + # + set url [$manager_obj pretty_link -query m=poll] + template::add_body_script -script [subst { + (function poll() { + setTimeout(function() { + var xhttp = new XMLHttpRequest(); + xhttp.open("GET", '$url', true); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var data = xhttp.responseText; + var el = document.querySelector('#answer-status'); + el.innerHTML = data; + poll(); + } + }; + xhttp.send(); + }, 1000); + })(); + }] + } + + return $answerStatus + } + } +} + +namespace eval ::xowf::test_item { + + + nx::Object create question_manager { + # + # This code manages questions and the information related to a + # current (selected) question via qthe "position" instance + # attribute. It provides the following public API: + # + # - goto_page + # - more_ahead + # + # - current_question_form + # - current_question_obj + # - current_question_name + # - current_question_title + # - nth_question_obj + # - nth_question_form + # + # - combined_question_form + # - question_objs + # - question_names + # - question_property + # + :public object method goto_page {obj:object position} { + $obj set_property position $position + } + + :public object method more_ahead {{-position ""} obj:object} { + if {$position eq ""} { + set position [$obj property position] + } + set questions [dict get [$obj instance_attributes] question] + return [expr {$position + 1 < [llength $questions]}] + } + + :object method load_question_objs {obj names} { + set questions [lmap ref $names { + if {![string match "*/*" $ref]} { + set ref [[$obj parent_id] name]/$ref + } + set ref + }] + set questionNames [join $questions |] + set questionForms [::xowiki::Weblog instantiate_forms \ + -package_id [$obj package_id] \ + -default_lang [$obj lang] \ + -forms $questionNames] + return $questionForms + } + + :public object method current_question_name {obj:object} { + set questions [dict get [$obj instance_attributes] question] + return [lindex [dict get [$obj instance_attributes] question] [$obj property position]] + } + + :public object method current_question_obj {obj:object} { + return [:load_question_objs $obj [:current_question_name $obj]] + } + + :public object method question_objs {obj:object} { + return [:load_question_objs $obj [$obj property question]] + } + :public object method question_names {obj:object} { + return [$obj property question] + } + + :public object method nth_question_obj {obj:object position:integer} { + set questions [dict get [$obj instance_attributes] question] + return [:load_question_objs $obj [lindex $questions $position]] + } + + :object method question_info { + {-numbers ""} + {-with_title:switch false} + form_objs + } { + set full_form {} + set full_fc {} + set full_disabled_fc {} + set titles {} + foreach form_obj $form_objs number $numbers { + set form_obj [::xowf::test_item::renaming_form_loader rename_attributes $form_obj] + set form_title [$form_obj title] + set title "" + if {$number ne ""} { + append title "#xowf.question# $number:" + } + if {$with_title} { + append title " $form_title" + } + append full_form "

$title

\n" + append full_form [$form_obj property form] \n + lappend title_infos \ + title $form_title \ + minutes [:question_property $form_obj minutes] \ + number $number + lappend full_fc [$form_obj property form_constraints] + lappend full_disabled_fc [$form_obj property disabled_form_constraints] + } + return [list \ + form $full_form \ + title_infos $title_infos \ + form_constraints [join [lsort -unique $full_fc] \n] \ + disabled_form_constraints [join [lsort -unique $full_disabled_fc] \n]] + } + + + :public object method question_property {form_obj:object attribute {default ""}} { + # + # Get an attribute of the original question + # + set question [$form_obj get_property -name question] + #:msg question=$question + if {[dict exists $question question.$attribute]} { + set value [dict get $question question.$attribute] + } else { + set value $default + } + return $value + } + + :public object method minutes_string {form_obj:object} { + # + # Get an attribute of the original question + # + set minutes [:question_property $form_obj minutes] + if {$minutes ne ""} { + set key [expr {$minutes eq "1" ? [_ xowiki.minute] : [_ xowiki.minutes]}] + set minutes "($minutes $key)" + } + } + + :public object method combined_question_form { + {-with_numbers:switch false} + {-with_title:switch false} + obj:object + } { + set form_objs [:question_objs $obj] + if {$with_numbers} { + set numbers "" + for {set i 1} {$i <= [llength $form_objs]} {incr i} { + lappend numbers $i + } + return [:question_info -with_title=$with_title -numbers $numbers $form_objs] + } else { + return [:question_info -with_title=$with_title $form_objs] + } + } + + :public object method current_question_form { + {-with_numbers:switch false} + {-with_title:switch false} + obj:object + } { + return [:nth_question_form -with_numbers=$with_numbers -with_title=$with_title $obj] + } + + :public object method nth_question_form { + {-position:integer} + {-with_numbers:switch false} + {-with_title:switch false} + obj:object + } { + ns_log notice "==== provided position [info exists position]" + if {![info exists position]} { + set position [$obj property position] + ns_log notice "==== provided position property $position" + } + set form_objs [:nth_question_obj $obj $position] + if {$with_numbers} { + set number [expr {$position + 1}] + return [:question_info -with_title=$with_title -numbers $number $form_objs] + } else { + return [:question_info -with_title=$with_title $form_objs] + } + } + + :public object method current_question_number {obj:object} { + return [expr {[$obj property position] + 1}] + } + :public object method current_question_title {{-with_numbers:switch false} obj:object} { + if {$with_numbers} { + return "#xowf.question# [:current_question_number $obj]" + } + } + + + # :public object method set_page {obj increment} { + # #set pages [$obj property pages] + # set position [$obj property position 0] + # incr position $increment + # if {$position < 0} { + # set position 0 + # } elseif {$position >= [llength $pages]} { + # set position [expr {[llength $pages] - 1}] + # } + # $obj set_property position $position + # #$obj set_property -new 1 current_form [lindex $pages $position] + # } + } +} + +namespace eval ::xowf::test_item { + + # + # Copy the default policy (policy1) from xowiki and add elements for + # FormPages as needed by the demo workflows: + # + # - online-exam.wf, online-exam-answer.wf + # - inclass-quiz.wf, inclass-quiz-answer.wf + # + ::xowiki::policy1 copy ::xowf::test_item::test-item-policy-publish + ::xowiki::policy1 copy ::xowf::test_item::test-item-policy-answer + + # + # Add policy rules as used in two demo workflow. We are permissive + # for student actions and require admin right for teacher activities. + # + test-item-policy-publish contains { + Class create FormPage -array set require_permission { + answer {{item_id read}} + poll admin + edit admin + print-answers admin + delete admin + qrcode admin + } + } + test-item-policy-answer contains { + Class create FormPage -array set require_permission { + poll {{item_id read}} + edit {{item_id read}} + } + } + + #ns_log notice [::xowf::test_item::test-item-policy1 serialize] + #ns_log notice =================================== + +} + + # # Local variables: # mode: tcl # tcl-indent-level: 2 +# eval: (setq tcl-type-alist (remove* "method" tcl-type-alist :test 'equal :key 'car)) # indent-tabs-mode: nil # End: Index: openacs-4/packages/xowf/www/resources/form-generator.css =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/www/resources/form-generator.css,v diff -u -r1.1.2.1 -r1.1.2.2 --- openacs-4/packages/xowf/www/resources/form-generator.css 27 Oct 2019 23:35:15 -0000 1.1.2.1 +++ openacs-4/packages/xowf/www/resources/form-generator.css 25 Nov 2019 14:51:46 -0000 1.1.2.2 @@ -9,3 +9,27 @@ .xowiki-content .margin-form fieldset div.form-help-text { margin-left: 11em; } + +.xowiki-content fieldset div.form-label { + padding: 0px 0px 0px 6px; +} +/* + * Styling of form-inline (e.g. shuffle) + */ +.xowiki-content fieldset div.form-inline { + display: inline-block; + padding: 0px 20px 0px 6px; +} + +.xowiki-content fieldset div.form-inline div { + display: inline-block; +} + +.xowiki-content fieldset div.form-inline div.form-label { + width: auto; + padding: 0px 15px 0px 0px; +} +.xowiki-content fieldset div.form-inline div input[type=number] { + width: 80px; +} + Index: openacs-4/packages/xowf/www/resources/test-item.css =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowf/www/resources/test-item.css,v diff -u -r1.1.2.4 -r1.1.2.5 --- openacs-4/packages/xowf/www/resources/test-item.css 6 Nov 2019 18:44:47 -0000 1.1.2.4 +++ openacs-4/packages/xowf/www/resources/test-item.css 25 Nov 2019 14:51:46 -0000 1.1.2.5 @@ -38,23 +38,40 @@ /*background-color: #fafafa;*/ } div.mc_interaction div.form-label, +div.sc_interaction div.form-label, div.text_interaction div.form-label { display: none; } -div.mc_interaction, div.sc_interaction, div.text_entry_interaction, div.text_interaction { +div.mc_interaction, +div.sc_interaction, +div.text_entry_interaction, +div.text_interaction { padding: 15px 0px 0px 15px; } -div.mc_interaction label, div.sc_interaction label { +div.mc_interaction label, +div.sc_interaction label { font-weight: normal; + /*margin: 0px 0px 10px 0px;*/ + padding: 0px 0px 4px 0px; } label div.richtext-label { display: inline-block; - max-width: 500px; + width: 450px; padding: 0px 0px 0px 15px; } - +div.text_entry_interaction div.progress, +div.mc_interaction div.progress, +div.sc_interaction div.progress { + margin: 0px; +} +div.text_entry_interaction div.progress div.progress-bar, +div.mc_interaction div.progress div.progress-bar, +div.sc_interaction div.progress div.progress-bar { + background-color: #ccc; + color: #444; +} div.mc_interaction label input[type="checkbox"], div.sc_interaction label input[type="radio"] { vertical-align: top; @@ -123,3 +140,26 @@ font-size: 14px; background-color: #fafafa; } + +/* + * Action button styling + */ +form button.current { + background-color: #ddd; + border-top: 2px solid #999; + border-bottom: 2px solid #999; + border-style: groove; +} + +/* + * Modal + jquery tag cloud + */ +div.text_entry_interaction .modal { + display: block; /* undo display:none */ + height: 0; /* height:0 is also invisible */ + overflow-y: hidden; /* no-overflow */ +} +div.text_entry_interaction .modal.fade.in { + height: auto; /* let the content decide it */ +} +