[LON-CAPA-cvs] cvs: rat /client parameter.html loncom/interface courseprefs.pm lonhtmlcommon.pm lonparmset.pm

raeburn raeburn at source.lon-capa.org
Thu Oct 29 19:24:13 EDT 2020


raeburn		Thu Oct 29 23:24:13 2020 EDT

  Modified files:              
    /loncom/interface	lonhtmlcommon.pm courseprefs.pm lonparmset.pm 
    /rat/client	parameter.html 
  Log:
  - Bug 6907 
    - deeplink parameter incorporates four components:
    (a) In Contents or Gradebook?, (b) Access scope via deep-link,
    (c) Supported Link Types, (d) Menu Items Displayed
        - Allowed values for (c) 'any','only','key','lti'
    - If (c) set to key, ':key value' appended; characters allowed in key:
      a-zA-Z\d_.!@#$%^&*()+=-
    - (d) is either 0 (i.e., standard menu), or numbered menu collection from
      Settings > Course Settings > Menu display
  
  
-------------- next part --------------
Index: loncom/interface/lonhtmlcommon.pm
diff -u loncom/interface/lonhtmlcommon.pm:1.396 loncom/interface/lonhtmlcommon.pm:1.397
--- loncom/interface/lonhtmlcommon.pm:1.396	Sun Aug 30 20:30:22 2020
+++ loncom/interface/lonhtmlcommon.pm	Thu Oct 29 23:24:13 2020
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common html routines
 #
-# $Id: lonhtmlcommon.pm,v 1.396 2020/08/30 20:30:22 raeburn Exp $
+# $Id: lonhtmlcommon.pm,v 1.397 2020/10/29 23:24:13 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -866,13 +866,14 @@
 ##############################################
 sub pjump_javascript_definition {
     my $Str = <<END;
-    function pjump(type,dis,value,marker,ret,call,hour,min,sec) {
+    function pjump(type,dis,value,marker,ret,call,hour,min,sec,extra) {
         openMyModal("/adm/rat/parameter.html?type="+escape(type)
                  +"&value="+escape(value)+"&marker="+escape(marker)
                  +"&return="+escape(ret)
                  +"&call="+escape(call)+"&name="+escape(dis)
                  +"&defhour="+escape(hour)+"&defmin="+escape(min)
-                 +"&defsec="+escape(sec)+"&modal=1",350,350,'no');
+                 +"&defsec="+escape(sec)+"&extra="+escape(extra)
+                 +"&modal=1",350,350,'no');
     }
 END
     return $Str;
Index: loncom/interface/courseprefs.pm
diff -u loncom/interface/courseprefs.pm:1.90 loncom/interface/courseprefs.pm:1.91
--- loncom/interface/courseprefs.pm:1.90	Thu Oct 29 17:14:23 2020
+++ loncom/interface/courseprefs.pm	Thu Oct 29 23:24:13 2020
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Handler to set configuration settings for a course
 #
-# $Id: courseprefs.pm,v 1.90 2020/10/29 17:14:23 raeburn Exp $
+# $Id: courseprefs.pm,v 1.91 2020/10/29 23:24:13 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -367,7 +367,7 @@
     my %values=&Apache::lonnet::dump('environment',$cdom,$cnum);
     my @prefs_order = ('courseinfo','localization','feedback','discussion',
                        'classlists','appearance','grading','printouts',
-                       'spreadsheet','bridgetasks','lti','other');
+                       'menuitems','spreadsheet','bridgetasks','lti','other');
 
     my %prefs = (
         'courseinfo' =>
@@ -542,6 +542,21 @@
                          'lti.lcmenu'     => 'Menu items',
                                 },
                   },
+        'menuitems' =>
+                   {
+                     text => 'Menu display',
+                     help => 'Course_Prefs_Menus',
+                     header => [{col1 => 'Default Menu',
+                                 col2 => 'Value',},
+                                {col1 => 'Menu collections',
+                                 col2 => 'Settings',
+                                }],
+                     ordered => ['menudefault','menucollections'],
+                     itemtext => {
+                         menudefault     => 'Choose default collection of menu items for course',
+                         menucollections => 'Menu collections',
+                                 },
+                   },
         'other' =>
                   { text => 'Other settings',
                     help => 'Course_Prefs_Other',
@@ -557,7 +572,13 @@
                                                   $cnum,undef,\@allitems,
                                                   'coursepref',$parm_permission);
     } elsif (($phase eq 'display') && ($parm_permission->{'display'})) {
-        my $jscript = &get_jscript($cid,$cdom,$phase,$crstype,\%values);
+        my $noedit;
+        if (ref($parm_permission) eq 'HASH') {
+            unless ($parm_permission->{'process'}) {
+                $noedit = 1;
+            }
+        }
+        my $jscript = &get_jscript($cid,$cdom,$phase,$crstype,\%values,$noedit);
         my @allitems = &get_allitems(%prefs);
         &Apache::lonconfigsettings::display_settings($r,$cdom,$phase,$context,
             \@prefs_order,\%prefs,\%values,undef,$jscript,\@allitems,$crstype,
@@ -634,7 +655,7 @@
     }
     $output .= '</span></th>'."\n".
                '</tr>';
-    if (($action eq 'feedback') || ($action eq 'classlists')) {
+    if (($action eq 'feedback') || ($action eq 'classlists') || ($action eq 'menuitems')) {
         $output .= '
           <tr>
            <td>
@@ -659,6 +680,8 @@
             $output .= &print_feedback('top',$cdom,$settings,$ordered,$itemtext,\$rowtotal,$noedit);
         } elsif ($action eq 'classlists') {
             $output .= &print_classlists('top',$cdom,$settings,$itemtext,\$rowtotal,$crstype,$noedit);
+        } elsif ($action eq 'menuitems') {
+            $output .= &print_menuitems('top',$cdom,$settings,$itemtext,\$rowtotal,$crstype,$noedit);
         }
         $output .= '
            </table>
@@ -739,6 +762,8 @@
         $output .= &print_bridgetasks($cdom,$settings,$ordered,$itemtext,\$rowtotal,$crstype,$noedit);
     } elsif ($action eq 'lti') {
         $output .= &print_lti($cdom,$settings,$ordered,$itemtext,\$rowtotal,$crstype,$noedit);
+    } elsif ($action eq 'menuitems') {
+        $output .= &print_menuitems('bottom',$cdom,$settings,$itemtext,\$rowtotal,$crstype,$noedit);
     } elsif ($action eq 'other') {
         $output .= &print_other($cdom,$settings,$allitems,\$rowtotal,$crstype,$noedit);
     }
@@ -831,6 +856,81 @@
                             $changes->{$ext_entry} = $newvalues{$ext_entry};
                         }
                     }
