[LON-CAPA-cvs] cvs: loncom /homework optionresponse.pm /interface lonpdfupload.pm lontiny.pm /xml lonxml.pm
raeburn
raeburn at source.lon-capa.org
Sun Feb 22 18:27:03 EST 2026
raeburn Sun Feb 22 23:27:03 2026 EDT
Modified files:
/loncom/interface lontiny.pm
/loncom/homework optionresponse.pm
/loncom/xml lonxml.pm
/loncom/interface lonpdfupload.pm
Log:
- Bug 6121
- Support checkbox mode for optionresponse in PDF Forms.
- Expand encoding checks for PDF form fields and submitted values to
include iso-2022-jp, cp1252, and MacRoman.
-------------- next part --------------
Index: loncom/interface/lontiny.pm
diff -u loncom/interface/lontiny.pm:1.8.2.6 loncom/interface/lontiny.pm:1.8.2.7
--- loncom/interface/lontiny.pm:1.8.2.6 Sat Feb 10 14:24:46 2024
+++ loncom/interface/lontiny.pm Thu Jan 16 21:42:12 2025
@@ -2,7 +2,7 @@
# Extract domain, courseID, and symb from a shortened URL,
# and switch role to a role in designated course.
#
-# $Id: lontiny.pm,v 1.8.2.6 2024/02/10 14:24:46 raeburn Exp $
+# $Id: lontiny.pm,v 1.8.2.7 2025/01/16 21:42:12 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -99,7 +99,7 @@
}
if ($env{'request.course.id'} eq $cdom.'_'.$cnum) {
# Check for ttoken
- my $newlauncher = &launch_check($r->uri,$symb);
+ my $newlauncher = &launch_check($r->uri,$symb,$cdom,$cnum);
my ($map,$resid,$url) = &Apache::lonnet::decode_symb($symb);
if (&Apache::lonnet::is_on_map($url)) {
my $realuri;
@@ -307,7 +307,7 @@
}
sub launch_check {
- my ($linkuri,$symb) = @_;
+ my ($linkuri,$symb,$cdom,$cnum) = @_;
my ($linkprotector,$linkproturi,$linkprotexit,$linkprotpbid,$linkprotpburl,
$linkkey,$newlauncher,$prevlaunch);
if ($env{'form.ttoken'}) {
@@ -447,6 +447,11 @@
}
if ($linkprotector) {
&Apache::lonnet::appenv({'request.linkprot' => $linkprotector.':'.$linkproturi});
+ if ($linkprotpburl && $linkprotpbid) {
+ my ($res,$error) = &store_passback_info($cdom,$cnum,$linkuri,$linkprotector,
+ $scope,$symb,$linkprotpbid,$linkprotpburl,
+ $currdeeplinklogin);
+ }
} elsif ($env{'request.linkprot'}) {
&Apache::lonnet::delenv('request.linkprot');
}
@@ -525,6 +530,68 @@
return $newlauncher;
}
+#
+# Store linkprotpburl and linkprotpbid in user's nohist_$cid_linkprot_pb.db
+# $linkuri\0$linkprotector\0$scope = [$linkprotpbid,$linkprotpburl]
+# Separately store $symb in course's nohist_linkprot_passback.db
+# which should trigger passback:
+# $symb => {$linkuri\0$linkprotector\0$scope => 1};
+#
+
+sub store_passback_info {
+ my ($cdom,$cnum,$linkuri,$linkprotector,$scope,$symb,
+ $linkprotpbid,$linkprotpburl,$currdeeplinklogin) = @_;
+ my $key = join("\0",($linkuri,$linkprotector,$scope));
+ my $namespace = 'nohist_'.$cdom.'_'.$cnum.'_linkprot_pb';
+ if ($linkuri eq $currdeeplinklogin) {
+ my %pbinfo = &Apache::lonnet::get($namespace,[$key]);
+ if (ref($pbinfo{$key}) eq 'ARRAY') {
+ if (($pbinfo{$key}[0] eq $linkprotpbid) &&
+ ($pbinfo{$key}[1] eq $linkprotpburl)) {
+ return ('ok');
+ }
+ }
+ }
+ my $now = time;
+ my $result = &Apache::lonnet::cput($namespace,{$key => [$linkprotpbid,$linkprotpburl]});
+ my $error;
+ if (($result eq 'ok') || ($result eq 'con_delayed')) {
+ $namespace = 'nohist_linkprot_passback';
+ my %triggers = &Apache::lonnet::get($namespace,[$symb],$cdom,$cnum);
+ my $newtrigger;
+ if ((exists($triggers{$symb})) && (ref($triggers{$symb}) eq 'HASH')) {
+ unless (exists($triggers{$symb}{$key})) {
+ $newtrigger = 1;
+ }
+ } else {
+ $newtrigger = 1;
+ }
+ if ($newtrigger) {
+ my ($lockhash,$tries,$gotlock);
+ $lockhash = {
+ lock => $env{'user.name'}.
+ ':'.$env{'user.domain'},
+ };
+ $tries = 0;
+ $gotlock = &Apache::lonnet::newput($namespace,$lockhash,$cdom,$cnum);
+ while (($gotlock ne 'ok') && ($tries<10)) {
+ $tries ++;
+ sleep (0.1);
+ $gotlock = &Apache::lonnet::newput($namespace,$lockhash,$cdom,$cnum);
+ }
+ if ($gotlock eq 'ok') {
+ %triggers = &Apache::lonnet::get($namespace,[$symb],$cdom,$cnum);
+ $triggers{$symb}{$key} = 1;
+ $result = &Apache::lonnet::cput($namespace,{$symb => $triggers{$symb}},$cdom,$cnum);
+ my $dellockoutcome = &Apache::lonnet::del($namespace,['lock'],$cdom,$cnum);
+ } else {
+ $error = 'nolock';
+ }
+ }
+ }
+ return ($result,$error);
+}
+
sub do_redirect {
my ($r,$destination,$linkprot) = @_;
my $windowname = 'loncapaclient';
Index: loncom/homework/optionresponse.pm
diff -u loncom/homework/optionresponse.pm:1.206 loncom/homework/optionresponse.pm:1.207
--- loncom/homework/optionresponse.pm:1.206 Sat Feb 21 16:03:57 2026
+++ loncom/homework/optionresponse.pm Sun Feb 22 23:27:02 2026
@@ -1,7 +1,7 @@
# LearningOnline Network with CAPA
# option list style responses
#
-# $Id: optionresponse.pm,v 1.206 2026/02/21 16:03:57 raeburn Exp $
+# $Id: optionresponse.pm,v 1.207 2026/02/22 23:27:02 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -610,10 +610,20 @@
sub check_box_opt {
my ($target,$checkboxvalue, at opt)=@_;
-# Check if we are in checkbox mode: checkboxvalue specified, on web, only two options.
-# If so, return "checked" value
+# Check if we are in checkbox mode: checkboxvalue specified, only two options.
if ($#opt!=1) { return ''; }
+ if (($target eq 'tex') && ($env{'form.pdfFormFields'} eq 'yes')
+ && ($Apache::inputtags::status[-1] eq 'CAN_ANSWER')) {
+# If this is tex for pdfFormFields -- return "checked" and "unchecked" values.
+ if (wantarray) {
+ ($checkboxvalue eq $opt[0]? return ($checkboxvalue,$opt[1]) :
+ return ($checkboxvalue,$opt[0]) );
+ } else {
+ return $checkboxvalue;
+ }
+ }
unless (($target eq 'web') || ($target eq 'grade')) { return ''; }
+# If on web, return "checked" value
return $checkboxvalue;
}
@@ -729,13 +739,13 @@
my $checkboxopt;
if ($target eq 'web') {
$checkboxopt=&check_box_opt($target,$checkboxvalue, at opt);
+ if ($checkboxopt && (!$no_tfprompt)) {
+ $result.='<br />'.
+ ($checkboxchoices?&mt('Choices: ').'<b>'.$opt[0].','.$opt[1].'</b>. ':'').
+ &mt('Select all that are [_1].','<b>'.$checkboxopt.'</b>');
+ }
}
- if ($checkboxopt && (!$no_tfprompt)) {
- $result.='<br />'.
- ($checkboxchoices?&mt('Choices: ').'<b>'.$opt[0].','.$opt[1].'</b>. ':'').
- &mt('Select all that are [_1].','<b>'.$checkboxopt.'</b>');
- }
- my $fieldname;
+ my ($fieldname,$checkboxoff);
if ($target eq 'tex' and $env{'form.pdfFormFields'} eq 'yes'
&& $Apache::inputtags::status[-1] eq 'CAN_ANSWER') {
$fieldname = &Apache::lonenc::encrypted($env{'request.symb'},1);
@@ -749,7 +759,26 @@
$escpart =~ s{_}{\\_}g;
my $escres = $Apache::inputtags::response['-1'];
$escres =~ s{_}{\\_}g;
- $fieldname .= '\&part\_'.$escpart.'\&optionresponse\&HWVAL\_'.$escres.':';
+ $fieldname .= '\&part\_'.$escpart.'\&optionresponse';
+ ($checkboxopt,$checkboxoff)=&check_box_opt($target,$checkboxvalue, at opt);
+
+# To support checkbox mode for optionresponse, include a hidden form
+# field in the PDF with the value set to the option in effect for
+# the unchecked checkboxes. When the submitted form data are processed
+# the values for any unchecked boxes (each null value in received form
+# data) are all set to the value retrieved from the hidden form field.
+
+ if ($checkboxopt) {
+ my $hidden = $fieldname.'\&HWHDN\_'.$escres.':';
+ $result .= &Apache::lonxml::print_pdf_hidden_textfield($hidden.'0',$checkboxoff).' ';
+ if (!$no_tfprompt) {
+ $result .= ($checkboxchoices?&mt('Choices: ').
+ '\textbf{'.$opt[0].','.$opt[1].'}. ':'').
+ &mt('Select all that are [_1].','\textbf{'.$checkboxopt.'}').
+ '\\\\';
+ }
+ }
+ $fieldname .= '\&HWVAL\_'.$escres.':';
}
foreach $name (@whichopt) {
if ($target eq 'web') {
@@ -766,7 +795,12 @@
if($target eq 'tex' and $env{'form.pdfFormFields'} eq 'yes'
&& $Apache::inputtags::status[-1] eq 'CAN_ANSWER') {
- $optionlist = &Apache::lonxml::print_pdf_start_combobox($fieldname.$temp);
+ if ($checkboxopt) {
+ $optionlist = '\vskip 4 mm'.
+ &Apache::lonxml::print_pdf_checkbox($fieldname.$temp,$checkboxopt);
+ } else {
+ $optionlist = &Apache::lonxml::print_pdf_start_combobox($fieldname.$temp);
+ }
}
foreach my $option (@opt) {
@@ -775,7 +809,9 @@
if ($target eq 'tex' && $env{'form.pdfFormFields'} eq 'yes'
&& $Apache::inputtags::status[-1] eq 'CAN_ANSWER'
&& $Apache::lonhomework::type ne 'exam') {
- $optionlist .= &Apache::lonxml::print_pdf_add_combobox_option($option);
+ unless ($checkboxopt) {
+ $optionlist .= &Apache::lonxml::print_pdf_add_combobox_option($option);
+ }
} else {
$optionlist.="<option value='".$escopt."' selected=\"selected\">$option</option>\n";
}
@@ -783,7 +819,9 @@
if ($target eq 'tex' && $env{'form.pdfFormFields'} eq 'yes'
&& $Apache::inputtags::status[-1] eq 'CAN_ANSWER'
&& $Apache::lonhomework::type ne 'exam') {
- $optionlist .= &Apache::lonxml::print_pdf_add_combobox_option($option);
+ unless ($checkboxopt) {
+ $optionlist .= &Apache::lonxml::print_pdf_add_combobox_option($option);
+ }
} else {
$optionlist.="<option value='".$escopt."'>$option</option>\n";
}
@@ -892,7 +930,13 @@
&& $Apache::inputtags::status[-1] eq 'CAN_ANSWER'
&& $Apache::lonhomework::type ne 'exam') {
$text =~ s/\\item//m;
- $result .= " $optionlist ". &Apache::lonxml::print_pdf_end_combobox($text).'\strut \\\\';
+ $result .= " $optionlist ";
+ if ($checkboxopt) {
+ $result .= $text;
+ } else {
+ $result .= &Apache::lonxml::print_pdf_end_combobox($text);
+ }
+ $result .= '\strut \\\\';
$temp++;
}
$displayoptionintex=0;
Index: loncom/xml/lonxml.pm
diff -u loncom/xml/lonxml.pm:1.579 loncom/xml/lonxml.pm:1.580
--- loncom/xml/lonxml.pm:1.579 Sat Feb 21 16:03:58 2026
+++ loncom/xml/lonxml.pm Sun Feb 22 23:27:03 2026
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# XML Parser Module
#
-# $Id: lonxml.pm,v 1.579 2026/02/21 16:03:58 raeburn Exp $
+# $Id: lonxml.pm,v 1.580 2026/02/22 23:27:03 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -2526,6 +2526,35 @@
.$fieldname.'}{10bp}{10bp}{'.$value.'}';
}
+=pod
+
+=item &print_pdf_checkbox(fieldname, value)
+
+Returns a latexline to generate a PDF-Form-checkBox.
+
+$fieldname: PDF internalname of the checkbox
+$value: Value of checkbox
+
+=cut
+sub print_pdf_checkbox {
+ my ($fieldname, $value) = @_;
+ return '\checkBox{'.$fieldname.'}{10bp}{10bp}{'.$value.'}';
+}
+
+=pod
+
+=item &print_pdf_hidden_textfield(fieldname, value)
+
+Returns a latexline to generate a generic PDF-Form-hiddenField
+
+$fieldname: PDF internalname of the hidden field
+$value: Value of the hidden field
+
+=cut
+sub print_pdf_hidden_textfield {
+ my ($fieldname, $value) = @_;
+ return '\textField[\F{\FHidden}\F{-\FPrint}\V{'.$value.'}]{'.$fieldname.'}{0mm}{0mm}';
+}
=pod
@@ -2586,7 +2615,7 @@
=pod
-=item &print_pdf_hiddenField(fieldname, user, domain)
+=item &print_pdf_hiddenfield(fieldname, user, domain)
Returns a latexline to generate a PDF-Form-hiddenField with userdata.
Index: loncom/interface/lonpdfupload.pm
diff -u loncom/interface/lonpdfupload.pm:1.31 loncom/interface/lonpdfupload.pm:1.32
--- loncom/interface/lonpdfupload.pm:1.31 Sat Feb 21 16:03:57 2026
+++ loncom/interface/lonpdfupload.pm Sun Feb 22 23:27:03 2026
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# PDF Form Upload Handler
#
-# $Id: lonpdfupload.pm,v 1.31 2026/02/21 16:03:57 raeburn Exp $
+# $Id: lonpdfupload.pm,v 1.32 2026/02/22 23:27:03 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -207,26 +207,84 @@
my @formFields = $pdf->getFormFieldList(); #get names of form fields
foreach my $field (@formFields) {
+ my $value;
my $dict = $pdf->getFormFieldDict($pdf->getFormField($field)); # get form field dictonary
- # this is necessary because CAM::PDF has a problem with form fieldnames which include a
- # dot in fieldnames. So a fieldname like "i.am.aFormfield" will offer three fieldnames
- # "i", "i.am" and "i.am.aFormfield". The fragmentary names keep no values and will be ignored.
- if($dict->{'V'}) {
- my $decoder = Encode::Guess->guess($field);
- if (ref($decoder)) {
- $field = $decoder->decode($field);
- }
- push(@data, $field."?". $dict->{'V'}{'value'}); #binding fieldname with value
+# Checking for $dict->{'V'}, present in rev. 1.19 to rev. 1.31
+# to handle fragmentary names from form fieldnames containing
+# a dot, eliminated for rev. 1.32 as fieldnames now only contain
+# encrypted symbs which will not contain any dots.
+# This allows checkboxes which are unchecked to be included
+# as form fields to support checkbox mode for optionresponse.
+
+ if ((ref($dict)) && (ref($dict->{'V'}))) {
+ $value = $dict->{'V'}{'value'};
+ }
+ $field = &check_encoding($field);
+ if ($value ne '') {
+ $value = &check_encoding($value);
}
+ push(@data, $field."?". $value); #binding fieldname with value
}
}
return @data;
}
+sub check_encoding {
+ my ($data) = @_;
+
+# ps2pdf13 used by LON-CAPA to output PDFs produces PDF format 1.3.
+# For pre-PDF 2.0, Adobe Acrobat attempts to use "best host encoding" for
+# values being submitted, which will depend on characters entered by the user.
+#
+# Encode::Guess checks ascii, utf8 and UTF-16/32 with BOM, by default
+
+ my $decoder = Encode::Guess->guess($data);
+ if (ref($decoder)) {
+ return Encode::encode('UTF-8',$decoder->decode($data));
+ } else {
+ # If encoding yet to be guessed, reuse code from pre_xml.pm
+ # try iso-2022-jp first
+ $decoder = Encode::Guess->guess($data, 'iso-2022-jp');
+ if (ref($decoder)) {
+ return Encode::encode('UTF-8',$decoder->decode($data));
+ } else {
+
+# NOTE: cp1252 is identical to iso-8859-1 but with additional characters in range 128-159
+# instead of control codes. Assume these control codes absent, so test for cp1252.
+# Use frequent non-ASCII characters to distinguish between cp1252 and MacRoman
+# (languages: mostly German, Spanish, Portuguese)
+
+ my $decoded_windows = Encode::decode('cp1252', $data);
+ my $decoded_mac = Encode::decode('MacRoman', $data);
+
+# Ã has been removed because it conflicts with â and â is more frequent
+# ± has been removed because it is, suprisingly, the same code in both encodings !
+
+ my $score_windows = $decoded_windows =~ tr/Ãáà äâãçéèêëñóöôõúüÃÃÃÃÿ¡ââââ °½âââ¦Â§//;
+ my $score_mac = $decoded_mac =~ tr/Ãáà äâãçéèêëñóöôõúüÃÃÃÃÿ¡ââââ °½âââ¦Â§//;
+ # check newlines too (\r on MacOS < X, \r\n on Windows)
+ my $ind_cr = index($data, "\r");
+ if ($ind_cr != -1) {
+ if (substr($data, $ind_cr + 1, 1) eq "\n") {
+ $score_windows++;
+ } else {
+ $score_mac++;
+ }
+ }
+ if ($score_windows >= $score_mac) {
+ return Encode::encode('UTF-8',$decoded_windows);
+ } else {
+ return Encode::encode('UTF-8',$decoded_mac);
+ }
+ }
+ }
+}
+
sub grade_pdf {
my @pdfdata = @_;
- my ($result,$meta,%grades,%problems,%foreigncourse,%mismatchuser,%hwvals,$debug);
+ my ($result,$meta,%grades,%problems,%foreigncourse,%mismatchuser,
+ %types,%checkboxoff,$debug);
my $navmap = Apache::lonnavmaps::navmap->new();
if (!defined($navmap)) {
@@ -296,6 +354,10 @@
if ($type eq 'radiobuttonresponse' && $value eq 'Off' ) {
next;
}
+ if (($type eq 'optionresponse') && ($HWVAL =~ /^HWHDN([^:]+):0$/)) {
+ $checkboxoff{$symb.$part}{'HWVAL'.$1} = $value;
+ next;
+ }
my $submit = $part;
$submit =~ s/part_(.*)/submit_$1/;
@@ -325,7 +387,7 @@
$problems{$symb.$part}{'inputid'} = $input_id;
}
}
- $hwvals{$symb.$part}{$HWVAL} = 1;
+ $types{$symb.$part}{$HWVAL} = $type;
} else {
$debug .= 'found: -> '.$entry;
next;
@@ -345,8 +407,8 @@
foreach my $key (sort(keys(%problems))) {
my %problem = %{$problems{$key}};
- if (ref($hwvals{$key}) eq 'HASH') {
- foreach my $hwval (keys(%{$hwvals{$key}})) {
+ if (ref($types{$key}) eq 'HASH') {
+ foreach my $hwval (keys(%{$types{$key}})) {
if (ref($problem{$hwval}) eq 'HASH') {
my %valhash = %{$problem{$hwval}};
my %ordered;
@@ -360,6 +422,14 @@
push(@{$problem{$hwval}},$ordered{$digit});
}
}
+ } elsif (ref($checkboxoff{$key}) eq 'HASH') {
+ if (($problem{$hwval} eq '') &&
+ ($types{$key}{$hwval} eq 'optionresponse')) {
+ my ($hwvalprefix) = split(/:/,$hwval);
+ if ($checkboxoff{$key}{$hwvalprefix} ne '') {
+ $problem{$hwval} = $checkboxoff{$key}{$hwvalprefix};
+ }
+ }
}
}
}
More information about the LON-CAPA-cvs
mailing list