-
+
[expr {$with_exam_heading ? "
$heading
" : ""}]
$runtime_panel
$signatureString
$question_form
- }]
+ }]]
return $HTML
}
#----------------------------------------------------------------------
# Class: Answer_manager
+ # Method: grading_scheme
+ #----------------------------------------------------------------------
+ :method grading_scheme {
+ {-grading:alnum,0..n ""}
+ {-total_points}
+ } {
+ #
+ # The management of the grading scheme has to be extended. For the
+ # time being, we have a single grading scheme with the option to
+ # round to full points or not. When an exam has less than 40
+ # points, we do not round per default, since this rounding could
+ # provide more than 1 percent of the result. This should be made
+ # configurable (also in www-print-answer-table, which is not used
+ # right now).
+ #
+ # @return fully qualified grading scheme object
+ #
+ if {$grading eq ""} {
+ set grading [expr {$total_points < 40 ? "wi1_noround" : "wi1p"}]
+ }
+
+ set grading_scheme ::xowf::test_item::grading::$grading
+ if {[info commands $grading_scheme] eq ""} {
+ set grading_scheme ::xowf::test_item::grading::wi1
+ }
+ #ns_log notice "USE grading_scheme $grading_scheme"
+ return $grading_scheme
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Answer_manager
# Method: render_answers
#----------------------------------------------------------------------
:public method render_answers {
@@ -2976,6 +3377,8 @@
# Return the answers in HTML format in a somewhat printer
# friendly way, e.g. as the exam protocol.
#
+ # @return dict containing "do_stream" and "HTML"
+ #
set combined_form_info [::xowf::test_item::question_manager combined_question_form $examWf]
set autograde [dict get $combined_form_info autograde]
set totalPoints [::xowf::test_item::question_manager total_points \
@@ -2996,24 +3399,8 @@
"valid [dict get $combined_form_info question_objs]"
set form_objs ""
}
- #
- # The management of the grading scheme has to be extended. For the
- # time being, we have a single grading scheme with the option to
- # round to full points or not. When an exam has less than 40
- # points, we do not round per default, since this rounding could
- # provide more than 1 percent of the result. This should be made
- # configurable (also in www-print-answer-table, which is not used
- # right now).
- #
- if {$grading eq ""} {
- set grading [expr {$totalPoints < 40 ? "wi1_noround" : "wi1p"}]
- }
- set grading_scheme ::xowf::test_item::grading::$grading
- if {[info commands $grading_scheme] eq ""} {
- set grading_scheme ::xowf::test_item::grading::wi1
- }
- #ns_log notice "USE grading_scheme $grading_scheme"
+ set grading_scheme [:grading_scheme -grading $grading -total_points $totalPoints]
set :grade_dict {}
set :grade_csv ""
@@ -3062,6 +3449,7 @@
set runtime_panel_view "default"
}
}
+ append HTML [:grading_dialog_setup $examWf]
if {$do_stream} {
# ns_log notice STREAM-[info level]-$::template::parse_level
@@ -3210,6 +3598,7 @@
if {$with_grading_table && $autograde
&& !$as_student && $filter_id eq "" && $creation_user eq "" && $revision_id eq ""
} {
+ set statistics {}
set ia [$examWf instance_attributes]
foreach var {__stats_success __stats_count} key {success count} {
if {[$examWf exists $var]} {
@@ -3249,8 +3638,17 @@
#ns_log notice "### '$att' value '$value'"
$answerObj combine_data_and_form_field_default 1 $f $value
$f set_feedback 1
- $f add_statistics -options {word_statistics word_cloud}
+
#
+ # TODO: world-cloud statistics make mostly sense for the
+ # inclass quizzes, but there these require still an
+ # interface via "reporting_obj" instead of "add_statistics"
+ # (although, for the purposes of the inclass-quiz,
+ # randomization is not an issue.
+ #
+ #$f add_statistics -options {word_statistics word_cloud}
+
+ #
# Leave the form-field in statistics mode in a state with
# correct anwers.
#
@@ -4301,12 +4699,6 @@
- $minutes - $points - $lang - $fc_dict - [$pool_question_obj revision_id]
ns_log notice "get_pool_questions fetch via key: '$key'"
- #return [:get_pool_replacement_candidates \
- -minutes $minutes \
- -points $points \
- -fc_dict $fc_dict \
- -lang $lang \
- $pool_question_obj]
return [ns_cache_eval -expires 1m -- ns:memoize $key {
:get_pool_replacement_candidates \
-minutes $minutes \
@@ -4390,8 +4782,13 @@
-exam_obj:object
} {
#
- # Applies replace_pool_question to all the relevant form objects
+ # Replaces all pool questions for the exam by random items. In
+ # case there were replacement items, set/update the property
+ # "question" for the individual answer_obj.
#
+ # @param answer_obj the workflow instance of the answer workflow
+ # @param exam_obj the exam objject to which the answer_object belongs to
+ #
if {[$answer_obj property question] ne ""} {
ns_log notice "answer_obj $answer_obj has already a 'question' property" \
[lsort [dict keys [$answer_obj instance_attributes]]]
@@ -4610,7 +5007,6 @@
ns_log warning "load_question_objs: only $loaded out of $out_of from '$names' could be loaded"
}
}
- #ns_log notice "XXX [$obj name] load_question_objs questionNames = <$names>"
return $questionForms
}
@@ -4921,6 +5317,161 @@
#----------------------------------------------------------------------
# Class: Question_manager
+ # Method: question_randomization_ok
+ #----------------------------------------------------------------------
+ :method question_randomization_ok {form_obj} {
+ set randomizationOk 1
+ set qd [:dict_value [$form_obj instance_attributes] question]
+ if {$qd ne ""} {
+ #
+ # No question should have shuffle "always".
+ #
+ if {[:dict_value $qd question.shuffle] eq "always"} {
+ #ns_log notice "FOUND shuffle $qd"
+ set randomizationOk 0
+ }
+ }
+ return $randomizationOk
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Question_manager
+ # Method: question_is_autograded
+ #----------------------------------------------------------------------
+ :method question_is_autograded {form_obj} {
+ #
+ # Return boolean information whether this question is autograded.
+ #
+
+ set formAttributes [$form_obj instance_attributes]
+ if {[dict exists $formAttributes question]} {
+ #
+ # Check autograding and randomization for exam.
+ #
+ set qd [dict get [$form_obj instance_attributes] question]
+
+ #
+ # For autoGrade, we assume currently to have either a grading,
+ # or a question, where every alternative is exactly provided.
+ #
+ if {[dict exists $qd question.grading]} {
+ #
+ # autograde ok on the item type level
+ #
+ set autoGrade 1
+
+ } elseif {[:dict_value $formAttributes auto_correct 0]} {
+ #
+ # auto_correct is in principle enabled, check details on
+ # the concrete question item.
+ #
+ set autoGrade 1
+
+ if {[:dict_value $formAttributes item_type] eq "ShortText"} {
+ #
+ # Check, if the correct_when specification of a short text
+ # question is suited for autocorrection. On the longer
+ # range, this function should be moved to a different
+ # place.
+ #
+
+ set dict [lindex [fc_to_dict [dict get $formAttributes form_constraints]] 1]
+ foreach a [dict get $dict answer] {
+ set op ""
+ regexp {^(\S+)\s} $a . op
+ if {$op ni {eq lt le gt ge btwn AND}} {
+ ns_log notice "question_info [$form_obj name]: not suited for autoGrade: '$a'"
+ set autoGrade 0
+ break
+ }
+ if {$op eq "AND"} {
+ foreach c [lrange $a 1 end] {
+ set op ""
+ regexp {^(\S+)\s} $c . op
+ if {$op ni {eq lt le gt ge btwn}} {
+ ns_log notice "question_info [$form_obj name]: not suited for autoGrade: AND clause '$c'"
+ set autoGrade 0
+ break
+ }
+ }
+ }
+ }
+ }
+ } elseif [dict exists $qd question.interaction question.interaction.answer] {
+ set autoGrade 1
+
+ set answer [dict get $qd question.interaction question.interaction.answer]
+ foreach k [dict keys $answer] {
+ if {![dict exists $answer $k $k.correct]} {
+ set autoGrade 0
+ }
+ }
+ } else {
+ set autoGrade 0
+ }
+ #ns_log notice "question_info [$form_obj name] [$form_obj title] autoGrade $autoGrade"
+ }
+ return $autoGrade
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Question_manager
+ # Method: exam_form
+ #----------------------------------------------------------------------
+ :public method aggregated_form {
+ {-titleless_form:switch false}
+ {-with_grading_box ""}
+ question_infos
+ } {
+ #
+ # Compute an aggregated form based on the chunks available in
+ # question_infos.
+ #
+ # @return HTML form content
+ #
+ set full_form ""
+ set count 0
+ foreach \
+ question_form [dict get $question_infos question_forms] \
+ title_info [dict get $question_infos title_infos] \
+ question_obj [dict get $question_infos question_objs] {
+ set item_type [$question_obj property item_type]
+ append full_form \
+ "
"
+
+ if {!$titleless_form} {
+ append full_form \
+ "
[dict get $title_info full_title]
\n"
+ }
+ if {$with_grading_box ne ""} {
+ set question_name [xowf::test_item::renaming_form_loader \
+ form_name_based_attribute_stem \
+ [$question_obj name]]
+ set visible [expr {$with_grading_box eq "hidden" ? "hidden" : ""}]
+ if {$with_grading_box eq "hidden"} {
+ set question_name answer_$question_name
+ }
+ append full_form [subst [ns_trim -delimiter | {
+ |
+ | #xowf.Points#:
+ |
#xowf.feedback#:
+ |
+ |
+ |
+ |
+ }]]
+ }
+ append full_form $question_form \n
+ }
+
+ regsub -all {<[/]?form>} $full_form "" full_form
+ return $full_form
+ }
+
+ #----------------------------------------------------------------------
+ # Class: Question_manager
# Method: question_info
#----------------------------------------------------------------------
:public method question_info {
@@ -4942,20 +5493,20 @@
# "randomization_for_exam" "autograde" and "question_objs". This
# information is obtained from the provided "form_objs".
#
- set full_form {}
- set full_fc {}
+ # @return dict containing "title_infos", "form_constraints",
+ # "disabled_form_constraints", "randomization_for_exam",
+ # "autograde", "question_forms", "question_objs"
set full_disabled_fc {}
set title_infos {}
+ set question_forms {}
+
if {[llength $positions] == 0} {
set position -1
set positions [lmap form_obj $form_objs {incr position}]
}
set randomizationOk 1
set autoGrade 1
foreach form_obj $form_objs number $numbers position $positions {
- #if {[info exists fixed_position]} {
- # set position $fixed_position
- #}
set form_obj [::xowf::test_item::renaming_form_loader rename_attributes $form_obj]
set form_title [$form_obj title]
set minutes [:question_property $form_obj minutes]
@@ -4993,19 +5544,14 @@
}
append title " " [join $title_components " - "]
- if {!$titleless_form} {
- append full_form \
- "
$title
\n"
- }
-
#
# The flag "no_position" is just provided for the composite
- # form, since we are called there at form generation time,
+ # form, in cases where we are called at form generation time,
# where the position is different from the position in the
- # questionnairee. When the position is fixed, we do not provide
- # it as an argument. As a consequence, the percent
- # substitution is not performed, since it would return always
- # very similar values based on the fixed position.
+ # exam. When the position is fixed, we do not provide it as an
+ # argument. As a consequence, the percent substitution is not
+ # performed, since it would return always very similar values
+ # based on a fixed position.
#
if {$no_position} {
set positionArg {}
@@ -5023,8 +5569,8 @@
-obj $user_answers \
{*}$positionArg \
-form_obj $form_obj]
- append full_form [dict get $d form]
+ lappend question_forms [dict get $d form]
lappend title_infos [list full_title $title \
title $form_title \
minutes $minutes \
@@ -5035,88 +5581,28 @@
-minutes $minutes \
-points $points \
{*}$positionArg]
+
lappend full_disabled_fc [:add_to_fc \
-fc [dict get $d disabled_form_constraints] \
-minutes $minutes \
-points $points \
{*}$positionArg]
- set formAttributes [$form_obj instance_attributes]
- if {[dict exists $formAttributes question]} {
- #
- # Check autograding and randomization for exam.
- #
- set qd [dict get [$form_obj instance_attributes] question]
- #
- # No question should have shuffle "always".
- #
- if {[:dict_value $qd question.shuffle] eq "always"} {
- #ns_log notice "FOUND shuffle $qd"
- set randomizationOk 0
- }
- #
- # For autoGrade, we assume currently to have either a grading,
- # or a question, where every alternative is exactly provided.
- #
- if {[dict exists $qd question.grading]} {
- #
- # autograde ok on the item type level
- #
- } elseif {[:dict_value $formAttributes auto_correct 0]} {
- #
- # auto_correct is in principle enabled, check details on
- # the concrete question item.
- #
- if {[:dict_value $formAttributes item_type] eq "ShortText"} {
- #
- # Check, if the correct_when specification of a short text
- # question is suited for autocorrection. On the longer
- # range, this function should be moved to a different
- # place.
- #
- set dict [lindex [fc_to_dict [dict get $formAttributes form_constraints]] 1]
- foreach a [dict get $dict answer] {
- set op ""
- regexp {^(\S+)\s} $a . op
- if {$op ni {eq lt le gt ge btwn AND}} {
- ns_log notice "question_info: not suited for autoGrade: '$a'"
- set autoGrade 0
- break
- }
- if {$op eq "AND"} {
- foreach c [lrange $a 1 end] {
- set op ""
- regexp {^(\S+)\s} $c . op
- if {$op ni {eq lt le gt ge btwn}} {
- ns_log notice "question_info: not suited for autoGrade: AND clause '$c'"
- set autoGrade 0
- break
- }
- }
- }
- }
- }
- } elseif [dict exists $qd question.interaction question.interaction.answer] {
- set answer [dict get $qd question.interaction question.interaction.answer]
- foreach k [dict keys $answer] {
- if {![dict exists $answer $k $k.correct]} {
- set autoGrade 0
- }
- }
- } else {
- set autoGrade 0
- }
- #ns_log notice "question_info [$form_obj name] [$form_obj title] autoGrade $autoGrade"
+ if {![:question_is_autograded $form_obj]} {
+ set autoGrade 0
}
+ if {![:question_randomization_ok $form_obj]} {
+ set randomizationOk 0
+ }
}
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] \
randomization_for_exam $randomizationOk \
autograde $autoGrade \
+ question_forms $question_forms \
question_objs $form_objs]
}
@@ -5531,8 +6017,9 @@
#
# Substitute form-field place-holders ion the combined form.
#
+ set combined_form [:aggregated_form $combined_form_info]
set form [$obj regsub_eval \
- [template::adp_variable_regexp] [dict get $combined_form_info form] \
+ [template::adp_variable_regexp] $combined_form \
{$obj form_field_as_html -mode display "\\\1" "\2" $form_field_objs}]
append HTML $form
@@ -5957,7 +6444,7 @@
nx::Class create Grading {
:property {percentage_boundaries {50.0 60.0 70.0 80.0}}
- :method calc_grade {-percentage -points -achieved_points} {
+ :method calc_grade {-percentage -points:required -achieved_points:required} {
#
# Return a numeric grade based on achieved_points dict and
# percentage_mapping. On invalid data, return 0.
@@ -5974,7 +6461,10 @@
# ns_log warning "test_item::grading legacy call, use 'achievablePoints' instead of 'totalPoints'"
# dict set achieved_points achievablePoints [dict get $achieved_points totalPoints]
#}
+
if {![info exists percentage]} {
+ #ns_log notice "=== calc_grade compute percentage from totalPoints"
+
if {[dict exists $achieved_points totalPoints] && [dict get $achieved_points totalPoints] > 0} {
set percentage \
[format %.2f [expr {
@@ -6013,13 +6503,32 @@
:method complete_dict {achieved_points} {
#
# This is a transitional method, just for defensive programming
- # to make sure, nobody elese uses the legacy field... should
+ # to make sure, nobody else uses the legacy field... should
# disappear soon.
#
if {![dict exists $achieved_points achievablePoints] && [dict exists $achieved_points totalPoints]} {
ns_log warning "test_item::grading legacy call, use 'achievablePoints' instead of 'totalPoints'"
dict set achieved_points achievablePoints [dict get $achieved_points totalPoints]
}
+ #
+ # When "achievedPoints" is set to empty, and "details" are
+ # provided, we perform a new calculation based on "details".
+ #
+ if {[dict get $achieved_points achievedPoints] eq ""
+ && [dict exists $achieved_points details]
+ } {
+ set achievablePoints 0
+ set achievedPoints 0
+ #ns_log notice "RECALC in complete_dict "
+ foreach detail [dict get $achieved_points details] {
+ #ns_log notice "RECALC in complete_dict '$detail'"
+ set achievedPoints [expr {$achievedPoints + [dict get $detail achieved]}]
+ set achievablePoints [expr {$achievablePoints + [dict get $detail achievable]}]
+ }
+ dict set achieved_points achievedPoints $achievedPoints
+ dict set achieved_points achievablePoints $achievablePoints
+ }
+
foreach key {
achievedPoints
achievablePoints
@@ -6201,6 +6710,7 @@
view admin
poll admin
send-participant-message admin
+ grade-single-item admin
edit admin
print-answers admin
proctoring-display admin
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.38 -r1.1.2.39
--- openacs-4/packages/xowf/www/resources/test-item.css 27 Aug 2021 18:33:47 -0000 1.1.2.38
+++ openacs-4/packages/xowf/www/resources/test-item.css 3 Dec 2021 12:17:47 -0000 1.1.2.39
@@ -15,22 +15,22 @@
background: #FFdddd;
color: black;
}
-div.single_exam div.runtime-data {
+div.single_exam div.runtime-panel {
background: #efefef;
color: black;
padding: 2px 10px 4px 10px;
}
-div.single_exam div.runtime-data .data {
+div.single_exam div.runtime-panel .data {
color: #888;
}
-div.single_exam div.runtime-data a.other,
-div.single_exam div.runtime-data a.live {
+div.single_exam div.runtime-panel a.other,
+div.single_exam div.runtime-panel a.live {
color: #66f;
}
-div.single_exam div.runtime-data .right {
+div.single_exam div.runtime-panel .right {
float: right;
}
-div.single_exam div.runtime-data .revision-details {
+div.single_exam div.runtime-panel .revision-details {
text-align: right;
}
div.single_exam > form {
@@ -336,3 +336,25 @@
div.xowiki-content .download-submissions {
margin-top: 2em;
}
+
+/*
+ * Grading box and grading modal dialog
+ */
+div.grading-box span.comment,
+div.grading-box span.points {
+ padding-right: .5ex;
+}
+div.grading-box a.manual-grade {
+ color: #dd1e1e;
+}
+div.grading-box span.comment,
+div.grading-box span.points {
+ color: #888888;
+}
+div.xowiki-content div.modal-dialog h5,
+div.xowiki-content div.modal-dialog h4 {
+ clear: inherit;
+}
+div.grading-box span.text-warn {
+ color: #ed920ac9;
+}