+                } elsif ($action eq 'menuitems') {
+                    my (%current, at colls);
+                    my $next = 1;
+                    if ($values->{'menucollections'}) {
+                        foreach my $item (split(/;/,$values->{'menucollections'})) {
+                            my ($num,$value) = split(/\%/,$item);
+                            if ($num =~ /^\d+$/) {
+                                unless (grep(/^$num$/, at colls)) {
+                                    push(@colls,$num);
+                                }
+                                my @entries = split(/\&/,$value);
+                                foreach my $entry (@entries) {
+                                    my ($name,$fields) = split(/=/,$entry);
+                                    $current{$num}{$name} = $fields;
+                                }
+                            }
+                        }
+                    }
+                    if (@colls) {
+                        @colls = sort { $a <=> $b } @colls;
+                        $next += $colls[-1];
+                    }
+                    if ($env{'form.menucollections_add'} eq $next) {
+                        push(@colls,$next);
+                    }
+                    my $currdef = $values->{'menudefault'};
+                    my $possdef = $env{'form.menudefault'};
+                    if (($possdef =~ /^\d+$/) && (grep(/^$possdef$/, at colls))) {
+                        if ($values->{'menudefault'} ne $possdef) {
+                            $changes->{'menudefault'} = $possdef;
+                        }
+                    } elsif ($values->{'menudefault'}) {
+                        $changes->{'menudefault'} = '';
+                    }
+                    my $menucoll;
+                    if (@colls) {
+                        my ($ordered,$cats) = &menuitems_categories();
+                        my %shortcats = &menuitems_abbreviations();
+                        foreach my $num (@colls) {
+                            my ($entry,%include);
+                            map { $include{$_}= 1; } &Apache::loncommon::get_env_multiple('form.menucollections_'.$num);
+                            foreach my $item (@{$ordered}) {
+                                if ($item eq 'shown') {
+                                    foreach my $type (@{$cats->{$item}}) {
+                                        $entry .= $type.'=';
+                                        if ($include{$type}) {
+                                            $entry .= 'y';
+                                        } else {
+                                            $entry .= 'n';
+                                        }
+                                        $entry .= '&';
+                                    }
+                                } else {
+                                    $entry .= $shortcats{$item}.'=';
+                                    foreach my $type (@{$cats->{$item}}) {
+                                        if ($include{$type}) {
+                                            $entry .= $type.',';
+                                        }
+                                    }
+                                    $entry =~ s/,$//;
+                                    $entry .= '&';
+                                }
+                            }
+                            $entry =~ s/\&$//;
+                            if ($menucoll) {
+                                $menucoll .= ';';
+                            }
+                            $menucoll .= $num.'%'.$entry;
+                        }
+                        if ($menucoll ne $values->{'menucollections'}) {
+                            $changes->{'menucollections'} = $menucoll;
+                        }
+                    } elsif ($values->{'menucollections'}) {
+                        $changes->{'menucollections'} = '';
+                    }
                 } else {
                     foreach my $entry (@ordered) {
                         if ($entry eq 'cloners') {
@@ -1530,6 +1630,16 @@
                                             }
                                         }
                                         $displayname = &mt($text);
+                                    } elsif ($item eq 'menuitems') {
+                                        unless ($changes->{$item}{$key} eq '') {
+                                            if ($key eq 'menudefault') {
+                                                $displayname = &mt('Default collection of menu items');
+                                                $displayval = &mt('Collection: [_1]',
+                                                                  $changes->{$item}{$key});
+                                            } elsif ($key eq 'menucollections') {
+                                                $displayval = &menucollections_display($changes->{$item}{$key});
+                                            }
+                                        }
                                     } else {
                                         $displayname = &mt($text);
                                     }
@@ -1605,10 +1715,19 @@
                                             } elsif (!exists($changes->{$item}{'lti.override'})) {
                                                 $output .= '<li>'.&mt('LTI settings only saved if Override is set to "Yes"').'</li>';
                                             }
+                                        } elsif ($item eq 'menuitems') {
+                                            if ($key eq 'menudefault') {
+                                                $output .= '<li>'.&mt("Default collection of menu items set to: 'Standard' (all menus shown)").'</li>';
+                                            } elsif ($key eq 'menucollections') {
+                                                $output .= '<li>'.&mt('Specific collections of menus no longer available').'</li>';
+                                            }
                                         } else {
                                             $output .= '<li>'.&Apache::lonhtmlcommon::confirm_success(&mt('Deleted setting for [_1]',
                                                        '<i>'.$displayname.'</i>')).'</li>';
                                         }
+                                    } elsif ($key eq 'menucollections') {
+                                        $output .= '<li>'.&Apache::lonhtmlcommon::confirm_success(&mt('Numbered menu collections:')).'<br />'.
+                                                   $displayval.'</li>';
                                     } else {
                                         $output .= '<li>'.&Apache::lonhtmlcommon::confirm_success(&mt('[_1] set to [_2]',
                                                    '<i>'.$displayname.'</i>',
@@ -1838,7 +1957,7 @@
 }
 
 sub get_jscript {
-    my ($cid,$cdom,$phase,$crstype,$settings) = @_;
+    my ($cid,$cdom,$phase,$crstype,$settings,$noedit) = @_;
     my ($can_toggle_cat,$can_categorize) = &can_modify_catsettings($cdom,$crstype);
     my ($jscript,$categorize_js,$loncaparev_js,$instcode_js);
     my $stubrowse_js = &Apache::loncommon::studentbrowser_javascript();
@@ -1964,11 +2083,94 @@
     }
 }
 ENDSCRIPT
+    my $menuitems_js;
+    unless ($noedit) {
+        my $collections;
+        my $next = 1;
+        if (ref($settings) eq 'HASH') {
+            if ($settings->{'menucollections'} ne '') {
+                my @current;
+                foreach my $item (split(/;/,$settings->{'menucollections'})) {
+                    my ($num) = split(/\%/,$item);
+                    if ($num =~ /^\d+$/) {
+                        push(@current,$num);
+                    }
+                }
+                $collections = join("','",sort { $a <=> $b } @current);
+                if ($collections) {
+                    $collections = "'$collections'";
+                }
+                $next += $current[-1];
+            }
+        }
+        my $deftext = &mt('Standard (all menus shown)');
+        $menuitems_js = <<ENDSCRIPT;
+function toggleAddmenucoll() {
+    if (document.getElementById('menucollections_add')) {
+        var state = 'none';
+        var add = document.getElementById('menucollections_add').checked;
+        if (add) {
+            state = 'inline-block';
+        }
+        var fieldsets = new Array('shown','text','links','list','inline');
+        for (var i=0; i<fieldsets.length; i++) {
+            if (document.getElementById('addmenucoll_'+fieldsets[i])) {
+                document.getElementById('addmenucoll_'+fieldsets[i]).style.display = state;
+            }
+        }
+        var box = document.getElementsByClassName('LC_menucoll_add');
+        if (box.length) {
+            for (var i=0; i<box.length; i++) {
+                if (add) {
+                    box[i].checked = true;
+                } else {
+                    box[i].checked = false;
+                }
+            }
+        }
+        if (document.getElementById('menudefault')) {
+            var menudef = document.getElementById('menudefault');
+            var currsel = menudef.selectedIndex;
+            var colls = new Array($collections);
+            menudef.options.length = 0;
+            if (!add) {
+                if (currsel == 1 + colls.length) {
+                    currsel = 0;
+                }
+            }
+            if (currsel == 0) {
+                menudef.options[0] = new Option('$deftext','',true,true);
+            } else {
+                menudef.options[0] = new Option('$deftext','',false,false);
+            }
+            if (colls.length) {
+                for (var i=0; i<colls.length; i++) {
+                    var idx = i+1;
+                    if (currsel == colls[i]) {
+                        menudef.options[idx] = new Option(colls[i],colls[i],true,true);
+                    } else {
+                        menudef.options[idx] = new Option(colls[i],colls[i],false,false);
+                    }
+                }
+            }
+            if (add) {
+                var addidx = 1 + colls.length;
+                if (currsel == addidx) {
+                     menudef.options[addidx] = new Option('$next','$next',true,true);
+                } else {
+                    menudef.options[addidx] = new Option('$next','$next',false,false);
+                }
+            }
+        }
+    }
+}
+ENDSCRIPT
+    }
     $jscript = '<script type="text/javascript" language="Javascript">'."\n".
                '// <![CDATA['."\n".  
                $browse_js."\n".$categorize_js."\n".$loncaparev_js."\n".
                $cloners_js."\n".$instcode_js.
-               $syllabus_js."\n".'//]]>'."\n".
+               $syllabus_js."\n".$menuitems_js."\n".'//]]>'."\n".
                '</script>'."\n".$stubrowse_js."\n";
     return $jscript;
 }
@@ -4595,6 +4797,247 @@
     );
 }
 
+sub print_menuitems {
+    my ($position,$cdom,$settings,$itemtext,$rowtotal,$crstype,$noedit) = @_;
+    unless ((ref($settings) eq 'HASH') && (ref($itemtext) eq 'HASH')) {
+        return;
+    }
+    if ($position eq 'top') {
+        my (%defaultmenu_options, at defaultmenu_order,$addcollection);
+        if ($settings->{'menucollections'} ne '') {
+            foreach my $item (split(/;/,$settings->{'menucollections'})) {
+                my ($num,$value) = split(/\%/,$item);
+                if ($num =~ /^\d+$/) {
+                    $defaultmenu_options{$num} = $num;
+                }
+            }
+            @defaultmenu_order = sort { $a <=> $b } keys(%defaultmenu_options);
+            $addcollection = $defaultmenu_order[-1] + 1;
+        } else {
+            $addcollection = 1;
+        }
+        $defaultmenu_options{$addcollection} = $addcollection;
+        my %items = (
+            'menudefault' => {
+                   text => '<b>'.&mt($itemtext->{'menudefault'}).'</b><br />'.
+                           &mt("(can be overriden in deep-link context)"),
+                   input => 'selectbox',
+                   options => \%defaultmenu_options,
+                   order  => \@defaultmenu_order,
+                   nullval => &mt('Standard (all menus shown)'),
+                            },
+        );
+        return &make_item_rows($cdom,\%items,['menudefault'],$settings,$rowtotal,$crstype,'menuitems',$noedit);
+    } else {
+        my %menu;
+        my $count = 0;
+        my $next = 1;
+        my ($datatable,$disabled);
+        if ($noedit) {
+            $disabled = ' disabled="disabled"';
+        }
+
+        my ($ordered,$cats) = &menuitems_categories();
+        my @order = @{$ordered};
+        my %categories = %{$cats};
+        my %menutitles = &menuitems_titles();
+        my %menufields = &menuitems_fields();
+
+        if ($settings->{'menucollections'} ne '') {
+            foreach my $item (split(/;/,$settings->{'menucollections'})) {
+                my ($num,$value) = split(/\%/,$item);
+                if ($num =~ /^\d+$/) {
+                    my @entries = split(/\&/,$value);
+                    foreach my $entry (@entries) {
+                        my ($name,$fields) = split(/=/,$entry);
+                        $menu{$num}{$name} = $fields;
+                    }
+                }
+            }
+            if (keys(%menu)) {
+                my @current = sort { $a <=> $b } keys(%menu);
+                $next += $current[-1];
+                foreach my $num (@current) {
+                    my %checked;
+                    my $on = ' checked="checked"';
+                    foreach my $key (keys(%{$menu{$num}})) {
+                        if (($key eq 'top') || ($key eq 'inline') || ($key eq 'main')) {
+                            if ($menu{$num}{$key} eq 'y') {
+                                $checked{$key} = $on;
+                            }
+                        } else {
+                            foreach my $field (split(/,/,$menu{$num}{$key})) {
+                                if (exists($menufields{$field})) {
+                                    $checked{$field} = $on;
+                                }
+                            }
+                        }
+                    }
+                    if (ref($menu{$num}) eq 'HASH') {
+                        $datatable .= &item_table_row_start('<em class="LC_nav_bar">'.$num.'</em>',$count,'','','','LC_left_item');
+                        foreach my $category (@order) {
+                            if ((ref($categories{$category}) eq 'ARRAY') && (@{$categories{$category}} > 0)) {
+                                $datatable .= '<fieldset style="vertical-align:top; display:inline-block"><legend>'.$menutitles{$category}.'</legend>'."\n";
+                                foreach my $field (@{$categories{$category}}) {
+                                    $datatable .= '<label><input type="checkbox" name="menucollections_'.$num.'" value="'.$field.'"'.$checked{$field}.$disabled.' />'.
+                                                  $menufields{$field}.'</label><br />';
+                                }
+                                $datatable .= '</fieldset>';
+                            }
+                        }
+                        $datatable .= &item_table_row_end();
+                        $count ++;
+                    }
+                }
+            }
+        } elsif ($noedit) {
+            my $text = &mt('No menu collections defined for this course.');
+            $datatable .= &item_table_row_start($text,$count);
+        }
+        unless ($noedit) {
+            my $add = '<label><input type="checkbox" name="menucollections_add" id="menucollections_add" value="'.$next.'" '.
+                      'onclick="javascript:toggleAddmenucoll();" />'.&mt('Add').'</label>';
+            $datatable .= &item_table_row_start($add,$count,'','','','LC_left_item');
+            foreach my $category (@order) {
+                if ((ref($categories{$category}) eq 'ARRAY') && (@{$categories{$category}} > 0)) {
+                    $datatable .= '<fieldset id="addmenucoll_'.$category.'" style="display:none; vertical-align:top;"><legend>'.$menutitles{$category}.'</legend>'."\n";
+                    foreach my $field (@{$categories{$category}}) {
+                        $datatable .= '<label><input type="checkbox" class="LC_menucoll_add" name="menucollections_'.$next.'" value="'.$field.'"'.$disabled.' />'.
+                                      $menufields{$field}.'</label><br />';
+                    }
+                    $datatable .= '</fieldset>';
+                }
+            }
+            $datatable .= &item_table_row_end();
+            $count ++;
+        }
+        return $datatable;
+    }
+}
+
+sub menuitems_abbreviations {
+    my %briefcats = (
+                     text => 'pt',
+                     links => 'p',
+                     list => 'ps',
+                     inline => 's',
+                    );
+    return %briefcats;
+}
+
+sub menuitems_categories {
+    my @order = ('shown','text','links','list','inline');
+    my %categories = (
+                       shown => ['top','inline','main'],
+                       text  => ['name','role','crs'],
+                       links => ['personal','menu','comm','roles','help','logout'],
+                       list => ['about','prefs','port','wish','anno','rss'],
+                       inline => ['cont','grades','chat','people','groups','resv','syll','feeds'],
+                     );
+    return (\@order,\%categories);
+}
+
+sub menuitems_titles {
+    return &Apache::lonlocal::texthash (
+        shown => 'Hierarchy',
+        text  => 'Header text',
+        links => 'Header links',
+        list => 'Drop-down list',
+        inline => 'Inline links',
+    );
+}
+
+sub menuitems_fields {
+    return &Apache::lonlocal::texthash (
+               top => 'Display header',
+               inline => 'Display inline menu',
+               main => 'Access to main menu',
+               personal => 'Personal',
+               menu => 'Home',
+               comm => 'Messages',
+               roles => 'Roles/Courses',
+               help => 'Help',
+               logout => 'Logout',
+               name => 'Fullname',
+               crs => 'Course Title',
+               role => 'Current Role',
+               about => 'Information',
+               prefs => 'Preferences',
+               port => 'Portfolio',
+               wish => 'Stored Links',
+               anno => 'Calendar',
+               rss => 'RSS Feeds',
+               cont => 'Contents',
+               grades => 'Grades',
+               chat => 'Chat',
+               people => 'People',
+               groups => 'Groups',
+               resv => 'Reservations',
+               syll => 'Syllabus',
+               feeds => 'Feeds',
+    );
+}
+
+sub menucollections_display {
+    my ($collections) = @_;
+    my %menu;
+    my ($ordered,$cats) = &menuitems_categories();
+    my @order = @{$ordered};
+    my %categories = %{$cats};
+    my %menutitles = &menuitems_titles();
+    my %menufields = &menuitems_fields();
+    foreach my $item (split(/;/,$collections)) {
+        my ($num,$value) = split(/\%/,$item);
+        if ($num =~ /^\d+$/) {
+            my @entries = split(/\&/,$value);
+            foreach my $entry (@entries) {
+                my ($name,$fields) = split(/=/,$entry);
+                $menu{$num}{$name} = $fields;
+            }
+        }
+    }
+    my $output = '';
+    if (keys(%menu)) {
+        my @current = sort { $a <=> $b } keys(%menu);
+        foreach my $num (@current) {
+            my %checked;
+            foreach my $key (keys(%{$menu{$num}})) {
+                if (($key eq 'top') || ($key eq 'inline') || ($key eq 'main')) {
+                    if ($menu{$num}{$key} eq 'y') {
+                        $checked{$key} = 1;
+                    }
+                } else {
+                    foreach my $field (split(/,/,$menu{$num}{$key})) {
+                        if (exists($menufields{$field})) {
+                            $checked{$field} = 1;
+                        }
+                    }
+                }
+            }
+            if (ref($menu{$num}) eq 'HASH') {
+                $output .= '<fieldset><legend>'.&mt('Collection [_1]',$num).'</legend>';
+                foreach my $category (@order) {
+                    if ((ref($categories{$category}) eq 'ARRAY') && (@{$categories{$category}} > 0)) {
+                        $output .= '<fieldset style="vertical-align:top; display:inline-block">'.
+                                   '<legend>'.$menutitles{$category}.'</legend>'."\n";
+                        foreach my $field (@{$categories{$category}}) {
+                            if ($checked{$field}) {
+                                $output .= &Apache::lonhtmlcommon::confirm_success($menufields{$field});
+                            } else {
+                                $output .= &Apache::lonhtmlcommon::confirm_success($menufields{$field},1);
+                            }
+                            $output .= '<br />';
+                        }
+                        $output .= '</fieldset>';
+                    }
+                }
+                $output .= '</fieldset>';
+            }
+        }
+    }
+    return $output;
+}
+
 sub print_other {
     my ($cdom,$settings,$allitems,$rowtotal,$crstype,$noedit) = @_;
     unless ((ref($settings) eq 'HASH') && (ref($allitems) eq 'ARRAY')) {
@@ -4646,17 +5089,23 @@
 }
 
 sub item_table_row_start {
-    my ($text,$count,$add_class,$colspan) = @_;
+    my ($text,$count,$add_class,$colspan,$leftclass,$rightclass) = @_;
     my $output;
     my $css_class = ($count % 2) ? 'LC_odd_row' : 'LC_even_row';
     $css_class = (join(' ',$css_class,$add_class)) unless ($add_class eq '');
+    if ($leftclass eq '') {
+        $leftclass = 'LC_left_item';
+    }
+    if ($rightclass eq '') {
+        $rightclass = 'LC_right_item';
+    }
     $output .= '<tr class="'.$css_class.'">'."\n".
-               '<td class="LC_left_item">'.$text.
+               '<td class="'.$leftclass.'">'.$text.
                '</td>';
-    if ($colspan) {
-        $output .= '<td class="LC_right_item" colspan="'.$colspan.'">';
+    if ($colspan > 1) {
+        $output .= '<td class="'.$rightclass.'" colspan="'.$colspan.'">';
     } else {
-        $output .= '<td class="LC_right_item">';
+        $output .= '<td class="'.$rightclass.'">';
     }
     return $output;
 }
@@ -4697,7 +5146,7 @@
 }
 
 sub select_from_options {
-    my ($item,$order,$options,$curr,$nullval,$multiple,$maxsize,$onchange,$noedit) = @_;
+    my ($item,$order,$options,$curr,$nullval,$multiple,$maxsize,$onchange,$noedit,$id) = @_;
     my $output;
     my $disabled;
     if ($noedit) {
@@ -4713,6 +5162,9 @@
                 $output .= ' size="'.$maxsize.'"';
             }
         }
+        if ($id ne '') {
+            $output .= ' id="'.$id.'"';
+        }
         $output .= $disabled.'>'."\n";
         if ($nullval ne '') {
             $output .= '<option value=""';
@@ -4801,12 +5253,16 @@
                 }
                 $datatable .= &yesno_radio($item,$settings,$unsetdefault,$valueyes,$valueno,$noedit);
             } elsif ($items->{$item}{input} eq 'selectbox') {
+                my $id;
+                if ($caller eq 'menuitems') {
+                    $id = $item;
+                }
                 my $curr = $settings->{$item};
                 $datatable .=
                     &select_from_options($item,$items->{$item}{'order'},
                                          $items->{$item}{'options'},$curr,
                                          $items->{$item}{'nullval'},
-                                         undef,undef,undef,$noedit);
+                                         undef,undef,undef,$noedit,$id);
             } elsif ($items->{$item}{input} eq 'textbox') {
                 my $disabled;
                 if ($noedit) {
Index: loncom/interface/lonparmset.pm
diff -u loncom/interface/lonparmset.pm:1.596 loncom/interface/lonparmset.pm:1.597
--- loncom/interface/lonparmset.pm:1.596	Wed Feb 12 16:25:47 2020
+++ loncom/interface/lonparmset.pm	Thu Oct 29 23:24:13 2020
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Handler to set parameters for assessments
 #
-# $Id: lonparmset.pm,v 1.596 2020/02/12 16:25:47 raeburn Exp $
+# $Id: lonparmset.pm,v 1.597 2020/10/29 23:24:13 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -1241,12 +1241,16 @@
     var tailLenient = /\.lenient$/;
     var patternRelWeight = /^\-?[\d.]+$/;
     var patternLenientStd = /^(yes|no|default)$/;
+    var ipRegExp = /^setip/;
     var ipallowRegExp = /^setipallow_/;
     var ipdenyRegExp = /^setipdeny_/; 
-    var deeplinkRegExp = /^deeplink_(listing|scope)_/;
-    var deeplinkUrlsRegExp = /^deeplink_urls_/;
-    var deeplinkltiRegExp = /^deeplink_lti_/;
-    var deeplinkkeyRegExp = /^deeplink_key_/;
+    var deeplinkRegExp = /^deeplink_/;
+    var dlListScopeRegExp = /^deeplink_(listing|scope)_/; 
+    var dlLinkUrlsRegExp = /^deeplink_urls_/;
+    var dlLtiRegExp = /^deeplink_lti_/;
+    var dlKeyRegExp = /^deeplink_key_/;
+    var dlMenusRegExp = /^deeplink_menus_/;
+    var dlCollsRegExp = /^deeplink_colls_/;
     var patternIP = /[\[\]\*\.a-zA-Z\d\-]+/;
     if ((document.parmform.elements.length != 'undefined')  && (document.parmform.elements.length) != 'null') {
         if (document.parmform.elements.length) {
@@ -1275,61 +1279,117 @@
                             }
                         }
                     }
-                } else if (ipallowRegExp.test(name)) {
-                    var identifier = name.replace(ipallowRegExp,'');
-                    var possallow = document.parmform.elements[i].value;
-                    possallow = possallow.replace(/^\s+|\s+$/g,'');
-                    if (patternIP.test(possallow)) {
-                        if (document.parmform.elements['set_'+identifier].value) {
-                            possallow = ','+possallow;
-                        }
-                        document.parmform.elements['set_'+identifier].value += possallow;
-                    }
-                } else if (ipdenyRegExp.test(name)) {
-                    var identifier = name.replace(ipdenyRegExp,'');
-                    var possdeny = document.parmform.elements[i].value;
-                    possdeny = possdeny.replace(/^\s+|\s+$/g,'');
-                    if (patternIP.test(possdeny)) {
-                        possdeny = '!'+possdeny;
-                        if (document.parmform.elements['set_'+identifier].value) {
-                            possdeny = ','+possdeny;
+                } else if (ipRegExp.test(name)) {
+                    if (ipallowRegExp.test(name)) {
+                        var identifier = name.replace(ipallowRegExp,'');
+                        var possallow = document.parmform.elements[i].value;
+                        possallow = possallow.replace(/^\s+|\s+$/g,'');
+                        if (patternIP.test(possallow)) {
+                            if (document.parmform.elements['set_'+identifier].value) {
+                                possallow = ','+possallow;
+                            }
+                            document.parmform.elements['set_'+identifier].value += possallow;
+                        }
+                    } else if (ipdenyRegExp.test(name)) {
+                        var identifier = name.replace(ipdenyRegExp,'');
+                        var possdeny = document.parmform.elements[i].value;
+                        possdeny = possdeny.replace(/^\s+|\s+$/g,'');
+                        if (patternIP.test(possdeny)) {
+                            possdeny = '!'+possdeny;
+                            if (document.parmform.elements['set_'+identifier].value) {
+                                possdeny = ','+possdeny;
+                            }
+                            document.parmform.elements['set_'+identifier].value += possdeny;
                         }
-                        document.parmform.elements['set_'+identifier].value += possdeny;
                     }
                 } else if (deeplinkRegExp.test(name)) {
-                    var identifier =  name.replace(deeplinkRegExp,'');
-                    var possdeeplink = document.parmform.elements[i].value;
-                    possdeeplink = possdeeplink.replace(/^\s+|\s+$/g,'');
-                    if (document.parmform.elements['set_'+identifier].value) {
-                        possdeeplink = ','+possdeeplink;
-                    }
-                    document.parmform.elements['set_'+identifier].value += possdeeplink;
-                } else if (deeplinkUrlsRegExp.test(name)) {
-                    if (document.parmform.elements[i].checked) {
-                        var identifier =  name.replace(deeplinkUrlsRegExp,'');
-                        var posslinkurl = document.parmform.elements[i].value;
-                        posslinkurl = posslinkurl.replace(/^\s+|\s+$/g,'');
-                        if (document.parmform.elements['set_'+identifier].value) {
-                            posslinkurl = ','+posslinkurl;
-                        }
-                        document.parmform.elements['set_'+identifier].value += posslinkurl;
-                    }
-                } else if (deeplinkltiRegExp.test(name)) {
-                    var identifier = name.replace(deeplinkltiRegExp,'');
-                    var posslti = document.parmform.elements[i].value;
-                    posslti = posslti.replace(/\D+/g,'');
-                    if (document.parmform.elements['set_'+identifier].value) {
-                        posslti = ':'+posslti;
-                    }
-                    document.parmform.elements['set_'+identifier].value += posslti;
-                } else if (deeplinkkeyRegExp.test(name)) {
-                    var identifier = name.replace(deeplinkkeyRegExp,'');
-                    var posskey = document.parmform.elements[i].value;
-                    posskey = posskey.replace(/\W+/g,'');
-                    if (document.parmform.elements['set_'+identifier].value) {
-                        posslti = ':'+posskey;
+                    if (dlListScopeRegExp.test(name)) {
+                        var identifier =  name.replace(dlListScopeRegExp,'');
+                        var idx = document.parmform.elements[i].selectedIndex;
+                        if (idx > 0) { 
+                            var possdeeplink = document.parmform.elements[i].options[idx].value
+                            possdeeplink = possdeeplink.replace(/^\s+|\s+$/g,'');
+                            if (document.parmform.elements['set_'+identifier].value) {
+                                possdeeplink = ','+possdeeplink;
+                            }
+                            document.parmform.elements['set_'+identifier].value += possdeeplink;
+                        }
+                    } else if (dlLinkUrlsRegExp.test(name)) {
+                        if (document.parmform.elements[i].checked) {
+                            var identifier =  name.replace(dlLinkUrlsRegExp,'');
+                            var posslinkurl = document.parmform.elements[i].value;
+                            posslinkurl = posslinkurl.replace(/^\s+|\s+$/g,'');
+                            if (document.parmform.elements['set_'+identifier].value) {
+                                posslinkurl = ','+posslinkurl;
+                            }
+                            document.parmform.elements['set_'+identifier].value += posslinkurl;
+                        }
+                    } else if (dlLtiRegExp.test(name)) {
+                        var identifier = name.replace(dlLtiRegExp,'');
+                        if (isRadioSet('deeplink_urls_'+identifier,'lti')) {
+                            var posslti = document.parmform.elements[i].value;
+                            posslti = posslti.replace(/\D+/g,'');
+                            if (posslti.length) {
+                                if (document.parmform.elements['set_'+identifier].value) {
+                                    posslti = ':'+posslti;
+                                }
+                                document.parmform.elements['set_'+identifier].value += posslti;
+                            } else {
+                                document.parmform.elements['set_'+identifier].value = '';
+                                alert("A link type of 'deep with LTI launch' was selected but no LTI launcher was selected.\nPlease select one, or choose a different supported link type.");
+                                return false;  
+                            }
+                        }
+                    } else if (dlKeyRegExp.test(name)) {
+                        var identifier = name.replace(dlKeyRegExp,'');
+                        if (isRadioSet('deeplink_urls_'+identifier,'key')) {
+                            var posskey = document.parmform.elements[i].value;
+                            posskey = posskey.replace(/^\s+|\s+$/g,'');
+                            var origlength = posskey.length;
+                            posskey = posskey.replace(/[^a-zA-Z\d_.!@#$%^&*()+=-]/g,'');
+                            var newlength = posskey.length;
+                            if (newlength > 0) {
+                                var change = origlength - newlength;
+                                if (change) {
+                                    alert(change+' disallowed character(s) removed from deeplink key'); 
+                                }
+                                if (document.parmform.elements['set_'+identifier].value) {
+                                    posskey = ':'+posskey;
+                                }
+                                document.parmform.elements['set_'+identifier].value += posskey;
+                            } else {
+                                document.parmform.elements['set_'+identifier].value = '';
+                                if (newlength < origlength) {
+                                    alert("A link type of 'deep with key' was selected but the key value was blank, after removing disallowed characters.\nPlease enter a key using one or more of: a-zA-Z0-9_.!@#$%^&*()+=-");
+                                } else {
+                                    alert("A link type of 'deep with key' was selected but the key value was blank.\nPlease enter a key.");
+                                }
+                                return false;
+                            }
+                        }
+                    } else if (dlMenusRegExp.test(name)) {
+                        if (document.parmform.elements[i].checked) {
+                            var identifier =  name.replace(dlMenusRegExp,'');
+                            var posslinkmenu = document.parmform.elements[i].value;
+                            posslinkmenu = posslinkmenu.replace(/^\s+|\s+$/g,'');
+                            if (posslinkmenu == 'std') {
+                                posslinkmenu = '0';
+                                if (document.parmform.elements['set_'+identifier].value) {
+                                    posslinkmenu = ','+posslinkmenu;
+                                }
+                                document.parmform.elements['set_'+identifier].value += posslinkmenu;
+                            }
+                        }
+                    } else if (dlCollsRegExp.test(name)) {
+                        var identifier =  name.replace(dlCollsRegExp,'');
+                        if (isRadioSet('deeplink_menus_'+identifier,'colls')) {
+                            var posslinkmenu = document.parmform.elements[i].value;
+                            if (document.parmform.elements['set_'+identifier].value) {
+                                posslinkmenu = ','+posslinkmenu;
+                            }
+                            document.parmform.elements['set_'+identifier].value += posslinkmenu;
+                        }
                     }
-                    document.parmform.elements['set_'+identifier].value += posskey;
                 }
             }
         }
@@ -1337,6 +1397,23 @@
     return true;
 }
 
+function isRadioSet(name,expected) {
+    var menuitems = document.getElementsByName(name);
+    var radioLength = menuitems.length;
+    result = false;
+    if (radioLength  > 1) {
+        for (var j=0; j<radioLength; j++) {
+            if (menuitems[j].checked) {
+                if (menuitems[j].value == expected) {
+                    result = true;
+                    break;
+                }
+            }
+        }
+    }
+    return result;
+}
+
 ENDSCRIPT
 }
 
@@ -1401,25 +1478,37 @@
         if (document.getElementById('deeplink_key_'+item+'_'+key)) {
             keybox = document.getElementById('deeplink_key_'+item+'_'+key);
         }
-        var ltidiv;
-        if (document.getElementById('deeplinkdiv_lti_'+item+'_'+key)) {
-            ltidiv = document.getElementById('deeplinkdiv_lti_'+item+'_'+key);
+        var divoption;
+        if (item == 'urls') {
+            divoption = 'lti'
+        } else {
+            if (item == 'menus') {
+                divoption = 'colls';
+            }
+        }
+        var seldiv;
+        if (document.getElementById('deeplinkdiv_'+divoption+'_'+item+'_'+key)) {
+            seldiv = document.getElementById('deeplinkdiv_'+divoption+'_'+item+'_'+key);
         }
         for (var i=0; i<radios.length; i++) {
             if (radios[i].checked) {
-                if (radios[i].value == 'lti') {
-                    ltidiv.style.display = 'inline-block';
-                    keybox.type = 'hidden';
-                    keybox.value = '';
-                } else {
-                    if (ltidiv != '') {
-                        ltidiv.style.display = 'none';
-                        form['deeplink_lti_'+key].selectedIndex = 0;
-                    }
-                    if (radios[i].value == 'key') {
-                        keybox.type = 'text';
-                    } else {
+                if (radios[i].value == divoption) {
+                    seldiv.style.display = 'inline-block';
+                    if (item == 'urls') {
                         keybox.type = 'hidden';
+                        keybox.value = '';
+                    }
+                } else {
+                    if (seldiv != '') {
+                        seldiv.style.display = 'none';
+                        form['deeplink_'+divoption+'_'+key].selectedIndex = 0;
+                    }
+                    if (item == 'urls') {
+                        if (radios[i].value == 'key') {
+                            keybox.type = 'text';
+                        } else {
+                            keybox.type = 'hidden';
+                        }
                     }
                 }
             }
@@ -1629,10 +1718,25 @@
         if (keys(%posslti)) {
             $extra = 'lti_';
             foreach my $lti (sort { $a <=> $b } keys(%posslti)) {
-                $extra .= $lti.':'.&js_escape($posslti{$lti}).',';
+                $extra .= $lti.':'.&escape($posslti{$lti}).',';
             }
             $extra =~ s/,$//;
         }
+        if ($env{'course.'.$env{'request.course.id'}.'.menucollections'}) {
+            my @colls;
+            foreach my $item (split(/;/,$env{'course.'.$env{'request.course.id'}.'.menucollections'})) {
+                my ($num,$value) = split(/\%/,$item);
+                if ($num =~ /^\d+$/) {
+                    push(@colls,$num);
+                }
+            }
+            if (@colls) {
+                if ($extra) {
+                    $extra .= '&';
+                }
+                $extra .= 'menus_'.join(',', at colls);
+            }
+        }
     }
     if ($parmlev eq 'general') {
         if ($uname) {
@@ -1766,7 +1870,7 @@
 # @param {boolean} $noeditgrp - true if no edit is allowed for group level parameters
 # @param {boolean} $readonly -true if editing not allowed.
 # @param {boolean} $ismaplevel - true if level is for a map.
-# @param {strring} $extra - extra informatio to pass to plink.
+# @param {string} $extra - extra information to pass to plink.
 sub print_td {
     my ($r,$which,$defbg,$result,$outpar,$mprefix,$value,$typeoutpar,$display,
         $noeditgrp,$readonly,$ismaplevel,$extra)=@_;
@@ -2517,7 +2621,7 @@
     }
 
     if (%grouphash) {
-        $groups=&mt('Group:').' <select name="cgroup"';
+        $groups=&mt('Group').': <select name="cgroup"';
         if (%sectionhash && $env{'form.action'} eq 'settable' && $currsec eq '') {
             $groups .= qq| onchange="group_or_section('cgroup')" |;
         }
@@ -4869,17 +4973,20 @@
 
 sub string_deeplink_selector {
     my ($thiskey, $showval, $readonly) = @_;
-    my (@components,%values, at current,%titles,%options,%optiontext,%defaults,%posslti);
-    @components = ('listing','scope','urls');
+    my (@components,%values, at current,%titles,%options,%optiontext,%defaults,
+        %selectnull,%posslti, at possmenus);
+    @components = ('listing','scope','urls','menus');
     %titles = &Apache::lonlocal::texthash (
                   listing => 'In Contents and/or Gradebook',
                   scope   => 'Access scope for link',
                   urls    => 'Supported link types',
+                  menus   => 'Menu Items Displayed',
               );
     %options = (
                    listing => ['full','absent','grades','details','datestatus'],
                    scope   => ['res','map','rec'],
                    urls    => ['any','only','key','lti'],
+                   menus   => ['std','colls'],
                );
     %optiontext = &Apache::lonlocal::texthash (
                     full       => 'Listed (linked) in both',
@@ -4894,16 +5001,25 @@
                     only       => 'deep only',
                     key        => 'deep with key',
                     lti        => 'deep with LTI launch',
+                    std        => 'Standard (all menus)',
+                    colls      => 'Numbered collection',
+                  );
+    %selectnull = &Apache::lonlocal::texthash (
+                    lti => 'Select Provider',
+                    colls => 'Select',
                   );
     if ($showval =~ /,/) {
+        %values=();
         @current = split(/,/,$showval);
         ($values{'listing'}) = ($current[0] =~ /^(full|absent|grades|details|datestatus)$/);
         ($values{'scope'}) = ($current[1] =~ /^(res|map|rec)$/);
-        ($values{'urls'}) = ($current[2] =~ /^(any|only|key:\w+|lti:\d+)$/);
+        ($values{'urls'}) = ($current[2] =~ /^(any|only|key:[a-zA-Z\d_.!\@#\$%^&*()+=-]+|lti:\d+)$/);
+        ($values{'menus'}) = ($current[3] =~ /^(\d+)$/);
     } else {
         $defaults{'listing'} = 'full';
         $defaults{'scope'} = 'res';
         $defaults{'urls'} = 'any';
+        $defaults{'menus'} = '0';
     }
     my $disabled;
     if ($readonly) {
@@ -4919,21 +5035,41 @@
             }
         }
     }
+    if ($env{'course.'.$env{'request.course.id'}.'.menucollections'}) {
+        foreach my $item (split(/;/,$env{'course.'.$env{'request.course.id'}.'.menucollections'})) {
+            my ($num,$value) = split(/\%/,$item);
+            if ($num =~ /^\d+$/) {
+                push(@possmenus,$num);
+            }
+        }
+    }
+
     my $output = '<input type="hidden" name="set_'.$thiskey.'" /><table><tr>';
-    foreach my $item ('listing','scope','urls') {
+    foreach my $item (@components) {
         $output .= '<th>'.$titles{$item}.'</th>';
     }
     $output .= '</tr><tr>';
     foreach my $item (@components) {
         $output .= '<td>';
-        if ($item eq 'urls') {
+        if (($item eq 'urls') || ($item eq 'menus')) {
             my $selected = $values{$item};
             foreach my $option (@{$options{$item}}) {
-                if ($option eq 'lti') {
+                if (($item eq 'urls') && ($option eq 'lti')) {
                     next unless (keys(%posslti));
+                } elsif (($item eq 'menus') && ($option eq 'colls')) {
+                    next unless (@possmenus);
                 }
                 my $checked;
-                if ($selected =~ /^\Q$option\E/) {
+                if ($item eq 'menus') {
+                    if (($selected =~ /^\d+$/) && (@possmenus) &&
+                        (grep(/^\Q$selected\E$/, at possmenus))) {
+                        if ($option eq 'colls') {
+                            $checked = ' checked="checked"';
+                        }
+                    } elsif (($option eq 'std') && ($selected == 0) && ($selected ne '')) {
+                        $checked = ' checked="checked"';
+                    }
+                } elsif ($selected =~ /^\Q$option\E/) {
                     $checked = ' checked="checked"';
                 }
                 my $onclick;
@@ -4944,7 +5080,7 @@
                 $output .= '<span class="LC_nobreak"><label>'.
                            '<input type="radio" name="deeplink_'.$item.'_'.$thiskey.'" value="'.$option.'"'.$onclick.$disabled.$checked.' />'."\n".
                            $optiontext{$option}.'</label>';
-                if ($option eq 'key') {
+                if (($item eq 'urls') && ($option eq 'key')) {
                     my $visibility="hidden";
                     my $currkey;
                     if ($checked) {
@@ -4952,26 +5088,42 @@
                         $currkey = (split(/\:/,$values{$item}))[1];
                     }
                     $output .= ' '.
-                        '<input type="'.$visibility.'" name="deeplink_'.$option.'_'.$thiskey.'" id="deeplink_'.$option.'_'.$item.'_'.$thiskey.'" value="'.$currkey.'" size="6"'.$disabled.' />';
-                } elsif ($option eq 'lti') {
+                        '<input type="'.$visibility.'" name="deeplink_'.$option.'_'.$thiskey.'" id="deeplink_'.$option.'_'.$item.'_'.$thiskey.'" value="'.$currkey.'" size="10"'.$disabled.' />';
+                } elsif (($option eq 'lti') || ($option eq 'colls')) {
                     my $display="none";
-                    my ($currlti,$blankcheck);
+                    my ($current,$blankcheck, at possibles);
                     if ($checked) {
                         $display = 'inline-block';
-                        $currlti = (split(/\:/,$values{$item}))[1];
+                        if ($option eq 'lti') {
+                            $current = (split(/\:/,$selected))[1];
+                        } else {
+                            $current = $selected;
+                        }
                     } else {
                         $blankcheck = ' selected="selected"';
                     }
+                    if ($option eq 'lti') {
+                        @possibles = keys(%posslti);
+                    } else {
+                        @possibles = @possmenus;
+                    }
                     $output .= '<div id="deeplinkdiv_'.$option.'_'.$item.'_'.$thiskey.'"'.
                                ' style="display: '.$display.'"> <select name="'.
-                               'deeplink_'.$option.'_'.$thiskey.'"'.$disabled.'>'.
-                               '<option value=""'.$blankcheck.'>'.&mt('Select Provider').'</option>'."\n";
-                    foreach my $lti (sort { $a <=> $b } keys(%posslti)) {
+                               'deeplink_'.$option.'_'.$thiskey.'"'.$disabled.'>';
+                    if (@possibles > 1) {
+                        $output .= '<option value=""'.$blankcheck.'>'.$selectnull{$option}.
+                                   '</option>'."\n";
+                    }
+                    foreach my $poss (sort { $a <=> $b } @possibles) {
                         my $selected;
-                        if ($lti == $currlti) {
+                        if (($poss == $current) || (scalar(@possibles) ==1)) {
                             $selected = ' selected="selected"';
                         }
-                        $output .= '<option value="'.$lti.'"'.$selected.'>'.$posslti{$lti}.'</option>';
+                        my $shown = $poss;
+                        if ($option eq 'lti') {
+                            $shown = $posslti{$poss};
+                        }
+                        $output .= '<option value="'.$poss.'"'.$selected.'>'.$shown.'</option>';
                     }
                     $output .= '</select></div>';
                 }
@@ -5035,7 +5187,7 @@
              => [['_allowfrom_','Hostname(s), or IP(s) from which access is allowed'],
                  ['_denyfrom_','Hostname(s) or IP(s) from which access is disallowed']], 
      'string_deeplink'
-             => [['on','Set choices for link protection, resource listing, and access scope']],
+             => [['on','Set choices for link protection, resource listing, access scope, and shown menu items']],
     );
    
 
@@ -5046,7 +5198,7 @@
               => [['_allowfrom_','[^\!]+'],
                   ['_denyfrom_','\!']],
          'string_deeplink'
-              => [['on','^(full|absent|grades|details|datestatus)\,(res|map|rec)\,(any|only|key\:\w+|lti\:\d+)$']],
+              => [['on','^(full|absent|grades|details|datestatus)\,(res|map|rec)\,(any|only|key\:\w+|lti\:\d+)\,(\d+|)$']],
     );
 
 my %stringtypes = (
Index: rat/client/parameter.html
diff -u rat/client/parameter.html:1.83 rat/client/parameter.html:1.84
--- rat/client/parameter.html:1.83	Wed Jun  3 12:58:32 2020
+++ rat/client/parameter.html	Thu Oct 29 23:24:13 2020
@@ -5,7 +5,7 @@
 The LearningOnline Network with CAPA
 Parameter Input Window
 //
-// $Id: parameter.html,v 1.83 2020/06/03 12:58:32 raeburn Exp $
+// $Id: parameter.html,v 1.84 2020/10/29 23:24:13 raeburn Exp $
 //
 // Copyright Michigan State University Board of Trustees
 //
@@ -402,7 +402,7 @@
                 if (sform.donebutton[i].value == '_done_proctor') {
                     if ((sform.donebutton_proctorkey.value == '') || 
                         (sform.donebutton_proctorkey.value == null)) {
-                        alert('Please provide a key for a proctor to enter when a student uses the "Done" button.');
+                        alert('Please enter a key for a proctor to enter when a student uses the "Done" button.');
                         return;
                     }
                 }
@@ -417,6 +417,8 @@
     var sform=choices.document.forms.sch;
     svalue = sform.deeplinklisted.options[sform.deeplinklisted.selectedIndex].value+',';
     svalue += sform.deeplinkacc.options[sform.deeplinkacc.selectedIndex].value+',';
+    var keyRegExp = /^[a-zA-Z\d_.!@#$%^&*()+=-]+$/;
+    var numRegExp = /^\d+$/;
     if (sform.deeplinktypes.length) {
         for (var i=0; i<sform.deeplinktypes.length; i++) {
             if (sform.deeplinktypes[i].checked) {
@@ -424,14 +426,39 @@
                 if (sform.deeplinktypes[i].value == 'key') {
                     var posskey = sform.deeplinkkey.value;
                     posskey = posskey.replace(/^\s+|\s+$/g,'');
-                    var keyRegExp = /^\w+$/;
                     if (keyRegExp.test(posskey)) {
                         svalue += ':'+posskey;
+                    } else {
+                        alert('Please enter a value for the key containing one or more of: a-zA-Z0-9_.!@#$%^&*()+=-\n'+
+                              'or choose a different supported link type.');
+                        return; 
                     }
                 } else if (sform.deeplinktypes[i].value == 'lti') {
                     var posslti = sform.linkposslti.options[sform.linkposslti.selectedIndex].value;
-                    if (posslti != '' && posslti != null) {
+                    if ((numRegExp.test(posslti)) && (posslti > 0)) {
                         svalue += ':'+posslti;
+                    } else {
+                        alert('Please select an LTI launcher, or choose a different supported link type.');
+                        return;
+                    }
+                }
+                break;
+            }
+        }
+    }
+    svalue += ',';
+    if (sform.deeplinkmenus.length) {
+        for (var i=0; i<sform.deeplinkmenus.length; i++) {
+            if (sform.deeplinkmenus[i].checked) {
+                if (sform.deeplinkmenus[i].value == 'std') {
+                    svalue += '0';
+                } else if (sform.deeplinkmenus[i].value == 'collnum') {
+                    var posscoll = sform.linkpossmenu.options[sform.linkpossmenu.selectedIndex].value;
+                    if ((numRegExp.test(posscoll)) && (posscoll > 0)) { 
+                        svalue += posscoll;
+                    } else {
+                        alert("Please select either a numbered collection or check 'Standard (all menus)'.");
+                        return;
                     }
                 }
                 break;
@@ -441,9 +468,9 @@
     assemble();
 }
 
-function toggleDeepLink() {
+function toggleDeepLink(caller) {
     var sform=choices.document.forms.sch;
-    if (sform.deeplinktypes.length) {
+    if ((caller == 'types') && (sform.deeplinktypes.length)) {
         var frame = window.frames["choices"];
         for (var i=0; i<sform.deeplinktypes.length; i++) {
             if (sform.deeplinktypes[i].checked) {
@@ -460,7 +487,7 @@
                     }
                     if (frame.document.getElementById('deeplinkltidiv')) {
                         if (sform.deeplinktypes[i].value == 'lti') {
-                            frame.document.getElementById('deeplinkltidiv').style.display='inline-block';
+                            frame.document.getElementById('deeplinkltidiv').style.display='block';
                         } else {
                             frame.document.getElementById('deeplinkltidiv').style.display='none';
                         }
@@ -470,10 +497,25 @@
             }
         }
     }
+    if ((caller == 'menus') && (sform.deeplinkmenus.length)) {
+        var frame = window.frames["choices"];
+        for (var i=0; i<sform.deeplinkmenus.length; i++) {
+            if (sform.deeplinkmenus[i].checked) {
+                if (frame.document.getElementById('deeplinkmenusdiv')) {
+                    if (sform.deeplinkmenus[i].value == 'collnum') {
+                        frame.document.getElementById('deeplinkmenusdiv').style.display='inline-block';
+                    } else {
+                        frame.document.getElementById('deeplinkmenusdiv').style.display='none';
+                    }
+                }
+                break;
+            }
+        }
+    }
 }
 
-function calldeeplink() {
-    return 'onclick="parent.toggleDeepLink()"';
+function calldeeplink(caller) {
+    return 'onclick="parent.toggleDeepLink(\''+caller+'\')"';
 }
 
 function integereval() {
@@ -1127,8 +1169,9 @@
            var linktypeparts = new Array();
            var ltikeyRegExp = /^(lti|key):(\w+)$/;
            var dlinkkeysty = 'hidden';
-           var dlinkltidivsty = 'none';
            var dlinkkeyval = '';
+           var dlinkltidivsty = 'none';
+           var dlinkmenusdivsty = 'none';
            if ((svalue != '') && (svalue != null)) {
                deeplinkvals = svalue.split(',');
                if (ltikeyRegExp.test(deeplinkvals[2])) {
@@ -1138,11 +1181,14 @@
                        dlinkkeysty = 'text';
                        dlinkkeyval = linktypeparts[1];
                    } else if (linktypeparts[0] == 'lti') {
-                       dlinkltidivsty = 'inline-block';
+                       dlinkltidivsty = 'block';
                    }
                }
+               if (deeplinkvals[3] >= 1) {
+                   dlinkmenusdivsty = 'inline-block';
+               }
            } else {
-               deeplinkvals = ['full','res','any'];
+               deeplinkvals = ['full','res','any','0'];
            }
            var deeplinklisting = new Array();
            deeplinklisting = ['full','absent','grades','details','datestatus'];
@@ -1155,7 +1201,7 @@
            var deeplinkurls = new Array();
            deeplinkurls = ['any','only','key','lti'];
            tablestart('Deep-linked items');
-           choicewrite('<tr><td>In Contents or Gradebook?</td><td>');
+           choicewrite('<tr><td>In Contents + Gradebook?</td><td>');
            choicewrite('<select name="deeplinklisted">');
            for (var i=0; i<deeplinklisting.length; i++) {
                choicewrite('<option value="'+deeplinklisting[i]+'"');
@@ -1178,65 +1224,106 @@
            choicewrite('<tr><td>Supported Link Types</td><td>');
            choicewrite('<span style="white-space: nowrap;"><label>');
            choicewrite('<input name="deeplinktypes" value="any"'+
-                  ' type="radio" '+calldeeplink());
+                  ' type="radio" '+calldeeplink('types'));
            if (deeplinkvals[2]=='any') { choicewrite(' checked="checked"'); }
            choicewrite(' /> regular + deep</label></span><br />');
            choicewrite('<span style="white-space: nowrap;"><label>');
            choicewrite('<input name="deeplinktypes" value="only"'+
-                  ' type="radio" '+calldeeplink());
+                  ' type="radio" '+calldeeplink('types'));
            if (deeplinkvals[2]=='only') { choicewrite(' checked="checked"'); }
            choicewrite(' /> deep only</label></span><br />');
            choicewrite('<span style="white-space: nowrap;"><label>');
            choicewrite('<input name="deeplinktypes" value="key"'+
-                  ' type="radio" '+calldeeplink());
+                  ' type="radio" '+calldeeplink('types'));
            if (deeplinkvals[2]=='key') { choicewrite(' checked="checked"'); }
            choicewrite(' /> deep with key</label>');
-           choicewrite('<input type="'+dlinkkeysty+'" name="deeplinkkey" id="deeplinkkey" value="'+dlinkkeyval+'" size="6" />');
+           choicewrite('<input type="'+dlinkkeysty+'" name="deeplinkkey" id="deeplinkkey" value="'+dlinkkeyval+'" size="10" />');
            choicewrite('</span><br />');
+
+           var possmenus = new Array();
            if ((pextra != '') && (pextra != null)) {
                var ltiRegExp = /^lti_/;
-               if (ltiRegExp.test(pextra)) {
-                   pextra = pextra.replace(ltiRegExp,'');
-                   var posslti = pextra.split(',');
-                   if (posslti.length >= 1) {
-                       var ltinums = new Array();
-                       var ltititles = new Array();
-                       for (var i=0; i<posslti.length; i++) {
-                           var entries = posslti[i].split(':');
-                           ltinums[i] = entries[0];
-                           ltititles[i] = decodeURI(entries[1]);
-                       }
-                       if (ltinums.length) {
-                           choicewrite('<span style="white-space: nowrap;"><label>');
-                           choicewrite('<input name="deeplinktypes" value="lti"'+
-                                       ' type="radio" '+calldeeplink());
-                           if (deeplinkvals[2]=='lti') { choicewrite(' checked="checked"'); }
-                           choicewrite(' /> deep with LTI launch</label>');
-                           choicewrite('<div id="deeplinkltidiv" style="display:'+dlinkltidivsty+'">');
-                           choicewrite('<select name="linkposslti">');
-                           var sel='';
-                           if (deeplinkvals[2]!='lti') {
-                               sel = ' selected="selected"';
+               var menusRegExp = /^menus_/;
+               var extras = pextra.split('&');
+               for (var i=0; i<extras.length; i++) {
+                   if (ltiRegExp.test(extras[i])) {
+                       extras[i] = extras[i].replace(ltiRegExp,'');
+                       var posslti = extras[i].split(',');
+                       if (posslti.length >= 1) {
+                           var ltinums = new Array();
+                           var ltititles = new Array();
+                           for (var j=0; j<posslti.length; j++) {
+                               var entries = posslti[j].split(':');
+                               ltinums[j] = entries[0];
+                               ltititles[j] = decodeURIComponent(entries[1]);
                            }
-                           if (ltinums.length > 1) {
-                               choicewrite('<option value=""'+sel+'>Please select</option>');
-                           }
-                           for (var i=0; i<ltinums.length; i++) {
-                               sel = '';
-                               if (deeplinkvals[2]=='lti') {
-                                   if (linktypeparts.length) {
-                                       if (ltinums[i] == linktypeparts[1]) {
-                                           sel = ' selected="selected"';
+                           if (ltinums.length) {
+                               choicewrite('<span style="white-space: nowrap;"><label>');
+                               choicewrite('<input name="deeplinktypes" value="lti"'+
+                                           ' type="radio" '+calldeeplink('types'));
+                               if (deeplinkvals[2]=='lti') { choicewrite(' checked="checked"'); }
+                               choicewrite(' /> deep with LTI launch</label>');
+                               choicewrite('<div id="deeplinkltidiv" style="display:'+dlinkltidivsty+'">');
+                               choicewrite('<select name="linkposslti">');
+                               var sel='';
+                               if (deeplinkvals[2]!='lti') {
+                                   sel = ' selected="selected"';
+                               }
+                               if (ltinums.length > 1) {
+                                   choicewrite('<option value=""'+sel+'>Select</option>');
+                               }
+                               for (var j=0; j<ltinums.length; j++) {
+                                   sel = '';
+                                   if (deeplinkvals[2]=='lti') {
+                                       if (linktypeparts.length) {
+                                           if (ltinums[j] == linktypeparts[1]) {
+                                               sel = ' selected="selected"';
+                                           }
                                        }
                                    }
+                                   choicewrite('<option value="'+ltinums[j]+'"'+sel+'>'+ltititles[j]+'</option>');
                                }
-                               choicewrite('<option value="'+ltinums[i]+'"'+sel+'>'+ltititles[i]+'</option>');
+                               choicewrite('</select></div></span><br />');
                            }
-                           choicewrite('</select></div></span><br />');
                        }
+                   } else if (menusRegExp.test(extras[i])) {
+                       extras[i] = extras[i].replace(menusRegExp,'');
+                       possmenus = extras[i].split(',');
                    }
                }
            }
+           choicewrite('<tr><td>Menu Items Displayed</td><td>');
+           choicewrite('<span style="white-space: nowrap;"><label>');
+           choicewrite('<input name="deeplinkmenus" value="std"'+
+                       ' type="radio" '+calldeeplink('menus'));
+           if (deeplinkvals[3] == 0) {
+               choicewrite(' checked="checked"');
+           }
+           choicewrite(' /> Standard (all menus)</label></span><br />');
+           if (possmenus.length >= 1) {  
+               choicewrite('<span style="white-space: nowrap;"><label>');
+               choicewrite('<input name="deeplinkmenus" value="collnum"'+
+                           ' type="radio" '+calldeeplink('menus'));
+               if (deeplinkvals[3] > 0) { choicewrite(' checked="checked"'); }
+               choicewrite(' /> Numbered collection</label>');
+               choicewrite('<div id="deeplinkmenusdiv" style="display:'+dlinkmenusdivsty+'">');
+               choicewrite('<select name="linkpossmenu">');
+               var sel='';
+               if (deeplinkvals[3] == 0) {
+                   sel = ' selected="selected"';
+               }
+               if (possmenus.length > 1) {
+                   choicewrite('<option value=""'+sel+'>Select</option>');
+               }
+               for (var i=0; i<possmenus.length; i++) {
+                   sel = '';
+                   if (deeplinkvals[3] == possmenus[i]) {
+                       sel = ' selected="selected"';
+                   }
+                   choicewrite('<option value="'+possmenus[i]+'"'+sel+'>'+possmenus[i]+'</option>');
+               }
+               choicewrite('</select></div></span><br />');
+           }
            choicewrite('</td></tr></table>');
        }
    }
@@ -1628,6 +1715,9 @@
  
   selwrite('</body></html>');
   this.window.selector.document.close();
+  if (pscat == 'deeplink') {
+      document.getElementById("LCparampopup").rows="60,*";
+  }
   draw();
  
 }
@@ -1636,7 +1726,7 @@
 </script>
 </head>
 
-<frameset rows="125,*" onload="init();">
+<frameset id="LCparampopup" rows="125,*" onload="init();">
 <frame name="selector" src="empty.html" />
 <frame name="choices" src="empty.html" />
 </frameset>


More information about the LON-CAPA-cvs mailing list