Annotation of loncom/interface/lonnavmaps.pm, revision 1.520

1.481     foxr        1: # The LearningOnline Network with CAPA
1.2       www         2: # Navigate Maps Handler
1.1       www         3: #
1.520   ! raeburn     4: # $Id: lonnavmaps.pm,v 1.519 2016/04/04 00:34:37 raeburn Exp $
1.20      albertel    5: #
                      6: # Copyright Michigan State University Board of Trustees
                      7: #
                      8: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
                      9: #
                     10: # LON-CAPA is free software; you can redistribute it and/or modify
                     11: # it under the terms of the GNU General Public License as published by
                     12: # the Free Software Foundation; either version 2 of the License, or
                     13: # (at your option) any later version.
                     14: #
                     15: # LON-CAPA is distributed in the hope that it will be useful,
                     16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
                     17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
                     18: # GNU General Public License for more details.
                     19: #
                     20: # You should have received a copy of the GNU General Public License
                     21: # along with LON-CAPA; if not, write to the Free Software
                     22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
                     23: #
                     24: # /home/httpd/html/adm/gpl.txt
                     25: #
                     26: # http://www.lon-capa.org/
                     27: #
1.245     www        28: ###
1.2       www        29: 
1.416     jms        30: =pod
                     31: 
                     32: =head1 NAME
                     33: 
1.435     raeburn    34: Apache::lonnavmaps - Subroutines to handle and render the navigation
1.416     jms        35: 
                     36: =head1 SYNOPSIS
                     37: 
                     38: Handles navigational maps.
                     39: 
                     40: The main handler generates the navigational listing for the course,
                     41: the other objects export this information in a usable fashion for
                     42: other modules.
                     43: 
                     44: 
                     45: This is part of the LearningOnline Network with CAPA project
                     46: described at http://www.lon-capa.org.
                     47: 
                     48: 
                     49: =head1 OVERVIEW
                     50: 
1.439     wenzelju   51: X<lonnavmaps, overview> When a user enters a course, LON-CAPA examines the
1.416     jms        52: course structure and caches it in what is often referred to as the
                     53: "big hash" X<big hash>. You can see it if you are logged into
                     54: LON-CAPA, in a course, by going to /adm/test. (You may need to
                     55: tweak the /home/httpd/lonTabs/htpasswd file to view it.) The
                     56: content of the hash will be under the heading "Big Hash".
                     57: 
                     58: Big Hash contains, among other things, how resources are related
                     59: to each other (next/previous), what resources are maps, which 
                     60: resources are being chosen to not show to the student (for random
                     61: selection), and a lot of other things that can take a lot of time
                     62: to compute due to the amount of data that needs to be collected and
                     63: processed.
                     64: 
                     65: Apache::lonnavmaps provides an object model for manipulating this
                     66: information in a higher-level fashion than directly manipulating 
                     67: the hash. It also provides access to several auxilary functions 
                     68: that aren't necessarily stored in the Big Hash, but are a per-
                     69: resource sort of value, like whether there is any feedback on 
                     70: a given resource.
                     71: 
                     72: Apache::lonnavmaps also abstracts away branching, and someday, 
                     73: conditions, for the times where you don't really care about those
                     74: things.
                     75: 
                     76: Apache::lonnavmaps also provides fairly powerful routines for
                     77: rendering navmaps, and last but not least, provides the navmaps
                     78: view for when the user clicks the NAV button.
                     79: 
                     80: B<Note>: Apache::lonnavmaps I<only> works for the "currently
                     81: logged in user"; if you want things like "due dates for another
                     82: student" lonnavmaps can not directly retrieve information like
                     83: that. You need the EXT function. This module can still help,
                     84: because many things, such as the course structure, are constant
                     85: between users, and Apache::lonnavmaps can help by providing
                     86: symbs for the EXT call.
                     87: 
                     88: The rest of this file will cover the provided rendering routines, 
                     89: which can often be used without fiddling with the navmap object at
                     90: all, then documents the Apache::lonnavmaps::navmap object, which
                     91: is the key to accessing the Big Hash information, covers the use
                     92: of the Iterator (which provides the logic for traversing the 
                     93: somewhat-complicated Big Hash data structure), documents the
                     94: Apache::lonnavmaps::Resource objects that are returned by 
                     95: 
                     96: =head1 Subroutine: render
                     97: 
                     98: The navmap renderer package provides a sophisticated rendering of the
                     99: standard navigation maps interface into HTML. The provided nav map
                    100: handler is actually just a glorified call to this.
                    101: 
                    102: Because of the large number of parameters this function accepts,
                    103: instead of passing it arguments as is normal, pass it in an anonymous
                    104: hash with the desired options.
                    105: 
                    106: The package provides a function called 'render', called as
                    107: Apache::lonnavmaps::render({}).
                    108: 
                    109: =head2 Overview of Columns
                    110: 
                    111: The renderer will build an HTML table for the navmap and return
                    112: it. The table consists of several columns, and a row for each
                    113: resource (or possibly each part). You tell the renderer how many
                    114: columns to create and what to place in each column, optionally using
                    115: one or more of the prepared columns, and the renderer will assemble
                    116: the table.
                    117: 
                    118: Any additional generally useful column types should be placed in the
                    119: renderer code here, so anybody can use it anywhere else. Any code
                    120: specific to the current application (such as the addition of <input>
                    121: elements in a column) should be placed in the code of the thing using
                    122: the renderer.
                    123: 
                    124: At the core of the renderer is the array reference COLS (see Example
                    125: section below for how to pass this correctly). The COLS array will
                    126: consist of entries of one of two types of things: Either an integer
                    127: representing one of the pre-packaged column types, or a sub reference
                    128: that takes a resource reference, a part number, and a reference to the
                    129: argument hash passed to the renderer, and returns a string that will
                    130: be inserted into the HTML representation as it.
                    131: 
                    132: All other parameters are ways of either changing how the columns
                    133: are printing, or which rows are shown.
                    134: 
                    135: The pre-packaged column names are refered to by constants in the
                    136: Apache::lonnavmaps namespace. The following currently exist:
                    137: 
                    138: =over 4
                    139: 
                    140: =item * B<Apache::lonnavmaps::resource>:
                    141: 
                    142: The general info about the resource: Link, icon for the type, etc. The
                    143: first column in the standard nav map display. This column provides the
                    144: indentation effect seen in the B<NAV> screen. This column also accepts
                    145: the following parameters in the renderer hash:
                    146: 
                    147: =over 4
                    148: 
                    149: =item * B<resource_nolink>: default false
                    150: 
                    151: If true, the resource will not be linked. By default, all non-folder
                    152: resources are linked.
                    153: 
                    154: =item * B<resource_part_count>: default true
                    155: 
                    156: If true, the resource will show a part count B<if> the full
                    157: part list is not displayed. (See "condense_parts" later.) If false,
                    158: the resource will never show a part count.
                    159: 
                    160: =item * B<resource_no_folder_link>:
                    161: 
                    162: If true, the resource's folder will not be clickable to open or close
                    163: it. Default is false. True implies printCloseAll is false, since you
                    164: can't close or open folders when this is on anyhow.
                    165: 
1.493     raeburn   166: =item * B<map_no_edit_link>:
                    167: 
                    168: If true, the title of the folder or page will not be followed by an
                    169: icon/link to direct editing of a folder or composite page, originally
                    170: added via the Course Editor.
                    171: 
1.416     jms       172: =back
                    173: 
                    174: =item * B<Apache::lonnavmaps::communication_status>:
                    175: 
                    176: Whether there is discussion on the resource, email for the user, or
                    177: (lumped in here) perl errors in the execution of the problem. This is
                    178: the second column in the main nav map.
                    179: 
                    180: =item * B<Apache::lonnavmaps::quick_status>:
                    181: 
                    182: An icon for the status of a problem, with five possible states:
                    183: Correct, incorrect, open, awaiting grading (for a problem where the
                    184: computer's grade is suppressed, or the computer can't grade, like
                    185: essay problem), or none (not open yet, not a problem). The
                    186: third column of the standard navmap.
                    187: 
                    188: =item * B<Apache::lonnavmaps::long_status>:
                    189: 
                    190: A text readout of the details of the current status of the problem,
                    191: such as "Due in 22 hours". The fourth column of the standard navmap.
                    192: 
                    193: =item * B<Apache::lonnavmaps::part_status_summary>:
                    194: 
                    195: A text readout summarizing the status of the problem. If it is a
                    196: single part problem, will display "Correct", "Incorrect", 
                    197: "Not yet open", "Open", "Attempted", or "Error". If there are
                    198: multiple parts, this will output a string that in HTML will show a
                    199: status of how many parts are in each status, in color coding, trying
                    200: to match the colors of the icons within reason.
1.1       www       201: 
1.416     jms       202: Note this only makes sense if you are I<not> showing parts. If 
                    203: C<showParts> is true (see below), this column will not output
                    204: anything. 
1.2       www       205: 
1.416     jms       206: =back
1.133     bowersj2  207: 
1.416     jms       208: If you add any others please be sure to document them here.
1.133     bowersj2  209: 
1.416     jms       210: An example of a column renderer that will show the ID number of a
                    211: resource, along with the part name if any:
1.140     bowersj2  212: 
1.416     jms       213:  sub { 
                    214:   my ($resource, $part, $params) = @_;   
                    215:   if ($part) { return '<td>' . $resource->{ID} . ' ' . $part . '</td>'; }
                    216:   return '<td>' . $resource->{ID} . '</td>';
                    217:  }
1.135     bowersj2  218: 
1.416     jms       219: Note these functions are responsible for the TD tags, which allow them
                    220: to override vertical and horizontal alignment, etc.
1.133     bowersj2  221: 
1.416     jms       222: =head2 Parameters
1.130     www       223: 
1.416     jms       224: Minimally, you should be
                    225: able to get away with just using 'cols' (to specify the columns
                    226: shown), 'url' (necessary for the folders to link to the current screen
                    227: correctly), and possibly 'queryString' if your app calls for it. In
                    228: that case, maintaining the state of the folders will be done
                    229: automatically.
1.268     albertel  230: 
1.416     jms       231: =over 4
1.268     albertel  232: 
1.483     raeburn   233: =item * B<iterator>: default: constructs one from %env
1.51      bowersj2  234: 
1.416     jms       235: A reference to a fresh ::iterator to use from the navmaps. The
                    236: rendering will reflect the options passed to the iterator, so you can
                    237: use that to just render a certain part of the course, if you like. If
                    238: one is not passed, the renderer will attempt to construct one from
                    239: env{'form.filter'} and env{'form.condition'} information, plus the
                    240: 'iterator_map' parameter if any.
1.51      bowersj2  241: 
1.416     jms       242: =item * B<iterator_map>: default: not used
1.51      bowersj2  243: 
1.416     jms       244: If you are letting the renderer do the iterator handling, you can
                    245: instruct the renderer to render only a particular map by passing it
                    246: the source of the map you want to process, like
                    247: '/res/103/jerf/navmap.course.sequence'.
1.51      bowersj2  248: 
1.416     jms       249: =item * B<include_top_level_map>: default: false
1.51      bowersj2  250: 
1.416     jms       251: If you need to include the top level map (meaning the course) in the
                    252: rendered output set this to true
1.51      bowersj2  253: 
1.416     jms       254: =item * B<navmap>: default: constructs one from %env
1.51      bowersj2  255: 
1.416     jms       256: A reference to a navmap, used only if an iterator is not passed in. If
                    257: this is necessary to make an iterator but it is not passed in, a new
                    258: one will be constructed based on env info. This is useful to do basic
                    259: error checking before passing it off to render.
1.51      bowersj2  260: 
1.416     jms       261: =item * B<r>: default: must be passed in
1.53      bowersj2  262: 
1.416     jms       263: The standard Apache response object. This must be passed to the
                    264: renderer or the course hash will be locked.
1.53      bowersj2  265: 
1.416     jms       266: =item * B<cols>: default: empty (useless)
1.407     raeburn   267: 
1.416     jms       268: An array reference
1.53      bowersj2  269: 
1.416     jms       270: =item * B<showParts>:default true
1.115     bowersj2  271: 
1.416     jms       272: A flag. If true, a line for the resource itself, and a line
                    273: for each part will be displayed. If not, only one line for each
                    274: resource will be displayed.
1.115     bowersj2  275: 
1.416     jms       276: =item * B<condenseParts>: default true
1.115     bowersj2  277: 
1.416     jms       278: A flag. If true, if all parts of the problem have the same
                    279: status and that status is Nothing Set, Correct, or Network Failure,
                    280: then only one line will be displayed for that resource anyhow. If no,
                    281: all parts will always be displayed. If showParts is 0, this is
                    282: ignored.
1.115     bowersj2  283: 
1.416     jms       284: =item * B<jumpCount>: default: determined from %env
1.115     bowersj2  285: 
1.416     jms       286: A string identifying the URL to place the anchor 'curloc' at.
                    287: It is the responsibility of the renderer user to
                    288: ensure that the #curloc is in the URL. By default, determined through
                    289: the use of the env{} 'jump' information, and should normally "just
                    290: work" correctly.
1.177     www       291: 
1.416     jms       292: =item * B<here>: default: empty string
1.68      bowersj2  293: 
1.416     jms       294: A Symb identifying where to place the 'here' marker. The empty
                    295: string means no marker.
1.73      bowersj2  296: 
1.416     jms       297: =item * B<indentString>: default: 25 pixel whitespace image
1.347     www       298: 
1.416     jms       299: A string identifying the indentation string to use. 
1.347     www       300: 
1.416     jms       301: =item * B<queryString>: default: empty
1.346     foxr      302: 
1.416     jms       303: A string which will be prepended to the query string used when the
                    304: folders are opened or closed. You can use this to pass
                    305: application-specific values.
1.73      bowersj2  306: 
1.416     jms       307: =item * B<url>: default: none
1.73      bowersj2  308: 
1.416     jms       309: The url the folders will link to, which should be the current
                    310: page. Required if the resource info column is shown, and you 
                    311: are allowing the user to open and close folders.
1.73      bowersj2  312: 
1.416     jms       313: =item * B<currentJumpIndex>: default: no jumping
1.73      bowersj2  314: 
1.416     jms       315: Describes the currently-open row number to cause the browser to jump
                    316: to, because the user just opened that folder. By default, pulled from
                    317: the Jump information in the env{'form.*'}.
1.73      bowersj2  318: 
1.416     jms       319: =item * B<printKey>: default: false
1.101     bowersj2  320: 
1.416     jms       321: If true, print the key that appears on the top of the standard
                    322: navmaps.
1.73      bowersj2  323: 
1.416     jms       324: =item * B<printCloseAll>: default: true
1.73      bowersj2  325: 
1.416     jms       326: If true, print the "Close all folders" or "open all folders"
                    327: links.
1.403     albertel  328: 
1.416     jms       329: =item * B<filterFunc>: default: sub {return 1;} (accept everything)
1.346     foxr      330: 
1.416     jms       331: A function that takes the resource object as its only parameter and
                    332: returns a true or false value. If true, the resource is displayed. If
                    333: false, it is simply skipped in the display.
1.346     foxr      334: 
1.416     jms       335: =item * B<suppressEmptySequences>: default: false
1.346     foxr      336: 
1.416     jms       337: If you're using a filter function, and displaying sequences to orient
                    338: the user, then frequently some sequences will be empty. Setting this to
                    339: true will cause those sequences not to display, so as not to confuse the
                    340: user into thinking that if the sequence is there there should be things
                    341: under it; for example, see the "Show Uncompleted Homework" view on the
                    342: B<NAV> screen.
1.73      bowersj2  343: 
1.416     jms       344: =item * B<suppressNavmaps>: default: false
1.53      bowersj2  345: 
1.416     jms       346: If true, will not display Navigate Content resources. 
1.51      bowersj2  347: 
1.416     jms       348: =back
1.133     bowersj2  349: 
1.416     jms       350: =head2 Additional Info
1.174     albertel  351: 
1.416     jms       352: In addition to the parameters you can pass to the renderer, which will
                    353: be passed through unchange to the column renderers, the renderer will
                    354: generate the following information which your renderer may find
                    355: useful:
1.174     albertel  356: 
1.416     jms       357: =over 4
1.174     albertel  358: 
1.416     jms       359: =item * B<counter>: 
1.133     bowersj2  360: 
1.416     jms       361: Contains the number of rows printed. Useful after calling the render 
                    362: function, as you can detect whether anything was printed at all.
1.216     bowersj2  363: 
1.416     jms       364: =item * B<isNewBranch>:
1.216     bowersj2  365: 
1.416     jms       366: Useful for renderers: If this resource is currently the first resource
                    367: of a new branch, this will be true. The Resource column (leftmost in the
                    368: navmaps screen) uses this to display the "new branch" icon 
1.216     bowersj2  369: 
1.416     jms       370: =back
1.216     bowersj2  371: 
1.416     jms       372: =cut
1.216     bowersj2  373: 
                    374: 
1.416     jms       375: =head1 SUBROUTINES
1.216     bowersj2  376: 
1.416     jms       377: =over
1.216     bowersj2  378: 
1.416     jms       379: =item update()
1.133     bowersj2  380: 
1.416     jms       381: =item addToFilter()
1.133     bowersj2  382: 
1.416     jms       383: Convenience functions: Returns a string that adds or subtracts
                    384: the second argument from the first hash, appropriate for the 
                    385: query string that determines which folders to recurse on
1.174     albertel  386: 
1.416     jms       387: =item removeFromFilter()
1.51      bowersj2  388: 
1.416     jms       389: =item getLinkForResource()
1.51      bowersj2  390: 
1.416     jms       391: Convenience function: Given a stack returned from getStack on the iterator,
                    392: return the correct src() value.
1.174     albertel  393: 
1.416     jms       394: =item getDescription()
1.174     albertel  395: 
1.416     jms       396: Convenience function: This separates the logic of how to create
                    397: the problem text strings ("Due: DATE", "Open: DATE", "Not yet assigned",
                    398: etc.) into a separate function. It takes a resource object as the
                    399: first parameter, and the part number of the resource as the second.
                    400: It's basically a big switch statement on the status of the resource.
1.174     albertel  401: 
1.416     jms       402: =item dueInLessThan24Hours()
1.216     bowersj2  403: 
1.416     jms       404: Convenience function, so others can use it: Is the problem due in less than 24 hours, and still can be done?
1.51      bowersj2  405: 
1.416     jms       406: =item lastTry()
1.51      bowersj2  407: 
1.416     jms       408: Convenience function, so others can use it: Is there only one try remaining for the
                    409: part, with more than one try to begin with, not due yet and still can be done?
1.51      bowersj2  410: 
1.416     jms       411: =item advancedUser()
1.51      bowersj2  412: 
1.416     jms       413: This puts a human-readable name on the env variable.
1.51      bowersj2  414: 
1.416     jms       415: =item timeToHumanString()
1.51      bowersj2  416: 
1.416     jms       417: timeToHumanString takes a time number and converts it to a
                    418: human-readable representation, meant to be used in the following
                    419: manner:
1.174     albertel  420: 
1.416     jms       421: =over 4
1.51      bowersj2  422: 
1.416     jms       423: =item * print "Due $timestring"
1.133     bowersj2  424: 
1.416     jms       425: =item * print "Open $timestring"
1.133     bowersj2  426: 
1.416     jms       427: =item * print "Answer available $timestring"
1.144     bowersj2  428: 
1.133     bowersj2  429: =back
1.51      bowersj2  430: 
1.416     jms       431: Very, very, very, VERY English-only... goodness help a localizer on
                    432: this func...
1.174     albertel  433: 
1.417     jms       434: =item resource()
1.174     albertel  435: 
1.417     jms       436: returns 0
1.51      bowersj2  437: 
1.417     jms       438: =item communication_status()
1.51      bowersj2  439: 
1.417     jms       440: returns 1
1.174     albertel  441: 
1.417     jms       442: =item quick_status()
1.51      bowersj2  443: 
1.417     jms       444: returns 2
1.225     bowersj2  445: 
1.417     jms       446: =item long_status()
1.225     bowersj2  447: 
1.417     jms       448: returns 3
1.225     bowersj2  449: 
1.417     jms       450: =item part_status_summary()
1.51      bowersj2  451: 
1.417     jms       452: returns 4
1.51      bowersj2  453: 
1.417     jms       454: =item render_resource()
1.51      bowersj2  455: 
1.417     jms       456: =item render_communication_status()
1.51      bowersj2  457: 
1.417     jms       458: =item render_quick_status()
                    459: 
                    460: =item render_long_status()
                    461: 
                    462: =item render_parts_summary_status()
                    463: 
                    464: =item setDefault()
                    465: 
                    466: =item cmp_title()
                    467: 
                    468: =item render()
                    469: 
                    470: =item add_linkitem()
                    471: 
1.476     raeburn   472: =item show_linkitems_toolbar()
1.130     www       473: 
1.416     jms       474: =back
1.115     bowersj2  475: 
1.416     jms       476: =cut
1.140     bowersj2  477: 
1.416     jms       478: package Apache::lonnavmaps;
1.51      bowersj2  479: 
1.416     jms       480: use strict;
                    481: use GDBM_File;
                    482: use Apache::loncommon();
                    483: use Apache::lonenc();
                    484: use Apache::lonlocal;
                    485: use Apache::lonnet;
1.465     foxr      486: use Apache::lonmap;
1.443     foxr      487: 
1.416     jms       488: use POSIX qw (floor strftime);
                    489: use Time::HiRes qw( gettimeofday tv_interval );
                    490: use LONCAPA;
                    491: use DateTime();
1.486     raeburn   492: use HTML::Entities;
1.465     foxr      493: 
                    494: # For debugging
                    495: 
1.499     raeburn   496: #use Data::Dumper;
1.465     foxr      497: 
                    498: 
1.416     jms       499: # symbolic constants
                    500: sub SYMB { return 1; }
                    501: sub URL { return 2; }
                    502: sub NOTHING { return 3; }
1.174     albertel  503: 
1.416     jms       504: # Some data
1.174     albertel  505: 
1.416     jms       506: my $resObj = "Apache::lonnavmaps::resource";
1.174     albertel  507: 
1.443     foxr      508: # Keep these mappings in sync with lonquickgrades, which usesthe colors
1.416     jms       509: # instead of the icons.
                    510: my %statusIconMap = 
                    511:     (
                    512:      $resObj->CLOSED       => '',
                    513:      $resObj->OPEN         => 'navmap.open.gif',
                    514:      $resObj->CORRECT      => 'navmap.correct.gif',
                    515:      $resObj->PARTIALLY_CORRECT      => 'navmap.partial.gif',
                    516:      $resObj->INCORRECT    => 'navmap.wrong.gif',
                    517:      $resObj->ATTEMPTED    => 'navmap.ellipsis.gif',
                    518:      $resObj->ERROR        => ''
                    519:      );
1.401     albertel  520: 
1.438     wenzelju  521: my %iconAltTags =   #texthash does not work here
1.439     wenzelju  522:     ( 'navmap.correct.gif'  => 'Correct',
                    523:       'navmap.wrong.gif'    => 'Incorrect',
1.462     christia  524:       'navmap.open.gif'     => 'Is Open',
1.439     wenzelju  525:       'navmap.partial.gif'  => 'Partially Correct',
                    526:       'navmap.ellipsis.gif' => 'Attempted',
                    527:      );
1.401     albertel  528: 
1.416     jms       529: # Defines a status->color mapping, null string means don't color
                    530: my %colormap = 
                    531:     ( $resObj->NETWORK_FAILURE        => '',
                    532:       $resObj->CORRECT                => '',
                    533:       $resObj->EXCUSED                => '#3333FF',
                    534:       $resObj->PAST_DUE_ANSWER_LATER  => '',
                    535:       $resObj->PAST_DUE_NO_ANSWER     => '',
                    536:       $resObj->ANSWER_OPEN            => '#006600',
                    537:       $resObj->OPEN_LATER             => '',
                    538:       $resObj->TRIES_LEFT             => '',
                    539:       $resObj->INCORRECT              => '',
                    540:       $resObj->OPEN                   => '',
                    541:       $resObj->NOTHING_SET            => '',
                    542:       $resObj->ATTEMPTED              => '',
1.450     raeburn   543:       $resObj->CREDIT_ATTEMPTED       => '',
1.416     jms       544:       $resObj->ANSWER_SUBMITTED       => '',
                    545:       $resObj->PARTIALLY_CORRECT      => '#006600'
                    546:       );
                    547: # And a special case in the nav map; what to do when the assignment
                    548: # is not yet done and due in less than 24 hours
                    549: my $hurryUpColor = "#FF0000";
1.174     albertel  550: 
1.416     jms       551: sub addToFilter {
                    552:     my $hashIn = shift;
                    553:     my $addition = shift;
                    554:     my %hash = %$hashIn;
                    555:     $hash{$addition} = 1;
1.174     albertel  556: 
1.416     jms       557:     return join (",", keys(%hash));
                    558: }
1.174     albertel  559: 
1.416     jms       560: sub removeFromFilter {
                    561:     my $hashIn = shift;
                    562:     my $subtraction = shift;
                    563:     my %hash = %$hashIn;
1.174     albertel  564: 
1.416     jms       565:     delete $hash{$subtraction};
                    566:     return join(",", keys(%hash));
                    567: }
1.174     albertel  568: 
1.416     jms       569: sub getLinkForResource {
                    570:     my $stack = shift;
                    571:     my $res;
1.174     albertel  572: 
1.416     jms       573:     # Check to see if there are any pages in the stack
                    574:     foreach $res (@$stack) {
                    575:         if (defined($res)) {
                    576: 	    my $anchor;
                    577: 	    if ($res->is_page()) {
                    578: 		foreach my $item (@$stack) { if (defined($item)) { $anchor = $item; }  }
                    579: 		$anchor=&escape($anchor->shown_symb());
                    580: 		return ($res->link(),$res->shown_symb(),$anchor);
                    581: 	    }
                    582:             # in case folder was skipped over as "only sequence"
                    583: 	    my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb());
                    584: 	    if ($map=~/\.page$/) {
                    585: 		my $url=&Apache::lonnet::clutter($map);
1.437     foxr      586: 		$anchor=&escape($res->shown_symb());
1.416     jms       587: 		return ($url,$res->shown_symb(),$anchor);
                    588: 	    }
                    589:         }
                    590:     }
1.174     albertel  591: 
1.416     jms       592:     # Failing that, return the src of the last resource that is defined
                    593:     # (when we first recurse on a map, it puts an undefined resource
                    594:     # on the bottom because $self->{HERE} isn't defined yet, and we
                    595:     # want the src for the map anyhow)
                    596:     foreach my $item (@$stack) {
                    597:         if (defined($item)) { $res = $item; }
                    598:     }
1.174     albertel  599: 
1.416     jms       600:     if ($res) {
                    601: 	return ($res->link(),$res->shown_symb());
                    602:     }
                    603:     return;
                    604: }
1.174     albertel  605: 
1.417     jms       606: 
1.140     bowersj2  607: 
1.416     jms       608: sub getDescription {
                    609:     my $res = shift;
                    610:     my $part = shift;
                    611:     my $status = $res->status($part);
1.92      bowersj2  612: 
1.416     jms       613:     my $open = $res->opendate($part);
                    614:     my $due = $res->duedate($part);
                    615:     my $answer = $res->answerdate($part);
1.164     bowersj2  616: 
1.416     jms       617:     if ($status == $res->NETWORK_FAILURE) { 
                    618:         return &mt("Having technical difficulties; please check status later"); 
                    619:     }
                    620:     if ($status == $res->NOTHING_SET) {
1.494     raeburn   621:         return &Apache::lonhtmlcommon::direct_parm_link(&mt('Not currently assigned'),$res->symb(),'opendate',$part);
1.416     jms       622:     }
                    623:     if ($status == $res->OPEN_LATER) {
1.467     www       624:         return &mt("Open [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($open,'start'),$res->symb(),'opendate',$part));
1.416     jms       625:     }
1.509     raeburn   626:     my $slotinfo;
1.432     raeburn   627:     if ($res->simpleStatus($part) == $res->OPEN) {
                    628:         unless (&Apache::lonnet::allowed('mgr',$env{'request.course.id'})) {
                    629:             my ($slot_status,$slot_time,$slot_name)=$res->check_for_slot($part);
1.509     raeburn   630:             my $slotmsg;
1.432     raeburn   631:             if ($slot_status == $res->UNKNOWN) {
1.509     raeburn   632:                 $slotmsg = &mt('Reservation status unknown');
1.432     raeburn   633:             } elsif ($slot_status == $res->RESERVED) {
1.509     raeburn   634:                 $slotmsg = &mt('Reserved - ends [_1]',
1.433     bisitz    635:                            timeToHumanString($slot_time,'end'));
1.432     raeburn   636:             } elsif ($slot_status == $res->RESERVED_LOCATION) {
1.509     raeburn   637:                 $slotmsg = &mt('Reserved - specific location(s) - ends [_1]',
1.433     bisitz    638:                            timeToHumanString($slot_time,'end'));
1.432     raeburn   639:             } elsif ($slot_status == $res->RESERVED_LATER) {
1.509     raeburn   640:                 $slotmsg = &mt('Reserved - next open [_1]',
1.433     bisitz    641:                            timeToHumanString($slot_time,'start'));
1.432     raeburn   642:             } elsif ($slot_status == $res->RESERVABLE) {
1.509     raeburn   643:                 $slotmsg = &mt('Reservable, reservations close [_1]',
1.433     bisitz    644:                            timeToHumanString($slot_time,'end'));
1.432     raeburn   645:             } elsif ($slot_status == $res->RESERVABLE_LATER) {
1.509     raeburn   646:                 $slotmsg = &mt('Reservable, reservations open [_1]',
1.433     bisitz    647:                            timeToHumanString($slot_time,'start'));
1.432     raeburn   648:             } elsif ($slot_status == $res->NOT_IN_A_SLOT) {
1.509     raeburn   649:                 $slotmsg = &mt('Reserve a time/place to work');
1.432     raeburn   650:             } elsif ($slot_status == $res->NOTRESERVABLE) {
1.509     raeburn   651:                 $slotmsg = &mt('Reservation not available');
1.432     raeburn   652:             } elsif ($slot_status == $res->WAITING_FOR_GRADE) {
1.509     raeburn   653:                 $slotmsg = &mt('Submission in grading queue');
                    654:             }
                    655:             if ($slotmsg) {
                    656:                 if ($res->is_task() || !$due) {
                    657:                      return $slotmsg;
                    658:                 }
                    659:                 $slotinfo = ('&nbsp;' x 2).'('.$slotmsg.')';
1.432     raeburn   660:             }
                    661:         }
                    662:     }
1.416     jms       663:     if ($status == $res->OPEN) {
                    664:         if ($due) {
                    665: 	    if ($res->is_practice()) {
1.509     raeburn   666: 		return &mt("Closes [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'duedate',$part)).$slotinfo;
1.416     jms       667: 	    } else {
1.509     raeburn   668: 		return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)).$slotinfo;
1.416     jms       669: 	    }
                    670:         } else {
1.509     raeburn   671:             return &Apache::lonhtmlcommon::direct_parm_link(&mt("Open, no due date"),$res->symb(),'duedate',$part).$slotinfo;
1.416     jms       672:         }
                    673:     }
                    674:     if ($status == $res->PAST_DUE_ANSWER_LATER) {
1.467     www       675:         return &mt("Answer open [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($answer,'start'),$res->symb(),'answerdate',$part));
1.416     jms       676:     }
                    677:     if ($status == $res->PAST_DUE_NO_ANSWER) {
                    678: 	if ($res->is_practice()) {
1.467     www       679: 	    return &mt("Closed [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'answerdate,duedate',$part));
1.416     jms       680: 	} else {
1.467     www       681: 	    return &mt("Was due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'answerdate,duedate',$part));
1.416     jms       682: 	}
                    683:     }
                    684:     if (($status == $res->ANSWER_OPEN || $status == $res->PARTIALLY_CORRECT)
                    685: 	&& $res->handgrade($part) ne 'yes') {
1.455     www       686:         return &Apache::lonhtmlcommon::direct_parm_link(&mt("Answer available"),$res->symb(),'answerdate,duedate',$part);
1.416     jms       687:     }
                    688:     if ($status == $res->EXCUSED) {
                    689:         return &mt("Excused by instructor");
                    690:     }
                    691:     if ($status == $res->ATTEMPTED) {
1.444     raeburn   692:         if ($res->is_anonsurvey($part) || $res->is_survey($part)) {
                    693:             return &mt("Survey submission recorded");
                    694:         } else {
                    695:             return &mt("Answer submitted, not yet graded");
                    696:         }
1.416     jms       697:     }
1.450     raeburn   698:     if ($status == $res->CREDIT_ATTEMPTED) {
                    699:         if ($res->is_anonsurvey($part) || $res->is_survey($part)) {
                    700:             return &mt("Credit for survey submission");
                    701:         }
                    702:     }
1.416     jms       703:     if ($status == $res->TRIES_LEFT) {
                    704:         my $tries = $res->tries($part);
                    705:         my $maxtries = $res->maxtries($part);
                    706:         my $triesString = "";
                    707:         if ($tries && $maxtries) {
1.488     bisitz    708:             $triesString = '<span class="LC_fontsize_medium"><i>('.&mt('[_1] of [quant,_2,try,tries] used',$tries,$maxtries).')</i></span>';
1.416     jms       709:             if ($maxtries > 1 && $maxtries - $tries == 1) {
                    710:                 $triesString = "<b>$triesString</b>";
                    711:             }
                    712:         }
                    713:         if ($due) {
1.467     www       714:             return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)) .
1.416     jms       715:                 " $triesString";
                    716:         } else {
1.455     www       717:             return &Apache::lonhtmlcommon::direct_parm_link(&mt("No due date"),$res->symb(),'duedate',$part)." $triesString";
1.416     jms       718:         }
                    719:     }
                    720:     if ($status == $res->ANSWER_SUBMITTED) {
                    721:         return &mt('Answer submitted');
                    722:     }
                    723: }
1.92      bowersj2  724: 
1.51      bowersj2  725: 
1.416     jms       726: sub dueInLessThan24Hours {
                    727:     my $res = shift;
                    728:     my $part = shift;
                    729:     my $status = $res->status($part);
1.55      bowersj2  730: 
1.416     jms       731:     return ($status == $res->OPEN() ||
                    732:             $status == $res->TRIES_LEFT()) &&
                    733: 	    $res->duedate($part) && $res->duedate($part) < time()+(24*60*60) &&
                    734: 	    $res->duedate($part) > time();
                    735: }
1.84      bowersj2  736: 
1.417     jms       737: 
1.416     jms       738: sub lastTry {
                    739:     my $res = shift;
                    740:     my $part = shift;
1.115     bowersj2  741: 
1.416     jms       742:     my $tries = $res->tries($part);
                    743:     my $maxtries = $res->maxtries($part);
                    744:     return $tries && $maxtries && $maxtries > 1 &&
                    745:         $maxtries - $tries == 1 && $res->duedate($part) &&
                    746:         $res->duedate($part) > time();
                    747: }
1.55      bowersj2  748: 
1.51      bowersj2  749: 
1.416     jms       750: sub advancedUser {
                    751:     return $env{'request.role.adv'};
                    752: }
1.51      bowersj2  753: 
1.416     jms       754: sub timeToHumanString {
                    755:     my ($time,$type,$format) = @_;
1.153     bowersj2  756: 
1.416     jms       757:     # zero, '0' and blank are bad times
                    758:     if (!$time) {
                    759:         return &mt('never');
                    760:     }
                    761:     unless (&Apache::lonlocal::current_language()=~/^en/) {
                    762: 	return &Apache::lonlocal::locallocaltime($time);
                    763:     } 
                    764:     my $now = time();
1.174     albertel  765: 
1.416     jms       766:     # Positive = future
                    767:     my $delta = $time - $now;
1.190     bowersj2  768: 
1.416     jms       769:     my $minute = 60;
                    770:     my $hour = 60 * $minute;
                    771:     my $day = 24 * $hour;
                    772:     my $week = 7 * $day;
                    773:     my $inPast = 0;
1.190     bowersj2  774: 
1.416     jms       775:     # Logic in comments:
                    776:     # Is it now? (extremely unlikely)
                    777:     if ( $delta == 0 ) {
                    778:         return "this instant";
                    779:     }
1.174     albertel  780: 
1.416     jms       781:     if ($delta < 0) {
                    782:         $inPast = 1;
                    783:         $delta = -$delta;
                    784:     }
1.143     bowersj2  785: 
1.416     jms       786:     if ( $delta > 0 ) {
1.51      bowersj2  787: 
1.416     jms       788:         my $tense = $inPast ? " ago" : "";
                    789:         my $prefix = $inPast ? "" : "in ";
                    790:         
                    791:         # Less than a minute
                    792:         if ( $delta < $minute ) {
                    793:             if ($delta == 1) { return "${prefix}1 second$tense"; }
                    794:             return "$prefix$delta seconds$tense";
                    795:         }
1.51      bowersj2  796: 
1.416     jms       797:         # Less than an hour
                    798:         if ( $delta < $hour ) {
1.464     raeburn   799:             # If so, use minutes; or minutes, seconds (if format requires)
1.416     jms       800:             my $minutes = floor($delta / 60);
1.464     raeburn   801:             if (($format ne '') && ($format =~ /\%(T|S)/)) {
                    802:                 my $display;
                    803:                 if ($minutes == 1) {
                    804:                     $display = "${prefix}1 minute";
                    805:                 } else {
                    806:                     $display = "$prefix$minutes minutes";
                    807:                 }
                    808:                 my $seconds = $delta % $minute;
                    809:                 if ($seconds == 0) {
                    810:                     $display .= $tense;
                    811:                 } elsif ($seconds == 1) {
                    812:                     $display .= ", 1 second$tense";
                    813:                 } else {
                    814:                     $display .= ", $seconds seconds$tense";
                    815:                 }
                    816:                 return $display;
                    817:             }
1.416     jms       818:             if ($minutes == 1) { return "${prefix}1 minute$tense"; }
                    819:             return "$prefix$minutes minutes$tense";
                    820:         }
                    821:         
                    822:         # Is it less than 24 hours away? If so,
1.464     raeburn   823:         # display hours + minutes, (and + seconds, if format specified it)  
1.416     jms       824:         if ( $delta < $hour * 24) {
                    825:             my $hours = floor($delta / $hour);
                    826:             my $minutes = floor(($delta % $hour) / $minute);
                    827:             my $hourString = "$hours hours";
                    828:             my $minuteString = ", $minutes minutes";
                    829:             if ($hours == 1) {
                    830:                 $hourString = "1 hour";
                    831:             }
                    832:             if ($minutes == 1) {
                    833:                 $minuteString = ", 1 minute";
                    834:             }
                    835:             if ($minutes == 0) {
                    836:                 $minuteString = "";
                    837:             }
1.464     raeburn   838:             if (($format ne '') && ($format =~ /\%(T|S)/)) {
                    839:                 my $display = "$prefix$hourString$minuteString";
                    840:                 my $seconds = $delta-(($hours * $hour)+($minutes * $minute));
                    841:                 if ($seconds == 0) {
                    842:                     $display .= $tense;
                    843:                 } elsif ($seconds == 1) {
                    844:                     $display .= ", 1 second$tense";
                    845:                 } else {
                    846:                     $display .= ", $seconds seconds$tense";
                    847:                 }
                    848:                 return $display;
                    849:             }
1.416     jms       850:             return "$prefix$hourString$minuteString$tense";
                    851:         }
1.174     albertel  852: 
1.464     raeburn   853:         # Date/time is more than 24 hours away
                    854: 
1.416     jms       855: 	my $dt = DateTime->from_epoch(epoch => $time)
                    856: 	                 ->set_time_zone(&Apache::lonlocal::gettimezone());
1.216     bowersj2  857: 
1.464     raeburn   858: 	# If there's a caller supplied format, use it, unless it only displays
                    859:         # H:M:S or H:M.
1.145     bowersj2  860: 
1.464     raeburn   861: 	if (($format ne '') && ($format ne '%T') && ($format ne '%R')) {
1.416     jms       862: 	    my $timeStr = $dt->strftime($format);
                    863: 	    return $timeStr.' ('.$dt->time_zone_short_name().')';
                    864: 	}
1.216     bowersj2  865: 
1.416     jms       866:         # Less than 5 days away, display day of the week and
                    867:         # HH:MM
1.216     bowersj2  868: 
1.416     jms       869:         if ( $delta < $day * 5 ) {
                    870:             my $timeStr = $dt->strftime("%A, %b %e at %I:%M %P (%Z)");
                    871:             $timeStr =~ s/12:00 am/00:00/;
                    872:             $timeStr =~ s/12:00 pm/noon/;
                    873:             return ($inPast ? "last " : "this ") .
                    874:                 $timeStr;
                    875:         }
                    876:         
                    877: 	my $conjunction='on';
                    878: 	if ($type eq 'start') {
                    879: 	    $conjunction='at';
                    880: 	} elsif ($type eq 'end') {
                    881: 	    $conjunction='by';
                    882: 	}
                    883:         # Is it this year?
                    884: 	my $dt_now = DateTime->from_epoch(epoch => $now)
                    885: 	                     ->set_time_zone(&Apache::lonlocal::gettimezone());
                    886:         if ( $dt->year() == $dt_now->year()) {
                    887:             # Return on Month Day, HH:MM meridian
                    888:             my $timeStr = $dt->strftime("$conjunction %A, %b %e at %I:%M %P (%Z)");
                    889:             $timeStr =~ s/12:00 am/00:00/;
                    890:             $timeStr =~ s/12:00 pm/noon/;
                    891:             return $timeStr;
                    892:         }
1.59      bowersj2  893: 
1.416     jms       894:         # Not this year, so show the year
                    895:         my $timeStr = 
                    896: 	    $dt->strftime("$conjunction %A, %b %e %Y at %I:%M %P (%Z)");
                    897:         $timeStr =~ s/12:00 am/00:00/;
                    898:         $timeStr =~ s/12:00 pm/noon/;
                    899:         return $timeStr;
                    900:     }
                    901: }
1.59      bowersj2  902: 
                    903: 
1.133     bowersj2  904: sub resource { return 0; }
                    905: sub communication_status { return 1; }
                    906: sub quick_status { return 2; }
                    907: sub long_status { return 3; }
1.225     bowersj2  908: sub part_status_summary { return 4; }
1.51      bowersj2  909: 
1.133     bowersj2  910: sub render_resource {
                    911:     my ($resource, $part, $params) = @_;
1.51      bowersj2  912: 
1.477     raeburn   913:     my $editmapLink;
1.133     bowersj2  914:     my $nonLinkedText = ''; # stuff after resource title not in link
1.51      bowersj2  915: 
1.134     bowersj2  916:     my $link = $params->{"resourceLink"};
1.512     raeburn   917:     if ($resource->ext()) {
                    918:         $link =~ s/\#.+(\?)/$1/g;
                    919:     }
1.306     foxr      920: 
                    921:     #  The URL part is not escaped at this point, but the symb is... 
                    922: 
1.134     bowersj2  923:     my $src = $resource->src();
                    924:     my $it = $params->{"iterator"};
1.133     bowersj2  925:     my $filter = $it->{FILTER};
                    926: 
                    927:     my $title = $resource->compTitle();
1.258     albertel  928: 
1.133     bowersj2  929:     my $partLabel = "";
                    930:     my $newBranchText = "";
1.298     albertel  931:     my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons");
1.133     bowersj2  932:     # If this is a new branch, label it so
                    933:     if ($params->{'isNewBranch'}) {
1.429     droeschl  934:         $newBranchText = "<img src='$location/branch.gif' alt=".mt('Branch')." />";
1.51      bowersj2  935:     }
                    936: 
1.133     bowersj2  937:     # links to open and close the folder
1.306     foxr      938: 
1.427     droeschl  939:     my $whitespace = $location.'/whitespace_21.gif';
                    940:     my $linkopen = "<img src='$whitespace' alt='' />"."<a href=\"$link\">";
1.133     bowersj2  941:     my $linkclose = "</a>";
1.51      bowersj2  942: 
1.204     albertel  943:     # Default icon: unknown page
1.426     droeschl  944:     my $icon = "<img class=\"LC_contentImage\" src='$location/unknown.gif' alt='' />";
1.133     bowersj2  945:     
                    946:     if ($resource->is_problem()) {
1.192     bowersj2  947:         if ($part eq '0' || $params->{'condensed'}) {
1.426     droeschl  948: 	    $icon = '<img class="LC_contentImage" src="'.$location.'/';
1.389     albertel  949: 	    if ($resource->is_task()) {
                    950: 		$icon .= 'task.gif" alt="'.&mt('Task');
                    951: 	    } else {
                    952: 		$icon .= 'problem.gif" alt="'.&mt('Problem');
                    953: 	    }
1.426     droeschl  954: 	    $icon .='" />';
1.133     bowersj2  955:         } else {
                    956:             $icon = $params->{'indentString'};
                    957:         }
1.204     albertel  958:     } else {
1.426     droeschl  959: 	$icon = "<img class=\"LC_contentImage\" src='".&Apache::loncommon::icon($resource->src)."' alt='' />";
1.133     bowersj2  960:     }
1.51      bowersj2  961: 
1.133     bowersj2  962:     # Display the correct map icon to open or shut map
                    963:     if ($resource->is_map()) {
                    964:         my $mapId = $resource->map_pc();
                    965:         my $nowOpen = !defined($filter->{$mapId});
                    966:         if ($it->{CONDITION}) {
                    967:             $nowOpen = !$nowOpen;
                    968:         }
1.426     droeschl  969: 	
1.205     bowersj2  970: 	my $folderType = $resource->is_sequence() ? 'folder' : 'page';
1.362     www       971:         my $title=$resource->title;
1.426     droeschl  972: 		$title=~s/\"/\&qout;/g;
1.144     bowersj2  973:         if (!$params->{'resource_no_folder_link'}) {
1.205     bowersj2  974:             $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif';
1.426     droeschl  975:             $icon = "<img src='$location/arrow." . ($nowOpen ? 'closed' : 'open') . ".gif' alt='' />"
                    976:                     ."<img class=\"LC_contentImage\" src='$location/$icon' alt=\""
                    977:                     .($nowOpen ? &mt('Open Folder') : &mt('Close Folder')).' '.$title."\" />";			
1.349     foxr      978:             $linkopen = "<a href=\"" . $params->{'url'} . '?' . 
1.392     albertel  979:                 $params->{'queryString'} . '&amp;filter=';
1.144     bowersj2  980:             $linkopen .= ($nowOpen xor $it->{CONDITION}) ?
                    981:                 addToFilter($filter, $mapId) :
                    982:                 removeFromFilter($filter, $mapId);
1.392     albertel  983:             $linkopen .= "&amp;condition=" . $it->{CONDITION} . '&amp;hereType='
                    984:                 . $params->{'hereType'} . '&amp;here=' .
1.384     www       985:                 &escape($params->{'here'}) . 
1.392     albertel  986:                 '&amp;jump=' .
1.384     www       987:                 &escape($resource->symb()) . 
1.392     albertel  988:                 "&amp;folderManip=1\">";
1.306     foxr      989: 
1.144     bowersj2  990:         } else {
                    991:             # Don't allow users to manipulate folder
1.426     droeschl  992:             $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif';
1.427     droeschl  993:             $icon = "<img class=\"LC_space\" src='$whitespace' alt='' />"."<img class=\"LC_contentImage\" src='$location/$icon' alt=\"".($nowOpen ? &mt('Open Folder') : &mt('Close Folder')).' '.$title."\" />";
1.144     bowersj2  994: 
                    995:             $linkopen = "";
                    996:             $linkclose = "";
                    997:         }
1.477     raeburn   998:         if ((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) &&
                    999:             ($resource->symb=~/\_\_\_[^\_]+\_\_\_uploaded/)) {
1.493     raeburn  1000:             if (!$params->{'map_no_edit_link'}) {
                   1001:                 my $icon = &Apache::loncommon::lonhttpdurl('/res/adm/pages').'/editmap.png';
                   1002:                 $editmapLink='&nbsp;'.
1.495     raeburn  1003:                          '<a href="/adm/coursedocs?command=directnav&amp;symb='.&escape($resource->symb()).'">'.
1.477     raeburn  1004:                          '<img src="'.$icon.'" alt="'.&mt('Edit Content').'" title="'.&mt('Edit Content').'" />'.
                   1005:                          '</a>';
1.493     raeburn  1006:             }
1.477     raeburn  1007:         }
1.518     raeburn  1008:         if ($params->{'mapHidden'} || $resource->randomout()) {
                   1009:             $nonLinkedText .= ' <span class="LC_warning">('.&mt('hidden').')</span> ';
                   1010:         }
                   1011:     } else {
                   1012:         if ($resource->randomout()) {
                   1013:             $nonLinkedText .= ' <span class="LC_warning">('.&mt('hidden').')</span> ';
                   1014:         }
1.133     bowersj2 1015:     }
1.342     albertel 1016:     if (!$resource->condval()) {
1.458     bisitz   1017:         $nonLinkedText .= ' <span class="LC_info">('.&mt('conditionally hidden').')</span> ';
1.342     albertel 1018:     }
1.413     www      1019:     if (($resource->is_practice()) && ($resource->is_raw_problem())) {
1.488     bisitz   1020:         $nonLinkedText .=' <span class="LC_info"><b>'.&mt('not graded').'</b></span>';
1.412     www      1021:     }
1.426     droeschl 1022: 
1.133     bowersj2 1023:     # We're done preparing and finally ready to start the rendering
1.426     droeschl 1024:     my $result = '<td class="LC_middle">';
                   1025:     my $newfolderType = $resource->is_sequence() ? 'folder' : 'page';
                   1026: 	
1.140     bowersj2 1027:     my $indentLevel = $params->{'indentLevel'};
                   1028:     if ($newBranchText) { $indentLevel--; }
                   1029: 
1.133     bowersj2 1030:     # print indentation
1.140     bowersj2 1031:     for (my $i = 0; $i < $indentLevel; $i++) {
1.133     bowersj2 1032:         $result .= $params->{'indentString'};
1.84      bowersj2 1033:     }
                   1034: 
1.133     bowersj2 1035:     # Decide what to display
                   1036:     $result .= "$newBranchText$linkopen$icon$linkclose";
                   1037:     
                   1038:     my $curMarkerBegin = '';
                   1039:     my $curMarkerEnd = '';
1.51      bowersj2 1040: 
1.133     bowersj2 1041:     # Is this the current resource?
1.158     bowersj2 1042:     if (!$params->{'displayedHereMarker'} && 
                   1043:         $resource->symb() eq $params->{'here'} ) {
1.477     raeburn  1044:         unless ($resource->is_map()) {
                   1045:             $curMarkerBegin = '<span class="LC_current_nav_location">';
                   1046:             $curMarkerEnd = '</span>';
                   1047:         }
1.424     schulted 1048: 	$params->{'displayedHereMarker'} = 1;
1.51      bowersj2 1049:     }
                   1050: 
1.192     bowersj2 1051:     if ($resource->is_problem() && $part ne '0' && 
1.133     bowersj2 1052:         !$params->{'condensed'}) {
1.274     matthew  1053: 	my $displaypart=$resource->part_display($part);
1.363     albertel 1054:         $partLabel = " (".&mt('Part: [_1]', $displaypart).")";
1.384     www      1055: 	if ($link!~/\#/) { $link.='#'.&escape($part); }
1.133     bowersj2 1056:         $title = "";
1.51      bowersj2 1057:     }
                   1058: 
1.163     bowersj2 1059:     if ($params->{'condensed'} && $resource->countParts() > 1) {
1.363     albertel 1060:         $nonLinkedText .= ' ('.&mt('[_1] parts', $resource->countParts()).')';
1.51      bowersj2 1061:     }
                   1062: 
1.266     raeburn  1063:     if (!$params->{'resource_nolink'} && !$resource->is_sequence() && !$resource->is_empty_sequence) {
1.490     raeburn  1064:         $result .= "$curMarkerBegin<a href=\"$link\">$title$partLabel</a>$curMarkerEnd$editmapLink$nonLinkedText</td>";
1.144     bowersj2 1065:     } else {
1.477     raeburn  1066:         $result .= "$curMarkerBegin$linkopen$title$partLabel</a>$curMarkerEnd$editmapLink$nonLinkedText</td>";
1.144     bowersj2 1067:     }
1.51      bowersj2 1068: 
1.133     bowersj2 1069:     return $result;
                   1070: }
1.51      bowersj2 1071: 
1.133     bowersj2 1072: sub render_communication_status {
                   1073:     my ($resource, $part, $params) = @_;
1.134     bowersj2 1074:     my $discussionHTML = ""; my $feedbackHTML = ""; my $errorHTML = "";
                   1075: 
                   1076:     my $link = $params->{"resourceLink"};
1.445     droeschl 1077:     my $linkopen = "<a href=\"$link\">";
1.134     bowersj2 1078:     my $linkclose = "</a>";
1.298     albertel 1079:     my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc");
1.477     raeburn  1080: 
1.134     bowersj2 1081:     if ($resource->hasDiscussion()) {
                   1082:         $discussionHTML = $linkopen .
1.442     wenzelju 1083:             '<img alt="'.&mt('New Discussion').'" src="'.$location.'/chat.gif" title="'.&mt('New Discussion').'"/>' .
1.134     bowersj2 1084:             $linkclose;
                   1085:     }
                   1086:     
                   1087:     if ($resource->getFeedback()) {
                   1088:         my $feedback = $resource->getFeedback();
1.394     raeburn  1089:         foreach my $msgid (split(/\,/, $feedback)) {
                   1090:             if ($msgid) {
1.445     droeschl 1091:                 $feedbackHTML .= '&nbsp;<a href="/adm/email?display='
1.394     raeburn  1092:                     . &escape($msgid) . '">'
1.442     wenzelju 1093:                     . '<img alt="'.&mt('New E-mail').'" src="'.$location.'/feedback.gif" title="'.&mt('New E-mail').'"/></a>';
1.134     bowersj2 1094:             }
                   1095:         }
                   1096:     }
                   1097:     
                   1098:     if ($resource->getErrors()) {
                   1099:         my $errors = $resource->getErrors();
1.254     matthew  1100:         my $errorcount = 0;
1.394     raeburn  1101:         foreach my $msgid (split(/,/, $errors)) {
1.254     matthew  1102:             last if ($errorcount>=10); # Only output 10 bombs maximum
1.394     raeburn  1103:             if ($msgid) {
1.254     matthew  1104:                 $errorcount++;
1.445     droeschl 1105:                 $errorHTML .= '&nbsp;<a href="/adm/email?display='
1.394     raeburn  1106:                     . &escape($msgid) . '">'
1.442     wenzelju 1107:                     . '<img alt="'.&mt('New Error').'" src="'.$location.'/bomb.gif" title="'.&mt('New Error').'"/></a>';
1.134     bowersj2 1108:             }
                   1109:         }
1.197     bowersj2 1110:     }
                   1111: 
                   1112:     if ($params->{'multipart'} && $part != '0') {
                   1113: 	$discussionHTML = $feedbackHTML = $errorHTML = '';
1.134     bowersj2 1114:     }
1.426     droeschl 1115:     return "<td class=\"LC_middle\">$discussionHTML$feedbackHTML$errorHTML&nbsp;</td>";
1.134     bowersj2 1116: 
1.133     bowersj2 1117: }
                   1118: sub render_quick_status {
                   1119:     my ($resource, $part, $params) = @_;
1.135     bowersj2 1120:     my $result = "";
                   1121:     my $firstDisplayed = !$params->{'condensed'} && 
                   1122:         $params->{'multipart'} && $part eq "0";
                   1123: 
                   1124:     my $link = $params->{"resourceLink"};
1.445     droeschl 1125:     my $linkopen = "<a href=\"$link\">";
1.135     bowersj2 1126:     my $linkclose = "</a>";
1.426     droeschl 1127: 	
                   1128: 	$result .= '<td class="LC_middle">';
1.135     bowersj2 1129:     if ($resource->is_problem() &&
                   1130:         !$firstDisplayed) {
1.224     bowersj2 1131:         my $icon = $statusIconMap{$resource->simpleStatus($part)};
1.135     bowersj2 1132:         my $alt = $iconAltTags{$icon};
                   1133:         if ($icon) {
1.298     albertel 1134: 	    my $location=
                   1135: 		&Apache::loncommon::lonhttpdurl("/adm/lonIcons/$icon");
1.438     wenzelju 1136: 		$result .= $linkopen.'<img src="'.$location.'" alt="'.&mt($alt).'" title="'.&mt($alt).'" />'.$linkclose;            
1.135     bowersj2 1137:         } else {
1.426     droeschl 1138:             $result .= "&nbsp;";
1.135     bowersj2 1139:         }
                   1140:     } else { # not problem, no icon
1.426     droeschl 1141:         $result .= "&nbsp;";
1.135     bowersj2 1142:     }
1.426     droeschl 1143: 	$result .= "</td>\n";
1.135     bowersj2 1144:     return $result;
1.133     bowersj2 1145: }
                   1146: sub render_long_status {
                   1147:     my ($resource, $part, $params) = @_;
1.426     droeschl 1148:     my $result = '<td class="LC_middle LC_right">';
1.136     bowersj2 1149:     my $firstDisplayed = !$params->{'condensed'} && 
                   1150:         $params->{'multipart'} && $part eq "0";
                   1151:                 
                   1152:     my $color;
1.488     bisitz   1153:     my $info = '';
1.402     albertel 1154:     if ($resource->is_problem() || $resource->is_practice()) {
1.136     bowersj2 1155:         $color = $colormap{$resource->status};
1.502     raeburn  1156: 
                   1157:         if (dueInLessThan24Hours($resource, $part)) {
1.136     bowersj2 1158:             $color = $hurryUpColor;
1.488     bisitz   1159:             $info = ' title="'.&mt('Due in less than 24 hours!').'"';
1.502     raeburn  1160:         } elsif (lastTry($resource, $part)) {
                   1161:             unless (($resource->problemstatus($part) eq 'no') ||
                   1162:                     ($resource->problemstatus($part) eq 'no_feedback_ever')) {
                   1163:                 $color = $hurryUpColor;
                   1164:                 $info = ' title="'.&mt('One try remaining!').'"';
                   1165:             }
                   1166:          }
1.136     bowersj2 1167:     }
                   1168:     
1.463     raeburn  1169:     if ($resource->kind() eq "res" &&
                   1170:         $resource->is_raw_problem() &&
                   1171:         !$firstDisplayed) {
1.488     bisitz   1172:         if ($color) {$result .= '<span style="color:'.$color.'"'.$info.'><b>'; }
1.136     bowersj2 1173:         $result .= getDescription($resource, $part);
1.488     bisitz   1174:         if ($color) {$result .= "</b></span>"; }
1.136     bowersj2 1175:     }
1.402     albertel 1176:     if ($resource->is_map() && &advancedUser() && $resource->randompick()) {
1.400     albertel 1177:         $result .= &mt('(randomly select [_1])', $resource->randompick());
                   1178:     }
                   1179:     if ($resource->is_map() && &advancedUser() && $resource->randomorder()) {
                   1180:         $result .= &mt('(randomly ordered)');
1.136     bowersj2 1181:     }
1.210     bowersj2 1182: 
1.213     bowersj2 1183:     # Debugging code
                   1184:     #$result .= " " . $resource->awarded($part) . '/' . $resource->weight($part) .
                   1185:     #	' - Part: ' . $part;
                   1186: 
1.210     bowersj2 1187:     $result .= "</td>\n";
1.136     bowersj2 1188:     
                   1189:     return $result;
1.51      bowersj2 1190: }
                   1191: 
1.227     bowersj2 1192: # Colors obtained by taking the icons, matching the colors, and
                   1193: # possibly reducing the Value (HSV) of the color, if it's too bright
                   1194: # for text, generally by one third or so.
1.225     bowersj2 1195: my %statusColors = 
                   1196:     (
                   1197:      $resObj->CLOSED => '#000000',
1.227     bowersj2 1198:      $resObj->OPEN   => '#998b13',
                   1199:      $resObj->CORRECT => '#26933f',
                   1200:      $resObj->INCORRECT => '#c48207',
                   1201:      $resObj->ATTEMPTED => '#a87510',
1.225     bowersj2 1202:      $resObj->ERROR => '#000000'
                   1203:      );
                   1204: my %statusStrings = 
                   1205:     (
                   1206:      $resObj->CLOSED => 'Not yet open',
                   1207:      $resObj->OPEN   => 'Open',
                   1208:      $resObj->CORRECT => 'Correct',
                   1209:      $resObj->INCORRECT => 'Incorrect',
                   1210:      $resObj->ATTEMPTED => 'Attempted',
                   1211:      $resObj->ERROR => 'Network Error'
                   1212:      );
                   1213: my @statuses = ($resObj->CORRECT, $resObj->ATTEMPTED, $resObj->INCORRECT, $resObj->OPEN, $resObj->CLOSED, $resObj->ERROR);
                   1214: 
                   1215: sub render_parts_summary_status {
                   1216:     my ($resource, $part, $params) = @_;
1.256     albertel 1217:     if (!$resource->is_problem() && !$resource->contains_problem) { return '<td></td>'; }
1.225     bowersj2 1218:     if ($params->{showParts}) { 
                   1219: 	return '<td></td>';
                   1220:     }
                   1221: 
                   1222:     my $td = "<td align='right'>\n";
                   1223:     my $endtd = "</td>\n";
1.256     albertel 1224:     my @probs;
1.225     bowersj2 1225: 
1.256     albertel 1226:     if ($resource->contains_problem) {
                   1227: 	@probs=$resource->retrieveResources($resource,sub { $_[0]->is_problem() },1,0);
                   1228:     } else {
                   1229: 	@probs=($resource);
                   1230:     }
                   1231:     my $return;
                   1232:     my %overallstatus;
                   1233:     my $totalParts;
                   1234:     foreach my $resource (@probs) {
                   1235: 	# If there is a single part, just show the simple status
                   1236: 	if ($resource->singlepart()) {
                   1237: 	    my $status = $resource->simpleStatus(${$resource->parts}[0]);
                   1238: 	    $overallstatus{$status}++;
                   1239: 	    $totalParts++;
                   1240: 	    next;
                   1241: 	}
                   1242: 	# Now we can be sure the $part doesn't really matter.
                   1243: 	my $statusCount = $resource->simpleStatusCount();
                   1244: 	my @counts;
                   1245: 	foreach my $status (@statuses) {
                   1246: 	    # decouple display order from the simpleStatusCount order
                   1247: 	    my $slot = Apache::lonnavmaps::resource::statusToSlot($status);
                   1248: 	    if ($statusCount->[$slot]) {
                   1249: 		$overallstatus{$status}+=$statusCount->[$slot];
                   1250: 		$totalParts+=$statusCount->[$slot];
                   1251: 	    }
                   1252: 	}
                   1253:     }
                   1254:     $return.= $td . $totalParts . ' parts: ';
                   1255:     foreach my $status (@statuses) {
1.488     bisitz   1256:         if ($overallstatus{$status}) {
                   1257:             $return.='<span style="color:' . $statusColors{$status}
                   1258:                    . '">' . $overallstatus{$status} . ' '
                   1259:                    . $statusStrings{$status} . '</span>';
                   1260:         }
1.225     bowersj2 1261:     }
1.256     albertel 1262:     $return.= $endtd;
                   1263:     return $return;
1.225     bowersj2 1264: }
                   1265: 
1.133     bowersj2 1266: my @preparedColumns = (\&render_resource, \&render_communication_status,
1.225     bowersj2 1267:                        \&render_quick_status, \&render_long_status,
                   1268: 		       \&render_parts_summary_status);
1.132     bowersj2 1269: 
                   1270: sub setDefault {
                   1271:     my ($val, $default) = @_;
                   1272:     if (!defined($val)) { return $default; }
                   1273:     return $val;
                   1274: }
                   1275: 
1.296     albertel 1276: sub cmp_title {
                   1277:     my ($atitle,$btitle) = (lc($_[0]->compTitle),lc($_[1]->compTitle));
                   1278:     $atitle=~s/^\s*//;
                   1279:     $btitle=~s/^\s*//;
                   1280:     return $atitle cmp $btitle;
                   1281: }
                   1282: 
1.132     bowersj2 1283: sub render {
                   1284:     my $args = shift;
1.140     bowersj2 1285:     &Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING});
                   1286:     my $result = '';
1.132     bowersj2 1287:     # Configure the renderer.
                   1288:     my $cols = $args->{'cols'};
                   1289:     if (!defined($cols)) {
                   1290:         # no columns, no nav maps.
                   1291:         return '';
                   1292:     }
1.140     bowersj2 1293:     my $navmap;
                   1294:     if (defined($args->{'navmap'})) {
                   1295:         $navmap = $args->{'navmap'};
                   1296:     }
                   1297: 
1.158     bowersj2 1298:     my $r = $args->{'r'};
1.140     bowersj2 1299:     my $queryString = $args->{'queryString'};
1.159     bowersj2 1300:     my $jump = $args->{'jump'};
1.158     bowersj2 1301:     my $here = $args->{'here'};
1.153     bowersj2 1302:     my $suppressNavmap = setDefault($args->{'suppressNavmap'}, 0);
1.256     albertel 1303:     my $closeAllPages = setDefault($args->{'closeAllPages'}, 0);
1.140     bowersj2 1304:     my $currentJumpDelta = 2; # change this to change how many resources are displayed
                   1305:                              # before the current resource when using #current
                   1306: 
                   1307:     # If we were passed 'here' information, we are not rendering
                   1308:     # after a folder manipulation, and we were not passed an
                   1309:     # iterator, make sure we open the folders to show the "here"
                   1310:     # marker
                   1311:     my $filterHash = {};
                   1312:     # Figure out what we're not displaying
1.394     raeburn  1313:     foreach my $item (split(/\,/, $env{"form.filter"})) {
                   1314:         if ($item) {
                   1315:             $filterHash->{$item} = "1";
1.140     bowersj2 1316:         }
                   1317:     }
                   1318: 
1.191     bowersj2 1319:     # Filter: Remember filter function and add our own filter: Refuse
                   1320:     # to show hidden resources unless the user can see them.
                   1321:     my $userCanSeeHidden = advancedUser();
                   1322:     my $filterFunc = setDefault($args->{'filterFunc'},
                   1323:                                 sub {return 1;});
                   1324:     if (!$userCanSeeHidden) {
                   1325:         # Without renaming the filterfunc, the server seems to go into
                   1326:         # an infinite loop
                   1327:         my $oldFilterFunc = $filterFunc;
                   1328:         $filterFunc = sub { my $res = shift; return !$res->randomout() && 
                   1329:                                 &$oldFilterFunc($res);};
                   1330:     }
                   1331: 
1.140     bowersj2 1332:     my $condition = 0;
1.320     albertel 1333:     if ($env{'form.condition'}) {
1.140     bowersj2 1334:         $condition = 1;
                   1335:     }
                   1336: 
1.320     albertel 1337:     if (!$env{'form.folderManip'} && !defined($args->{'iterator'})) {
1.140     bowersj2 1338:         # Step 1: Check to see if we have a navmap
                   1339:         if (!defined($navmap)) {
1.221     bowersj2 1340:             $navmap = Apache::lonnavmaps::navmap->new();
1.340     albertel 1341: 	    if (!defined($navmap)) {
1.423     raeburn  1342: 		# no longer in course
1.388     albertel 1343: 		return '<span class="LC_error">'.&mt('No course selected').'</span><br />
1.362     www      1344:                         <a href="/adm/roles">'.&mt('Select a course').'</a><br />';
1.340     albertel 1345: 	    }
                   1346: 	}
1.140     bowersj2 1347: 
                   1348:         # Step two: Locate what kind of here marker is necessary
                   1349:         # Determine where the "here" marker is and where the screen jumps to.
                   1350: 
1.332     albertel 1351:         if ($env{'form.postsymb'} ne '') {
1.320     albertel 1352:             $here = $jump = &Apache::lonnet::symbclean($env{'form.postsymb'});
1.332     albertel 1353:         } elsif ($env{'form.postdata'} ne '') {
1.140     bowersj2 1354:             # couldn't find a symb, is there a URL?
1.320     albertel 1355:             my $currenturl = $env{'form.postdata'};
1.158     bowersj2 1356:             #$currenturl=~s/^http\:\/\///;
                   1357:             #$currenturl=~s/^[^\/]+//;
1.140     bowersj2 1358:             
1.158     bowersj2 1359:             $here = $jump = &Apache::lonnet::symbread($currenturl);
1.331     albertel 1360: 	}
                   1361: 	if ($here eq '') {
1.317     albertel 1362: 	    my $last;
1.320     albertel 1363: 	    if (tie(my %hash,'GDBM_File',$env{'request.course.fn'}.'_symb.db',
1.317     albertel 1364:                     &GDBM_READER(),0640)) {
                   1365: 		$last=$hash{'last_known'};
                   1366: 		untie(%hash);
                   1367: 	    }
                   1368: 	    if ($last) { $here = $jump = $last; }
                   1369: 	}
1.158     bowersj2 1370: 
1.140     bowersj2 1371:         # Step three: Ensure the folders are open
                   1372:         my $mapIterator = $navmap->getIterator(undef, undef, undef, 1);
1.222     bowersj2 1373:         my $curRes;
1.140     bowersj2 1374:         my $found = 0;
                   1375:         
                   1376:         # We only need to do this if we need to open the maps to show the
                   1377:         # current position. This will change the counter so we can't count
                   1378:         # for the jump marker with this loop.
1.294     albertel 1379:         while ($here && ($curRes = $mapIterator->next()) && !$found) {
1.158     bowersj2 1380:             if (ref($curRes) && $curRes->symb() eq $here) {
1.140     bowersj2 1381:                 my $mapStack = $mapIterator->getStack();
                   1382:                 
                   1383:                 # Ensure the parent maps are open
                   1384:                 for my $map (@{$mapStack}) {
                   1385:                     if ($condition) {
                   1386:                         undef $filterHash->{$map->map_pc()};
                   1387:                     } else {
                   1388:                         $filterHash->{$map->map_pc()} = 1;
                   1389:                     }
                   1390:                 }
                   1391:                 $found = 1;
                   1392:             }
                   1393:         }            
1.159     bowersj2 1394:     }        
1.140     bowersj2 1395: 
1.320     albertel 1396:     if ( !defined($args->{'iterator'}) && $env{'form.folderManip'} ) { # we came from a user's manipulation of the nav page
1.140     bowersj2 1397:         # If this is a click on a folder or something, we want to preserve the "here"
                   1398:         # from the querystring, and get the new "jump" marker
1.320     albertel 1399:         $here = $env{'form.here'};
                   1400:         $jump = $env{'form.jump'};
1.140     bowersj2 1401:     } 
                   1402:     
1.132     bowersj2 1403:     my $it = $args->{'iterator'};
                   1404:     if (!defined($it)) {
1.320     albertel 1405:         # Construct a default iterator based on $env{'form.'} information
1.140     bowersj2 1406:         
                   1407:         # Step 1: Check to see if we have a navmap
                   1408:         if (!defined($navmap)) {
1.221     bowersj2 1409:             $navmap = Apache::lonnavmaps::navmap->new();
1.423     raeburn  1410:             if (!defined($navmap)) {
                   1411:                 # no longer in course
                   1412:                 return '<span class="LC_error">'.&mt('No course selected').'</span><br />
                   1413:                         <a href="/adm/roles">'.&mt('Select a course').'</a><br />';
                   1414:             }
1.140     bowersj2 1415:         }
                   1416: 
1.144     bowersj2 1417:         # See if we're being passed a specific map
                   1418:         if ($args->{'iterator_map'}) {
                   1419:             my $map = $args->{'iterator_map'};
1.145     bowersj2 1420:             $map = $navmap->getResourceByUrl($map);
1.144     bowersj2 1421:             my $firstResource = $map->map_start();
                   1422:             my $finishResource = $map->map_finish();
                   1423: 
                   1424:             $args->{'iterator'} = $it = $navmap->getIterator($firstResource, $finishResource, $filterHash, $condition);
                   1425:         } else {
1.401     albertel 1426:             $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition,undef,$args->{'include_top_level_map'});
1.144     bowersj2 1427:         }
1.132     bowersj2 1428:     }
1.256     albertel 1429: 
1.159     bowersj2 1430:     # (re-)Locate the jump point, if any
1.191     bowersj2 1431:     # Note this does not take filtering or hidden into account... need
                   1432:     # to be fixed?
1.159     bowersj2 1433:     my $mapIterator = $navmap->getIterator(undef, undef, $filterHash, 0);
1.222     bowersj2 1434:     my $curRes;
1.159     bowersj2 1435:     my $foundJump = 0;
                   1436:     my $counter = 0;
                   1437:     
1.222     bowersj2 1438:     while (($curRes = $mapIterator->next()) && !$foundJump) {
1.159     bowersj2 1439:         if (ref($curRes)) { $counter++; }
                   1440:         
                   1441:         if (ref($curRes) && $jump eq $curRes->symb()) {
                   1442:             
                   1443:             # This is why we have to use the main iterator instead of the
                   1444:             # potentially faster DFS: The count has to be the same, so
                   1445:             # the order has to be the same, which DFS won't give us.
                   1446:             $args->{'currentJumpIndex'} = $counter;
                   1447:             $foundJump = 1;
                   1448:         }
                   1449:     }
                   1450: 
1.132     bowersj2 1451:     my $showParts = setDefault($args->{'showParts'}, 1);
                   1452:     my $condenseParts = setDefault($args->{'condenseParts'}, 1);
1.139     bowersj2 1453:     # keeps track of when the current resource is found,
                   1454:     # so we can back up a few and put the anchor above the
                   1455:     # current resource
1.140     bowersj2 1456:     my $printKey = $args->{'printKey'};
                   1457:     my $printCloseAll = $args->{'printCloseAll'};
                   1458:     if (!defined($printCloseAll)) { $printCloseAll = 1; }
1.425     schulted 1459:    
1.140     bowersj2 1460:     # Print key?
                   1461:     if ($printKey) {
                   1462:         $result .= '<table border="0" cellpadding="2" cellspacing="0">';
                   1463:         $result.='<tr><td align="right" valign="bottom">Key:&nbsp;&nbsp;</td>';
1.298     albertel 1464: 	my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc");
1.140     bowersj2 1465:         if ($navmap->{LAST_CHECK}) {
                   1466:             $result .= 
1.434     bisitz   1467:                 '<img src="'.$location.'/chat.gif" alt="" /> '.&mt('New discussion since').' '.
1.140     bowersj2 1468:                 strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})).
                   1469:                 '</td><td align="center" valign="bottom">&nbsp;&nbsp;'.
1.434     bisitz   1470:                 '<img src="'.$location.'/feedback.gif" alt="" /> '.&mt('New message (click to open)').'<p>'.
1.140     bowersj2 1471:                 '</td>'; 
                   1472:         } else {
                   1473:             $result .= '<td align="center" valign="bottom">&nbsp;&nbsp;'.
1.434     bisitz   1474:                 '<img src="'.$location.'/chat.gif" alt="" /> '.&mt('Discussions').'</td><td align="center" valign="bottom">'.
                   1475:                 '&nbsp;&nbsp;<img src="'.$location.'/feedback.gif" alt="" /> '.&mt('New message (click to open)').
1.140     bowersj2 1476:                 '</td>'; 
                   1477:         }
                   1478: 
                   1479:         $result .= '</tr></table>';
                   1480:     }
                   1481: 
1.172     bowersj2 1482:     if ($printCloseAll && !$args->{'resource_no_folder_link'}) {
1.281     albertel 1483: 	my ($link,$text);
1.140     bowersj2 1484:         if ($condition) {
1.446     raeburn  1485: 	    $link='navmaps?condition=0&amp;filter=&amp;'.$queryString.
1.449     bisitz   1486: 		'&amp;here='.&escape($here);
1.377     www      1487: 	    $text='Close all folders';
1.140     bowersj2 1488:         } else {
1.446     raeburn  1489: 	    $link='navmaps?condition=1&amp;filter=&amp;'.$queryString.
1.449     bisitz   1490: 		'&amp;here='.&escape($here);
1.377     www      1491: 	    $text='Open all folders';
1.281     albertel 1492:         }
1.446     raeburn  1493:         if ($env{'form.register'}) {
                   1494:             $link .= '&amp;register='.$env{'form.register'};
                   1495:         }
1.281     albertel 1496: 	if ($args->{'caller'} eq 'navmapsdisplay') {
1.478     raeburn  1497:             unless ($args->{'notools'}) {
                   1498:                 &add_linkitem($args->{'linkitems'},'changefolder',
                   1499:                               "location.href='$link'",$text);
                   1500:             }
1.281     albertel 1501: 	} else {
1.446     raeburn  1502: 	    $result.= '<a href="'.$link.'">'.&mt($text).'</a>';
1.281     albertel 1503: 	}
1.266     raeburn  1504:         $result .= "\n";
                   1505:     }
1.140     bowersj2 1506: 
1.266     raeburn  1507:     # Check for any unread discussions in all resources.
1.478     raeburn  1508:     if (($args->{'caller'} eq 'navmapsdisplay') && (!$args->{'notools'})) {
1.296     albertel 1509: 	&add_linkitem($args->{'linkitems'},'clearbubbles',
                   1510: 		      'document.clearbubbles.submit()',
                   1511: 		      'Mark all posts read');
1.297     albertel 1512: 	my $time=time;
1.486     raeburn  1513:         my $querystr = &HTML::Entities::encode($ENV{'QUERY_STRING'},'<>&"');
1.296     albertel 1514: 	$result .= (<<END);
                   1515:     <form name="clearbubbles" method="post" action="/adm/feedback">
1.486     raeburn  1516: 	<input type="hidden" name="navurl" value="$querystr" />
1.297     albertel 1517: 	<input type="hidden" name="navtime" value="$time" />
1.296     albertel 1518: END
1.446     raeburn  1519:         if ($env{'form.register'}) {
                   1520:             $result .= '<input type="hidden" name="register" value="'.$env{'form.register'}.'" />';
                   1521:         }
1.296     albertel 1522:         if ($args->{'sort'} eq 'discussion') { 
                   1523: 	    my $totdisc = 0;
                   1524: 	    my $haveDisc = '';
                   1525: 	    my @allres=$navmap->retrieveResources();
                   1526: 	    foreach my $resource (@allres) {
                   1527: 		if ($resource->hasDiscussion()) {
1.323     albertel 1528: 		    $haveDisc .= $resource->wrap_symb().':';
1.296     albertel 1529: 		    $totdisc ++;
1.267     albertel 1530: 		}
                   1531: 	    }
1.296     albertel 1532: 	    if ($totdisc > 0) {
                   1533: 		$haveDisc =~ s/:$//;
                   1534: 		$result .= (<<END);
                   1535: 	<input type="hidden" name="navmaps" value="$haveDisc" />
                   1536:     </form>
                   1537: END
                   1538:             }
1.297     albertel 1539: 	}
                   1540: 	$result.='</form>';
1.478     raeburn  1541:     }
                   1542:     if (($args->{'caller'} eq 'navmapsdisplay') &&
                   1543:         (&Apache::lonnet::allowed('mdc',$env{'request.course.id'}))) {
                   1544:         my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
                   1545:         my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
                   1546:         if ($env{'course.'.$env{'request.course.id'}.'.url'} eq 
                   1547:             "uploaded/$cdom/$cnum/default.sequence") {
                   1548:             &add_linkitem($args->{'linkitems'},'edittoplevel',
                   1549:                           "javascript:gocmd('/adm/coursedocs','editdocs');",
                   1550:                           'Content Editor');
1.475     raeburn  1551:         }
1.266     raeburn  1552:     }
1.276     albertel 1553: 
1.281     albertel 1554:     if ($args->{'caller'} eq 'navmapsdisplay') {
1.476     raeburn  1555:         $result .= &show_linkitems_toolbar($args,$condition);
1.281     albertel 1556:     } elsif ($args->{'sort_html'}) { 
                   1557:         $result.=$args->{'sort_html'}; 
                   1558:     }
1.276     albertel 1559: 
1.425     schulted 1560:     #$result .= "<br />\n";
1.140     bowersj2 1561:     if ($r) {
                   1562:         $r->print($result);
                   1563:         $r->rflush();
                   1564:         $result = "";
                   1565:     }
1.132     bowersj2 1566:     # End parameter setting
1.425     schulted 1567:     
                   1568:     $result .= "<br />\n";
                   1569: 
1.132     bowersj2 1570:     # Data
1.431     schulted 1571:     $result.=&Apache::loncommon::start_data_table("LC_tableOfContent");    
1.426     droeschl 1572: 
1.132     bowersj2 1573:     my $res = "Apache::lonnavmaps::resource";
                   1574:     my %condenseStatuses =
                   1575:         ( $res->NETWORK_FAILURE    => 1,
                   1576:           $res->NOTHING_SET        => 1,
                   1577:           $res->CORRECT            => 1 );
1.133     bowersj2 1578: 
                   1579:     # Shared variables
                   1580:     $args->{'counter'} = 0; # counts the rows
                   1581:     $args->{'indentLevel'} = 0;
                   1582:     $args->{'isNewBranch'} = 0;
1.426     droeschl 1583:     $args->{'condensed'} = 0;   
                   1584: 
1.428     droeschl 1585:     my $location = &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace_21.gif");
1.426     droeschl 1586:     $args->{'indentString'} = setDefault($args->{'indentString'}, "<img src='$location' alt='' />");
1.133     bowersj2 1587:     $args->{'displayedHereMarker'} = 0;
1.132     bowersj2 1588: 
1.518     raeburn  1589:     # If we're suppressing empty sequences, look for them here.
                   1590:     # We also do this even if $args->{'suppressEmptySequences'}
                   1591:     # is not true, so we can hide empty sequences for which the
                   1592:     # hiddenresource parameter is set to yes (at map level), or
                   1593:     # mark as hidden for users who have $userCanSeeHidden.  
                   1594:     # Use DFS for speed, since structure actually doesn't matter,
                   1595:     # except what map has what resources.
                   1596: 
                   1597:     my $dfsit = Apache::lonnavmaps::DFSiterator->new($navmap,
                   1598:                                                      $it->{FIRST_RESOURCE},
                   1599:                                                      $it->{FINISH_RESOURCE},
                   1600:                                                      {}, undef, 1);
                   1601:     my $depth = 0;
                   1602:     $dfsit->next();
                   1603:     my $curRes = $dfsit->next();
                   1604:     while ($depth > -1) {
                   1605:         if ($curRes == $dfsit->BEGIN_MAP()) { $depth++; }
                   1606:         if ($curRes == $dfsit->END_MAP()) { $depth--; }
                   1607: 
                   1608:         if (ref($curRes)) { 
                   1609:             # Parallel pre-processing: Do sequences have non-filtered-out children?
                   1610:             if ($curRes->is_map()) {
                   1611:                 $curRes->{DATA}->{HAS_VISIBLE_CHILDREN} = 0;
                   1612:                 # Sequences themselves do not count as visible children,
                   1613:                 # unless those sequences also have visible children.
                   1614:                 # This means if a sequence appears, there's a "promise"
                   1615:                 # that there's something under it if you open it, somewhere.
                   1616:             } elsif ($curRes->src()) {
                   1617:                 # Not a sequence: if it's filtered, ignore it, otherwise
                   1618:                 # rise up the stack and mark the sequences as having children
                   1619:                 if (&$filterFunc($curRes)) {
                   1620:                     for my $sequence (@{$dfsit->getStack()}) {
                   1621:                         $sequence->{DATA}->{HAS_VISIBLE_CHILDREN} = 1;
1.190     bowersj2 1622:                     }
                   1623:                 }
                   1624:             }
                   1625:         }
1.518     raeburn  1626:     } continue {
                   1627:         $curRes = $dfsit->next();
1.190     bowersj2 1628:     }
                   1629: 
1.133     bowersj2 1630:     my $displayedJumpMarker = 0;
1.132     bowersj2 1631:     # Set up iteration.
                   1632:     my $now = time();
                   1633:     my $in24Hours = $now + 24 * 60 * 60;
                   1634:     my $rownum = 0;
                   1635: 
1.141     bowersj2 1636:     # export "here" marker information
                   1637:     $args->{'here'} = $here;
                   1638: 
1.222     bowersj2 1639:     $args->{'indentLevel'} = -1; # first BEGIN_MAP takes this to 0
1.270     albertel 1640:     my @resources;
                   1641:     my $code='';# sub { !(shift->is_map();) };
                   1642:     if ($args->{'sort'} eq 'title') {
1.276     albertel 1643:         my $oldFilterFunc = $filterFunc;
                   1644: 	my $filterFunc= 
                   1645: 	    sub {
                   1646: 		my ($res)=@_;
                   1647: 		if ($res->is_map()) { return 0;}
                   1648: 		return &$oldFilterFunc($res);
                   1649: 	    };
                   1650: 	@resources=$navmap->retrieveResources(undef,$filterFunc);
1.296     albertel 1651: 	@resources= sort { &cmp_title($a,$b) } @resources;
                   1652:     } elsif ($args->{'sort'} eq 'duedate') {
                   1653: 	my $oldFilterFunc = $filterFunc;
                   1654: 	my $filterFunc= 
                   1655: 	    sub {
                   1656: 		my ($res)=@_;
                   1657: 		if (!$res->is_problem()) { return 0;}
                   1658: 		return &$oldFilterFunc($res);
                   1659: 	    };
                   1660: 	@resources=$navmap->retrieveResources(undef,$filterFunc);
1.289     raeburn  1661: 	@resources= sort {
1.270     albertel 1662: 	    if ($a->duedate ne $b->duedate) {
                   1663: 	        return $a->duedate cmp $b->duedate;
                   1664: 	    }
1.296     albertel 1665: 	    my $value=&cmp_title($a,$b);
                   1666: 	    return $value;
1.270     albertel 1667: 	} @resources;
1.296     albertel 1668:     } elsif ($args->{'sort'} eq 'discussion') {
                   1669: 	my $oldFilterFunc = $filterFunc;
                   1670: 	my $filterFunc= 
                   1671: 	    sub {
                   1672: 		my ($res)=@_;
                   1673: 		if (!$res->hasDiscussion() &&
                   1674: 		    !$res->getFeedback() &&
                   1675: 		    !$res->getErrors()) { return 0;}
                   1676: 		return &$oldFilterFunc($res);
                   1677: 	    };
                   1678: 	@resources=$navmap->retrieveResources(undef,$filterFunc);
                   1679: 	@resources= sort { &cmp_title($a,$b) } @resources;
1.276     albertel 1680:     } else {
                   1681: 	#unknow sort mechanism or default
                   1682: 	undef($args->{'sort'});
1.270     albertel 1683:     }
                   1684: 
1.276     albertel 1685: 
1.270     albertel 1686:     while (1) {
                   1687: 	if ($args->{'sort'}) {
                   1688: 	    $curRes = shift(@resources);
                   1689: 	} else {
                   1690: 	    $curRes = $it->next($closeAllPages);
                   1691: 	}
                   1692: 	if (!$curRes) { last; }
                   1693: 
1.132     bowersj2 1694:         # Maintain indentation level.
                   1695:         if ($curRes == $it->BEGIN_MAP() ||
                   1696:             $curRes == $it->BEGIN_BRANCH() ) {
1.133     bowersj2 1697:             $args->{'indentLevel'}++;
1.132     bowersj2 1698:         }
                   1699:         if ($curRes == $it->END_MAP() ||
                   1700:             $curRes == $it->END_BRANCH() ) {
1.133     bowersj2 1701:             $args->{'indentLevel'}--;
1.132     bowersj2 1702:         }
                   1703:         # Notice new branches
                   1704:         if ($curRes == $it->BEGIN_BRANCH()) {
1.133     bowersj2 1705:             $args->{'isNewBranch'} = 1;
                   1706:         }
                   1707: 
                   1708:         # If this isn't an actual resource, continue on
                   1709:         if (!ref($curRes)) {
                   1710:             next;
1.132     bowersj2 1711:         }
1.133     bowersj2 1712: 
1.143     bowersj2 1713:         # If this has been filtered out, continue on
                   1714:         if (!(&$filterFunc($curRes))) {
                   1715:             $args->{'isNewBranch'} = 0; # Don't falsely remember this
                   1716:             next;
                   1717:         } 
1.132     bowersj2 1718: 
1.190     bowersj2 1719:         # If this is an empty sequence and we're filtering them, continue on
1.518     raeburn  1720:         $args->{'mapHidden'} = 0;
                   1721:         if (($curRes->is_map()) && (!$curRes->{DATA}->{HAS_VISIBLE_CHILDREN})) {
                   1722:             if ($args->{'suppressEmptySequences'}) {
                   1723:                 next;
                   1724:             } else {
                   1725:                 my $mapname = &Apache::lonnet::declutter($curRes->src());
                   1726:                 $mapname = &Apache::lonnet::deversion($mapname); 
                   1727:                 if (lc($navmap->get_mapparam(undef,$mapname,"0.hiddenresource")) eq 'yes') {
                   1728:                     if ($userCanSeeHidden) {
                   1729:                         $args->{'mapHidden'} = 1;
                   1730:                     } else {
                   1731:                         next;
                   1732:                     }
                   1733:                 }
                   1734:             }
1.190     bowersj2 1735:         }
                   1736: 
1.153     bowersj2 1737:         # If we're suppressing navmaps and this is a navmap, continue on
                   1738:         if ($suppressNavmap && $curRes->src() =~ /^\/adm\/navmaps/) {
                   1739:             next;
                   1740:         }
                   1741: 
1.191     bowersj2 1742:         $args->{'counter'}++;
                   1743: 
1.132     bowersj2 1744:         # Does it have multiple parts?
1.133     bowersj2 1745:         $args->{'multipart'} = 0;
                   1746:         $args->{'condensed'} = 0;
1.132     bowersj2 1747:         my @parts;
                   1748:             
                   1749:         # Decide what parts to show.
1.133     bowersj2 1750:         if ($curRes->is_problem() && $showParts) {
1.132     bowersj2 1751:             @parts = @{$curRes->parts()};
1.192     bowersj2 1752:             $args->{'multipart'} = $curRes->multipart();
1.132     bowersj2 1753:             
                   1754:             if ($condenseParts) { # do the condensation
1.133     bowersj2 1755:                 if (!$args->{'condensed'}) {
1.132     bowersj2 1756:                     # Decide whether to condense based on similarity
1.192     bowersj2 1757:                     my $status = $curRes->status($parts[0]);
                   1758:                     my $due = $curRes->duedate($parts[0]);
                   1759:                     my $open = $curRes->opendate($parts[0]);
1.132     bowersj2 1760:                     my $statusAllSame = 1;
                   1761:                     my $dueAllSame = 1;
                   1762:                     my $openAllSame = 1;
1.192     bowersj2 1763:                     for (my $i = 1; $i < scalar(@parts); $i++) {
1.132     bowersj2 1764:                         if ($curRes->status($parts[$i]) != $status){
                   1765:                             $statusAllSame = 0;
                   1766:                         }
                   1767:                         if ($curRes->duedate($parts[$i]) != $due ) {
                   1768:                             $dueAllSame = 0;
                   1769:                         }
                   1770:                         if ($curRes->opendate($parts[$i]) != $open) {
                   1771:                             $openAllSame = 0;
                   1772:                         }
                   1773:                     }
                   1774:                     # $*allSame is true if all the statuses were
                   1775:                     # the same. Now, if they are all the same and
                   1776:                     # match one of the statuses to condense, or they
                   1777:                     # are all open with the same due date, or they are
                   1778:                     # all OPEN_LATER with the same open date, display the
                   1779:                     # status of the first non-zero part (to get the 'correct'
                   1780:                     # status right, since 0 is never 'correct' or 'open').
                   1781:                     if (($statusAllSame && defined($condenseStatuses{$status})) ||
                   1782:                         ($dueAllSame && $status == $curRes->OPEN && $statusAllSame)||
                   1783:                         ($openAllSame && $status == $curRes->OPEN_LATER && $statusAllSame) ){
1.192     bowersj2 1784:                         @parts = ($parts[0]);
1.133     bowersj2 1785:                         $args->{'condensed'} = 1;
1.132     bowersj2 1786:                     }
                   1787:                 }
1.198     bowersj2 1788: 		# Multipart problem with one part: always "condense" (happens
                   1789: 		#  to match the desirable behavior)
                   1790: 		if ($curRes->countParts() == 1) {
                   1791: 		    @parts = ($parts[0]);
                   1792: 		    $args->{'condensed'} = 1;
                   1793: 		}
1.132     bowersj2 1794:             }
1.157     bowersj2 1795:         } 
1.132     bowersj2 1796:             
                   1797:         # If the multipart problem was condensed, "forget" it was multipart
                   1798:         if (scalar(@parts) == 1) {
1.133     bowersj2 1799:             $args->{'multipart'} = 0;
1.192     bowersj2 1800:         } else {
                   1801:             # Add part 0 so we display it correctly.
                   1802:             unshift @parts, '0';
1.132     bowersj2 1803:         }
1.310     albertel 1804: 	
                   1805: 	{
                   1806: 	    my ($src,$symb,$anchor,$stack);
                   1807: 	    if ($args->{'sort'}) {
                   1808: 		my $it = $navmap->getIterator(undef, undef, undef, 1);
                   1809: 		while ( my $res=$it->next()) {
                   1810: 		    if (ref($res) &&
                   1811: 			$res->symb() eq  $curRes->symb()) { last; }
                   1812: 		}
                   1813: 		$stack=$it->getStack();
                   1814: 	    } else {
                   1815: 		$stack=$it->getStack();
                   1816: 	    }
                   1817: 	    ($src,$symb,$anchor)=getLinkForResource($stack);
                   1818: 	    if (defined($anchor)) { $anchor='#'.$anchor; }
                   1819: 	    my $srcHasQuestion = $src =~ /\?/;
                   1820: 	    $args->{"resourceLink"} = $src.
1.449     bisitz   1821: 		($srcHasQuestion?'&amp;':'?') .
1.384     www      1822: 		'symb=' . &escape($symb).$anchor;
1.310     albertel 1823: 	}
1.132     bowersj2 1824:         # Now, we've decided what parts to show. Loop through them and
                   1825:         # show them.
1.192     bowersj2 1826:         foreach my $part (@parts) {
1.132     bowersj2 1827:             $rownum ++;
                   1828:             
1.431     schulted 1829:             $result .= &Apache::loncommon::start_data_table_row();
1.134     bowersj2 1830: 
                   1831:             # Set up some data about the parts that the cols might want
                   1832:             my $filter = $it->{FILTER};
1.270     albertel 1833: 
1.132     bowersj2 1834:             # Now, display each column.
                   1835:             foreach my $col (@$cols) {
1.139     bowersj2 1836:                 my $colHTML = '';
                   1837:                 if (ref($col)) {
                   1838:                     $colHTML .= &$col($curRes, $part, $args);
                   1839:                 } else {
                   1840:                     $colHTML .= &{$preparedColumns[$col]}($curRes, $part, $args);
                   1841:                 }
1.132     bowersj2 1842: 
1.133     bowersj2 1843:                 # If this is the first column and it's time to print
                   1844:                 # the anchor, do so
                   1845:                 if ($col == $cols->[0] && 
                   1846:                     $args->{'counter'} == $args->{'currentJumpIndex'} - 
1.139     bowersj2 1847:                     $currentJumpDelta) {
                   1848:                     # Jam the anchor after the <td> tag;
                   1849:                     # necessary for valid HTML (which Mozilla requires)
                   1850:                     $colHTML =~ s/\>/\>\<a name="curloc" \/\>/;
1.133     bowersj2 1851:                     $displayedJumpMarker = 1;
                   1852:                 }
1.139     bowersj2 1853:                 $result .= $colHTML . "\n";
1.132     bowersj2 1854:             }
1.431     schulted 1855:             $result .= &Apache::loncommon::end_data_table_row();
1.140     bowersj2 1856:             $args->{'isNewBranch'} = 0;
1.139     bowersj2 1857:         }
1.153     bowersj2 1858: 
1.139     bowersj2 1859:         if ($r && $rownum % 20 == 0) {
                   1860:             $r->print($result);
                   1861:             $result = "";
                   1862:             $r->rflush();
1.132     bowersj2 1863:         }
1.156     bowersj2 1864:     } continue {
1.208     bowersj2 1865: 	if ($r) {
                   1866: 	    # If we have the connection, make sure the user is still connected
                   1867: 	    my $c = $r->connection;
                   1868: 	    if ($c->aborted()) {
                   1869: 		# Who cares what we do, nobody will see it anyhow.
                   1870: 		return '';
                   1871: 	    }
                   1872: 	}
1.132     bowersj2 1873:     }
1.486     raeburn  1874: 
                   1875:     $result.=&Apache::loncommon::end_data_table();
1.132     bowersj2 1876:     
1.134     bowersj2 1877:     # Print out the part that jumps to #curloc if it exists
1.159     bowersj2 1878:     # delay needed because the browser is processing the jump before
                   1879:     # it finishes rendering, so it goes to the wrong place!
                   1880:     # onload might be better, but this routine has no access to that.
                   1881:     # On mozilla, the 0-millisecond timeout seems to prevent this;
                   1882:     # it's quite likely this might fix other browsers, too, and 
                   1883:     # certainly won't hurt anything.
1.139     bowersj2 1884:     if ($displayedJumpMarker) {
1.449     bisitz   1885:         $result .= &Apache::lonhtmlcommon::scripttag("
1.247     albertel 1886: if (location.href.indexOf('#curloc')==-1) {
                   1887:     setTimeout(\"location += '#curloc';\", 0)
                   1888: }
1.449     bisitz   1889: ");
1.134     bowersj2 1890:     }
                   1891: 
1.140     bowersj2 1892:     if ($r) {
                   1893:         $r->print($result);
                   1894:         $result = "";
                   1895:         $r->rflush();
                   1896:     }
                   1897:         
1.132     bowersj2 1898:     return $result;
                   1899: }
                   1900: 
1.281     albertel 1901: sub add_linkitem {
                   1902:     my ($linkitems,$name,$cmd,$text)=@_;
                   1903:     $$linkitems{$name}{'cmd'}=$cmd;
                   1904:     $$linkitems{$name}{'text'}=&mt($text);
                   1905: }
                   1906: 
1.430     schulted 1907: sub show_linkitems_toolbar {
1.476     raeburn  1908:     my ($args,$condition) = @_;
1.478     raeburn  1909:     my $result;
1.476     raeburn  1910:     if (ref($args) eq 'HASH') {
                   1911:         if (ref($args->{'linkitems'}) eq 'HASH') {
1.478     raeburn  1912:             my $numlinks = scalar(keys(%{$args->{'linkitems'}}));
                   1913:             if ($numlinks > 1) {
                   1914:                 $result = '<td>'.
                   1915:                           &Apache::loncommon::help_open_menu('Navigation Screen','Navigation_Screen',
                   1916:                                                              undef,'RAT').
                   1917:                           '</td>'.
                   1918:                           '<td>&nbsp;</td>'.
                   1919:                           '<td class="LC_middle">'.&mt('Tools:').'</td>';
                   1920:             }
                   1921:             $result .= '<td align="left">'."\n".
                   1922:                        '<ul id="LC_toolbar">';
1.476     raeburn  1923:             my @linkorder = ('firsthomework','everything','uncompleted',
                   1924:                              'changefolder','clearbubbles','edittoplevel');
                   1925:             foreach my $link (@linkorder) {
                   1926:                 if (ref($args->{'linkitems'}{$link}) eq 'HASH') {
                   1927:                     if ($args->{'linkitems'}{$link}{'text'} ne '') {
                   1928:                         $args->{'linkitems'}{$link}{'cmd'}=~s/"/'/g;
                   1929:                         if ($args->{'linkitems'}{$link}{'cmd'}) {
1.478     raeburn  1930:                             my $link_id = 'LC_content_toolbar_'.$link;
1.476     raeburn  1931:                             if ($link eq 'changefolder') {
                   1932:                                 if ($condition) {
                   1933:                                     $link_id='LC_content_toolbar_changefolder_toggled';
                   1934:                                 } else {
                   1935:                                     $link_id='LC_content_toolbar_changefolder';
                   1936:                                 }
                   1937:                             }
                   1938:                             $result .= '<li><a href="#" '.
                   1939:                                        'onclick="'.$args->{'linkitems'}{$link}{'cmd'}.'" '.
                   1940:                                        'id="'.$link_id.'" '.
                   1941:                                        'class="LC_toolbarItem" '.
                   1942:                                        'title="'.$args->{'linkitems'}{$link}{'text'}.'">'.
                   1943:                                        '</a></li>'."\n";
1.446     raeburn  1944:                         }
                   1945:                     }
                   1946:                 }
                   1947:             }
1.476     raeburn  1948:             $result .= '</ul>'.
1.486     raeburn  1949:                        '</td>';
1.478     raeburn  1950:             if (($numlinks==1) && (exists($args->{'linkitems'}{'edittoplevel'}))) {
                   1951:                 $result .= '<td><a href="'.$args->{'linkitems'}{'edittoplevel'}{'cmd'}.'">'.
                   1952:                            &mt('Content Editor').'</a></td>';
                   1953:             }
1.476     raeburn  1954:         }
                   1955:         if ($args->{'sort_html'}) {
                   1956:             $result .= '<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>'.
                   1957:                        '<td align="right">'.$args->{'sort_html'}.'</td>';
1.446     raeburn  1958:         }
1.430     schulted 1959:     }
1.478     raeburn  1960:     if ($result) {
                   1961:         $result = "<table><tr>$result</tr></table>";
                   1962:     }
1.430     schulted 1963:     return $result;
                   1964: }
                   1965: 
1.133     bowersj2 1966: 1;
                   1967: 
1.418     jms      1968: 
                   1969: 
                   1970: 
                   1971: 
                   1972: 
                   1973: 
                   1974: 
                   1975: 
1.133     bowersj2 1976: package Apache::lonnavmaps::navmap;
                   1977: 
                   1978: =pod
                   1979: 
1.216     bowersj2 1980: =head1 Object: Apache::lonnavmaps::navmap
1.133     bowersj2 1981: 
1.217     bowersj2 1982: =head2 Overview
1.133     bowersj2 1983: 
1.217     bowersj2 1984: The navmap object's job is to provide access to the resources
                   1985: in the course as Apache::lonnavmaps::resource objects, and to
                   1986: query and manage the relationship between those resource objects.
                   1987: 
                   1988: Generally, you'll use the navmap object in one of three basic ways.
                   1989: In order of increasing complexity and power:
                   1990: 
                   1991: =over 4
                   1992: 
1.358     albertel 1993: =item * C<$navmap-E<gt>getByX>, where X is B<Id>, B<Symb> or B<MapPc> and getResourceByUrl. This provides
1.217     bowersj2 1994:     various ways to obtain resource objects, based on various identifiers.
                   1995:     Use this when you want to request information about one object or 
                   1996:     a handful of resources you already know the identities of, from some
                   1997:     other source. For more about Ids, Symbs, and MapPcs, see the
                   1998:     Resource documentation. Note that Url should be a B<last resort>,
1.358     albertel 1999:     not your first choice; it only really works when there is only one
1.217     bowersj2 2000:     instance of the resource in the course, which only applies to
1.358     albertel 2001:     maps, and even that may change in the future (see the B<getResourceByUrl>
                   2002:     documentation for more details.)
1.217     bowersj2 2003: 
                   2004: =item * C<my @resources = $navmap-E<gt>retrieveResources(args)>. This
                   2005:     retrieves resources matching some criterion and returns them
                   2006:     in a flat array, with no structure information. Use this when
                   2007:     you are manipulating a series of resources, based on what map
                   2008:     the are in, but do not care about branching, or exactly how
                   2009:     the maps and resources are related. This is the most common case.
                   2010: 
                   2011: =item * C<$it = $navmap-E<gt>getIterator(args)>. This allows you traverse
                   2012:     the course's navmap in various ways without writing the traversal
                   2013:     code yourself. See iterator documentation below. Use this when
                   2014:     you need to know absolutely everything about the course, including
                   2015:     branches and the precise relationship between maps and resources.
                   2016: 
                   2017: =back
                   2018: 
                   2019: =head2 Creation And Destruction
                   2020: 
                   2021: To create a navmap object, use the following function:
1.133     bowersj2 2022: 
                   2023: =over 4
                   2024: 
1.221     bowersj2 2025: =item * B<Apache::lonnavmaps::navmap-E<gt>new>():
1.133     bowersj2 2026: 
1.221     bowersj2 2027: Creates a new navmap object. Returns the navmap object if this is
                   2028: successful, or B<undef> if not.
1.174     albertel 2029: 
1.216     bowersj2 2030: =back
                   2031: 
                   2032: =head2 Methods
                   2033: 
                   2034: =over 4
                   2035: 
1.174     albertel 2036: =item * B<getIterator>(first, finish, filter, condition):
                   2037: 
                   2038: See iterator documentation below.
1.133     bowersj2 2039: 
                   2040: =cut
                   2041: 
                   2042: use strict;
                   2043: use GDBM_File;
1.320     albertel 2044: use Apache::lonnet;
1.397     albertel 2045: use LONCAPA;
1.133     bowersj2 2046: 
                   2047: sub new {
                   2048:     # magic invocation to create a class instance
                   2049:     my $proto = shift;
                   2050:     my $class = ref($proto) || $proto;
                   2051:     my $self = {};
1.501     raeburn  2052:     bless($self);		# So we can call change_user if necessary
1.133     bowersj2 2053: 
1.453     www      2054:     $self->{USERNAME} = shift || $env{'user.name'};
                   2055:     $self->{DOMAIN}   = shift || $env{'user.domain'};
1.491     raeburn  2056:     $self->{CODE}     = shift;
1.496     raeburn  2057:     $self->{NOHIDE} = shift;
1.453     www      2058: 
1.465     foxr     2059: 
                   2060: 
1.133     bowersj2 2061:     # Resource cache stores navmap resources as we reference them. We generate
                   2062:     # them on-demand so we don't pay for creating resources unless we use them.
                   2063:     $self->{RESOURCE_CACHE} = {};
                   2064: 
                   2065:     # Network failure flag, if we accessed the course or user opt and
                   2066:     # failed
                   2067:     $self->{NETWORK_FAILURE} = 0;
                   2068: 
1.465     foxr     2069:     # We can only tie the nav hash as done below if the username/domain
                   2070:     # match the env one. Otherwise change_user does everything we need...since we can't
1.501     raeburn  2071:     # assume there are course hashes for the specific requested user:domain
                   2072:     # Note: change_user is also called if we need the nav hash when printing CODEd 
                   2073:     # assignments or printing an exam, in which the enclosing folder for the items in
                   2074:     # the exam has hidden set.
1.465     foxr     2075:     #
1.133     bowersj2 2076: 
1.501     raeburn  2077:     if (($self->{USERNAME} eq $env{'user.name'}) && ($self->{DOMAIN} eq $env{'user.domain'}) &&
                   2078:          !$self->{CODE} && !$self->{NOHIDE}) {
1.465     foxr     2079: 	
                   2080: 	# tie the nav hash
                   2081: 	
                   2082: 	my %navmaphash;
                   2083: 	my %parmhash;
                   2084: 	my $courseFn = $env{"request.course.fn"};
                   2085: 	if (!(tie(%navmaphash, 'GDBM_File', "${courseFn}.db",
                   2086: 		  &GDBM_READER(), 0640))) {
                   2087: 	    return undef;
                   2088: 	}
                   2089: 	
                   2090: 	if (!(tie(%parmhash, 'GDBM_File', "${courseFn}_parms.db",
                   2091: 		  &GDBM_READER(), 0640)))
                   2092: 	{
                   2093: 	    untie %{$self->{PARM_HASH}};
                   2094: 	    return undef;
                   2095: 	}
                   2096: 	
                   2097: 	$self->{NAV_HASH} = \%navmaphash;
                   2098: 	$self->{PARM_HASH} = \%parmhash;
                   2099: 	$self->{PARM_CACHE} = {};
                   2100:     } else {
1.496     raeburn  2101: 	$self->change_user($self->{USERNAME}, $self->{DOMAIN},  $self->{CODE}, $self->{NOHIDE});
1.133     bowersj2 2102:     }
1.470     foxr     2103: 
1.465     foxr     2104:     return $self;
                   2105: }
                   2106: 
                   2107: #
                   2108: #  In some instances it is useful to be able to dynamically change the
                   2109: # username/domain associated with a navmap (e.g. to navigate for someone
                   2110: # else besides the current user...if sufficiently privileged.
                   2111: # Parameters:
                   2112: #    user  - New user.
                   2113: #    domain- Domain the user belongs to.
1.491     raeburn  2114: #    code  - Anonymous CODE in use.
1.465     foxr     2115: # Implicit inputs:
                   2116: #   
                   2117: sub change_user {
                   2118:     my $self = shift;
                   2119:     $self->{USERNAME} = shift;
                   2120:     $self->{DOMAIN}   = shift;
1.491     raeburn  2121:     $self->{CODE}     = shift;
1.496     raeburn  2122:     $self->{NOHIDE}   = shift;
1.465     foxr     2123: 
                   2124:     # If the hashes are already tied make sure to break that bond:
                   2125: 
                   2126:     untie %{$self->{NAV_HASH}}; 
                   2127:     untie %{$self->{PARM_HASH}};
                   2128: 
                   2129:     # The assumption is that we have to
                   2130:     # use lonmap here to re-read the hash and from it reconstruct
                   2131:     # new big and parameter hashes.  An implicit assumption at this time
                   2132:     # is that the course file is probably not created locally yet
                   2133:     # an that we will therefore just read without tying.
                   2134: 
                   2135:     my ($cdom, $cnum) = split(/\_/, $env{'request.course.id'});
                   2136: 
                   2137:     my %big_hash;
1.496     raeburn  2138:     &Apache::lonmap::loadmap($cnum, $cdom, $self->{USERNAME}, $self->{DOMAIN}, $self->{CODE}, $self->{NOHIDE}, \%big_hash);
1.465     foxr     2139:     $self->{NAV_HASH} = \%big_hash;
                   2140: 
1.470     foxr     2141: 
                   2142: 
1.514     raeburn  2143:     # Now clear the parm cache and reconstruct the parm hash from the big_hash
1.465     foxr     2144:     # param.xxxx keys.
                   2145: 
                   2146:     $self->{PARM_CACHE} = {};
1.133     bowersj2 2147:     
1.465     foxr     2148:     my %parm_hash = {};
1.504     raeburn  2149:     foreach my $key (keys(%big_hash)) {
1.465     foxr     2150: 	if ($key =~ /^param\./) {
                   2151: 	    my $param_key = $key;
                   2152: 	    $param_key =~ s/^param\.//;
                   2153: 	    $parm_hash{$param_key} = $big_hash{$key};
                   2154: 	}
1.133     bowersj2 2155:     }
                   2156: 
1.465     foxr     2157:     $self->{PARM_HASH} = \%parm_hash;
                   2158: 
1.500     raeburn  2159: }
1.133     bowersj2 2160: 
1.221     bowersj2 2161: sub generate_course_user_opt {
1.133     bowersj2 2162:     my $self = shift;
1.221     bowersj2 2163:     if ($self->{COURSE_USER_OPT_GENERATED}) { return; }
1.133     bowersj2 2164: 
1.453     www      2165:     my $uname=$self->{USERNAME};
                   2166:     my $udom=$self->{DOMAIN};
                   2167: 
1.320     albertel 2168:     my $cid=$env{'request.course.id'};
1.326     albertel 2169:     my $cdom=$env{'course.'.$cid.'.domain'};
                   2170:     my $cnum=$env{'course.'.$cid.'.num'};
1.221     bowersj2 2171:     
1.133     bowersj2 2172: # ------------------------------------------------- Get coursedata (if present)
1.326     albertel 2173:     my $courseopt=&Apache::lonnet::get_courseresdata($cnum,$cdom);
                   2174:     # Check for network failure
                   2175:     if (!ref($courseopt)) {
                   2176: 	if ( $courseopt =~ /no.such.host/i || $courseopt =~ /con_lost/i) {
1.324     albertel 2177: 	    $self->{NETWORK_FAILURE} = 1;
1.221     bowersj2 2178: 	}
1.326     albertel 2179: 	undef($courseopt);
                   2180:     }
1.325     albertel 2181: 
1.133     bowersj2 2182: # --------------------------------------------------- Get userdata (if present)
1.325     albertel 2183: 	
1.326     albertel 2184:     my $useropt=&Apache::lonnet::get_userresdata($uname,$udom);
                   2185:     # Check for network failure
                   2186:     if (!ref($useropt)) {
                   2187: 	if ( $useropt =~ /no.such.host/i || $useropt =~ /con_lost/i) {
1.324     albertel 2188: 	    $self->{NETWORK_FAILURE} = 1;
1.221     bowersj2 2189: 	}
1.326     albertel 2190: 	undef($useropt);
1.221     bowersj2 2191:     }
                   2192: 
1.326     albertel 2193:     $self->{COURSE_OPT} = $courseopt;
                   2194:     $self->{USER_OPT} = $useropt;
                   2195: 
1.221     bowersj2 2196:     $self->{COURSE_USER_OPT_GENERATED} = 1;
                   2197:     
                   2198:     return;
                   2199: }
                   2200: 
1.485     foxr     2201: 
                   2202: 
1.221     bowersj2 2203: sub generate_email_discuss_status {
                   2204:     my $self = shift;
1.259     raeburn  2205:     my $symb = shift;
1.221     bowersj2 2206:     if ($self->{EMAIL_DISCUSS_GENERATED}) { return; }
1.133     bowersj2 2207: 
1.320     albertel 2208:     my $cid=$env{'request.course.id'};
1.326     albertel 2209:     my $cdom=$env{'course.'.$cid.'.domain'};
                   2210:     my $cnum=$env{'course.'.$cid.'.num'};
1.221     bowersj2 2211:     
1.454     www      2212:     my %emailstatus = &Apache::lonnet::dump('email_status',$self->{DOMAIN},$self->{USERNAME});
1.221     bowersj2 2213:     my $logoutTime = $emailstatus{'logout'};
1.320     albertel 2214:     my $courseLeaveTime = $emailstatus{'logout_'.$env{'request.course.id'}};
1.221     bowersj2 2215:     $self->{LAST_CHECK} = (($courseLeaveTime > $logoutTime) ?
                   2216: 			   $courseLeaveTime : $logoutTime);
                   2217:     my %discussiontime = &Apache::lonnet::dump('discussiontimes', 
                   2218: 					       $cdom, $cnum);
1.259     raeburn  2219:     my %lastread = &Apache::lonnet::dump('nohist_'.$cid.'_discuss',
1.453     www      2220:                                         $self->{DOMAIN},$self->{USERNAME},'lastread');
1.259     raeburn  2221:     my %lastreadtime = ();
1.504     raeburn  2222:     foreach my $key (keys(%lastread)) {
1.394     raeburn  2223:         my $shortkey = $key;
                   2224:         $shortkey =~ s/_lastread$//;
                   2225:         $lastreadtime{$shortkey} = $lastread{$key};
1.259     raeburn  2226:     }
                   2227: 
1.221     bowersj2 2228:     my %feedback=();
                   2229:     my %error=();
1.453     www      2230:     my @keys = &Apache::lonnet::getkeys('nohist_email',$self->{DOMAIN},
                   2231: 					$self->{USERNAME});
1.221     bowersj2 2232:     
1.325     albertel 2233:     foreach my $msgid (@keys) {
1.295     albertel 2234: 	if ((!$emailstatus{$msgid}) || ($emailstatus{$msgid} eq 'new')) {
1.395     raeburn  2235:             my ($sendtime,$shortsubj,$fromname,$fromdomain,$status,$fromcid,
                   2236:                 $symb,$error) = &Apache::lonmsg::unpackmsgid($msgid);
1.396     raeburn  2237:             &Apache::lonenc::check_decrypt(\$symb); 
                   2238:             if (($fromcid ne '') && ($fromcid ne $cid)) {
                   2239:                 next;
                   2240:             }
1.395     raeburn  2241:             if (defined($symb)) {
                   2242:                 if (defined($error) && $error == 1) {
                   2243:                     $error{$symb}.=','.$msgid;
                   2244:                 } else {
                   2245:                     $feedback{$symb}.=','.$msgid;
                   2246:                 }
                   2247:             } else {
                   2248:                 my $plain=
                   2249:                     &LONCAPA::unescape(&LONCAPA::unescape($msgid));
                   2250:                 if ($plain=~/ \[([^\]]+)\]\:/) {
                   2251:                     my $url=$1;
                   2252:                     if ($plain=~/\:Error \[/) {
                   2253:                         $error{$url}.=','.$msgid;
                   2254:                     } else {
                   2255:                         $feedback{$url}.=','.$msgid;
                   2256:                     }
                   2257:                 }
                   2258:             }
1.221     bowersj2 2259: 	}
1.211     bowersj2 2260:     }
1.221     bowersj2 2261:     
1.395     raeburn  2262:     #symbs of resources that have feedbacks (will be urls pre-2.3)
1.221     bowersj2 2263:     $self->{FEEDBACK} = \%feedback;
1.395     raeburn  2264:     #or errors (will be urls pre 2.3)
1.295     albertel 2265:     $self->{ERROR_MSG} = \%error;
1.221     bowersj2 2266:     $self->{DISCUSSION_TIME} = \%discussiontime;
                   2267:     $self->{EMAIL_STATUS} = \%emailstatus;
1.259     raeburn  2268:     $self->{LAST_READ} = \%lastreadtime;
1.221     bowersj2 2269:     
                   2270:     $self->{EMAIL_DISCUSS_GENERATED} = 1;
                   2271: }
                   2272: 
                   2273: sub get_user_data {
                   2274:     my $self = shift;
                   2275:     if ($self->{RETRIEVED_USER_DATA}) { return; }
1.211     bowersj2 2276: 
1.221     bowersj2 2277:     # Retrieve performance data on problems
1.320     albertel 2278:     my %student_data = Apache::lonnet::currentdump($env{'request.course.id'},
1.453     www      2279: 						   $self->{DOMAIN},
                   2280: 						   $self->{USERNAME});
1.221     bowersj2 2281:     $self->{STUDENT_DATA} = \%student_data;
1.133     bowersj2 2282: 
1.221     bowersj2 2283:     $self->{RETRIEVED_USER_DATA} = 1;
1.133     bowersj2 2284: }
                   2285: 
1.354     raeburn  2286: sub get_discussion_data {
                   2287:     my $self = shift;
                   2288:     if ($self->{RETRIEVED_DISCUSSION_DATA}) {
1.366     albertel 2289: 	return $self->{DISCUSSION_DATA};
1.354     raeburn  2290:     }
1.366     albertel 2291: 
                   2292:     $self->generate_email_discuss_status();    
                   2293: 
1.354     raeburn  2294:     my $cid=$env{'request.course.id'};
                   2295:     my $cdom=$env{'course.'.$cid.'.domain'};
                   2296:     my $cnum=$env{'course.'.$cid.'.num'};
                   2297:     # Retrieve discussion data for resources in course
1.367     albertel 2298:     my %discussion_data = &Apache::lonnet::dumpstore($cid,$cdom,$cnum);
1.366     albertel 2299: 
                   2300: 
1.354     raeburn  2301:     $self->{DISCUSSION_DATA} = \%discussion_data;
                   2302:     $self->{RETRIEVED_DISCUSSION_DATA} = 1;
                   2303:     return $self->{DISCUSSION_DATA};
                   2304: }
                   2305: 
                   2306: 
1.133     bowersj2 2307: # Internal function: Takes a key to look up in the nav hash and implements internal
                   2308: # memory caching of that key.
                   2309: sub navhash {
                   2310:     my $self = shift; my $key = shift;
                   2311:     return $self->{NAV_HASH}->{$key};
                   2312: }
                   2313: 
1.217     bowersj2 2314: =pod
                   2315: 
                   2316: =item * B<courseMapDefined>(): Returns true if the course map is defined, 
                   2317:     false otherwise. Undefined course maps indicate an error somewhere in
                   2318:     LON-CAPA, and you will not be able to proceed with using the navmap.
                   2319:     See the B<NAV> screen for an example of using this.
                   2320: 
                   2321: =cut
                   2322: 
1.133     bowersj2 2323: # Checks to see if coursemap is defined, matching test in old lonnavmaps
                   2324: sub courseMapDefined {
                   2325:     my $self = shift;
1.320     albertel 2326:     my $uri = &Apache::lonnet::clutter($env{'request.course.uri'});
1.133     bowersj2 2327: 
                   2328:     my $firstres = $self->navhash("map_start_$uri");
                   2329:     my $lastres = $self->navhash("map_finish_$uri");
                   2330:     return $firstres && $lastres;
                   2331: }
                   2332: 
                   2333: sub getIterator {
                   2334:     my $self = shift;
                   2335:     my $iterator = Apache::lonnavmaps::iterator->new($self, shift, shift,
1.314     albertel 2336:                                                      shift, undef, shift,
                   2337: 						     shift, shift);
1.133     bowersj2 2338:     return $iterator;
                   2339: }
                   2340: 
                   2341: # Private method: Does the given resource (as a symb string) have
                   2342: # current discussion? Returns 0 if chat/mail data not extracted.
                   2343: sub hasDiscussion {
                   2344:     my $self = shift;
                   2345:     my $symb = shift;
1.221     bowersj2 2346:     $self->generate_email_discuss_status();
                   2347: 
1.133     bowersj2 2348:     if (!defined($self->{DISCUSSION_TIME})) { return 0; }
                   2349: 
                   2350:     #return defined($self->{DISCUSSION_TIME}->{$symb});
1.259     raeburn  2351: 
1.323     albertel 2352:     # backward compatibility (bulletin boards used to be 'wrapped')
                   2353:     my $ressymb = $self->wrap_symb($symb);
1.259     raeburn  2354:     if ( defined ( $self->{LAST_READ}->{$ressymb} ) ) {
                   2355:         return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_READ}->{$ressymb};
                   2356:     } else {
1.266     raeburn  2357: #        return $self->{DISCUSSION_TIME}->{$ressymb} >  $self->{LAST_CHECK}; # v.1.1 behavior 
                   2358:         return $self->{DISCUSSION_TIME}->{$ressymb} >  0; # in 1.2 will display speech bubble icons for all items with posts until marked as read (even if read in v 1.1).
1.259     raeburn  2359:     }
1.133     bowersj2 2360: }
                   2361: 
1.366     albertel 2362: sub last_post_time {
                   2363:     my $self = shift;
                   2364:     my $symb = shift;
                   2365:     my $ressymb = $self->wrap_symb($symb);
                   2366:     return $self->{DISCUSSION_TIME}->{$ressymb};
                   2367: }
                   2368: 
1.393     raeburn  2369: sub discussion_info {
1.366     albertel 2370:     my $self = shift;
                   2371:     my $symb = shift;
1.393     raeburn  2372:     my $filter = shift;
1.366     albertel 2373: 
                   2374:     $self->get_discussion_data();
1.372     raeburn  2375: 
1.366     albertel 2376:     my $ressymb = $self->wrap_symb($symb);
1.372     raeburn  2377:     # keys used to store bulletinboard postings use 'unwrapped' symb. 
1.397     albertel 2378:     my $discsymb = &escape($self->unwrap_symb($ressymb));
1.372     raeburn  2379:     my $version = $self->{DISCUSSION_DATA}{'version:'.$discsymb};
1.366     albertel 2380:     if (!$version) { return; }
                   2381: 
                   2382:     my $prevread = $self->{LAST_READ}{$ressymb};
                   2383: 
1.393     raeburn  2384:     my $count = 0;
1.366     albertel 2385:     my $hiddenflag = 0;
                   2386:     my $deletedflag = 0;
1.393     raeburn  2387:     my ($hidden,$deleted,%info);
1.366     albertel 2388: 
                   2389:     for (my $id=$version; $id>0; $id--) {
1.372     raeburn  2390: 	my $vkeys=$self->{DISCUSSION_DATA}{$id.':keys:'.$discsymb};
1.366     albertel 2391: 	my @keys=split(/:/,$vkeys);
                   2392: 	if (grep(/^hidden$/ ,@keys)) {
                   2393: 	    if (!$hiddenflag) {
1.372     raeburn  2394: 		$hidden = $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':hidden'};
1.366     albertel 2395: 		$hiddenflag = 1;
                   2396: 	    }
                   2397: 	} elsif (grep(/^deleted$/,@keys)) {
                   2398: 	    if (!$deletedflag) {
1.372     raeburn  2399: 		$deleted = $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':deleted'};
1.366     albertel 2400: 		$deletedflag = 1;
                   2401: 	    }
                   2402: 	} else {
1.393     raeburn  2403: 	    if (($hidden !~/\.$id\./) && ($deleted !~/\.$id\./)) {
                   2404:                 if ($filter eq 'unread') {
                   2405: 		    if ($prevread >= $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':timestamp'}) {
                   2406:                         next;
                   2407:                     }
                   2408:                 }
                   2409: 		$count++;
                   2410: 		$info{$count}{'subject'} =
                   2411: 		    $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':subject'};
                   2412:                 $info{$count}{'id'} = $id;
                   2413:                 $info{$count}{'timestamp'} = $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':timestamp'};
                   2414:             }
1.366     albertel 2415: 	}
                   2416:     }
                   2417:     if (wantarray) {
1.393     raeburn  2418: 	return ($count,%info);
1.366     albertel 2419:     }
1.393     raeburn  2420:     return $count;
1.366     albertel 2421: }
                   2422: 
1.321     raeburn  2423: sub wrap_symb {
1.322     albertel 2424:     my $self = shift;
1.321     raeburn  2425:     my $symb = shift;
1.373     albertel 2426:     if ($symb =~ m-___(adm/[^/]+/[^/]+/)(\d+)(/bulletinboard)$-) {
1.322     albertel 2427:         unless ($symb =~ m|adm/wrapper/adm|) {
                   2428:             $symb = 'bulletin___'.$2.'___adm/wrapper/'.$1.$2.$3;
1.321     raeburn  2429:         }
                   2430:     }
1.322     albertel 2431:     return $symb;
1.321     raeburn  2432: }
                   2433: 
1.372     raeburn  2434: sub unwrap_symb {
                   2435:     my $self = shift;
                   2436:     my $ressymb = shift;
                   2437:     my $discsymb = $ressymb;
1.373     albertel 2438:     if ($ressymb =~ m-^(bulletin___\d+___)adm/wrapper/(adm/[^/]+/[^/]+/\d+/bulletinboard)$-) {
1.372     raeburn  2439:          $discsymb = $1.$2;
                   2440:     }
                   2441:     return $discsymb;
                   2442: }
                   2443: 
1.322     albertel 2444: # Private method: Does the given resource (as a symb string) have
                   2445: # current feedback? Returns the string in the feedback hash, which
                   2446: # will be false if it does not exist.
                   2447: 
1.133     bowersj2 2448: sub getFeedback { 
                   2449:     my $self = shift;
                   2450:     my $symb = shift;
1.395     raeburn  2451:     my $source = shift;
1.133     bowersj2 2452: 
1.221     bowersj2 2453:     $self->generate_email_discuss_status();
                   2454: 
1.133     bowersj2 2455:     if (!defined($self->{FEEDBACK})) { return ""; }
                   2456:     
1.395     raeburn  2457:     my $feedback;
                   2458:     if ($self->{FEEDBACK}->{$symb}) {
                   2459:         $feedback = $self->{FEEDBACK}->{$symb};
                   2460:         if ($self->{FEEDBACK}->{$source}) {
                   2461:             $feedback .= ','.$self->{FEEDBACK}->{$source};
                   2462:         }
                   2463:     } else {
                   2464:         if ($self->{FEEDBACK}->{$source}) {
                   2465:             $feedback = $self->{FEEDBACK}->{$source};
                   2466:         }
                   2467:     }
                   2468:     return $feedback;
1.133     bowersj2 2469: }
                   2470: 
                   2471: # Private method: Get the errors for that resource (by source).
                   2472: sub getErrors { 
                   2473:     my $self = shift;
1.395     raeburn  2474:     my $symb = shift;
1.133     bowersj2 2475:     my $src = shift;
1.221     bowersj2 2476: 
                   2477:     $self->generate_email_discuss_status();
                   2478: 
1.133     bowersj2 2479:     if (!defined($self->{ERROR_MSG})) { return ""; }
1.395     raeburn  2480: 
                   2481:     my $errors;
                   2482:     if ($self->{ERROR_MSG}->{$symb}) {
                   2483:         $errors = $self->{ERROR_MSG}->{$symb};
                   2484:         if ($self->{ERROR_MSG}->{$src}) {
                   2485:             $errors .= ','.$self->{ERROR_MSG}->{$src};
                   2486:         }
                   2487:     } else {
                   2488:         if ($self->{ERROR_MSG}->{$src}) {
                   2489:             $errors = $self->{ERROR_MSG}->{$src};
                   2490:         }
                   2491:     }
                   2492:     return $errors;
1.133     bowersj2 2493: }
                   2494: 
                   2495: =pod
                   2496: 
1.174     albertel 2497: =item * B<getById>(id):
                   2498: 
                   2499: Based on the ID of the resource (1.1, 3.2, etc.), get a resource
                   2500: object for that resource. This method, or other methods that use it
                   2501: (as in the resource object) is the only proper way to obtain a
                   2502: resource object.
1.133     bowersj2 2503: 
1.194     bowersj2 2504: =item * B<getBySymb>(symb):
                   2505: 
                   2506: Based on the symb of the resource, get a resource object for that
                   2507: resource. This is one of the proper ways to get a resource object.
                   2508: 
1.448     raeburn  2509: =item * B<getByMapPc>(map_pc):
1.194     bowersj2 2510: 
                   2511: Based on the map_pc of the resource, get a resource object for
                   2512: the given map. This is one of the proper ways to get a resource object.
                   2513: 
1.133     bowersj2 2514: =cut
                   2515: 
                   2516: # The strategy here is to cache the resource objects, and only construct them
                   2517: # as we use them. The real point is to prevent reading any more from the tied
1.398     banghart 2518: # hash than we have to, which should hopefully alleviate speed problems.
1.133     bowersj2 2519: 
                   2520: sub getById {
                   2521:     my $self = shift;
                   2522:     my $id = shift;
                   2523: 
                   2524:     if (defined ($self->{RESOURCE_CACHE}->{$id}))
                   2525:     {
                   2526:         return $self->{RESOURCE_CACHE}->{$id};
                   2527:     }
                   2528: 
                   2529:     # resource handles inserting itself into cache.
                   2530:     # Not clear why the quotes are necessary, but as of this
                   2531:     # writing it doesn't work without them.
                   2532:     return "Apache::lonnavmaps::resource"->new($self, $id);
1.132     bowersj2 2533: }
1.133     bowersj2 2534: 
1.172     bowersj2 2535: sub getBySymb {
                   2536:     my $self = shift;
                   2537:     my $symb = shift;
1.277     matthew  2538: 
1.228     albertel 2539:     my ($mapUrl, $id, $filename) = &Apache::lonnet::decode_symb($symb);
1.172     bowersj2 2540:     my $map = $self->getResourceByUrl($mapUrl);
1.277     matthew  2541:     my $returnvalue = undef;
                   2542:     if (ref($map)) {
                   2543:         $returnvalue = $self->getById($map->map_pc() .'.'.$id);
                   2544:     }
                   2545:     return $returnvalue;
1.194     bowersj2 2546: }
                   2547: 
                   2548: sub getByMapPc {
                   2549:     my $self = shift;
                   2550:     my $map_pc = shift;
                   2551:     my $map_id = $self->{NAV_HASH}->{'map_id_' . $map_pc};
                   2552:     $map_id = $self->{NAV_HASH}->{'ids_' . $map_id};
                   2553:     return $self->getById($map_id);
1.172     bowersj2 2554: }
                   2555: 
1.133     bowersj2 2556: =pod
                   2557: 
1.174     albertel 2558: =item * B<firstResource>():
                   2559: 
                   2560: Returns a resource object reference corresponding to the first
                   2561: resource in the navmap.
1.133     bowersj2 2562: 
                   2563: =cut
                   2564: 
                   2565: sub firstResource {
                   2566:     my $self = shift;
                   2567:     my $firstResource = $self->navhash('map_start_' .
1.320     albertel 2568:                      &Apache::lonnet::clutter($env{'request.course.uri'}));
1.133     bowersj2 2569:     return $self->getById($firstResource);
1.132     bowersj2 2570: }
1.133     bowersj2 2571: 
                   2572: =pod
                   2573: 
1.174     albertel 2574: =item * B<finishResource>():
                   2575: 
                   2576: Returns a resource object reference corresponding to the last resource
                   2577: in the navmap.
1.133     bowersj2 2578: 
                   2579: =cut
                   2580: 
                   2581: sub finishResource {
                   2582:     my $self = shift;
                   2583:     my $firstResource = $self->navhash('map_finish_' .
1.320     albertel 2584:                      &Apache::lonnet::clutter($env{'request.course.uri'}));
1.133     bowersj2 2585:     return $self->getById($firstResource);
1.132     bowersj2 2586: }
1.133     bowersj2 2587: 
                   2588: # Parmval reads the parm hash and cascades the lookups. parmval_real does
                   2589: # the actual lookup; parmval caches the results.
                   2590: sub parmval {
                   2591:     my $self = shift;
1.399     albertel 2592:     my ($what,$symb,$recurse)=@_;
1.133     bowersj2 2593:     my $hashkey = $what."|||".$symb;
1.459     foxr     2594:     my $cache = $self->{PARM_CACHE};
1.133     bowersj2 2595:     if (defined($self->{PARM_CACHE}->{$hashkey})) {
1.411     raeburn  2596:         if (ref($self->{PARM_CACHE}->{$hashkey}) eq 'ARRAY') { 
                   2597:             if (defined($self->{PARM_CACHE}->{$hashkey}->[0])) {
                   2598:                 if (wantarray) {
                   2599:                     return @{$self->{PARM_CACHE}->{$hashkey}};
                   2600:                 } else {
                   2601:                     return $self->{PARM_CACHE}->{$hashkey}->[0];
                   2602:                 }
                   2603:             }
                   2604:         } else {
                   2605:             return $self->{PARM_CACHE}->{$hashkey};
                   2606:         }
1.133     bowersj2 2607:     }
1.515     raeburn  2608: 
1.399     albertel 2609:     my $result = $self->parmval_real($what, $symb, $recurse);
1.133     bowersj2 2610:     $self->{PARM_CACHE}->{$hashkey} = $result;
1.405     albertel 2611:     if (wantarray) {
1.411     raeburn  2612:         return @{$result};
1.405     albertel 2613:     }
                   2614:     return $result->[0];
1.132     bowersj2 2615: }
                   2616: 
1.485     foxr     2617: 
1.133     bowersj2 2618: sub parmval_real {
                   2619:     my $self = shift;
1.219     albertel 2620:     my ($what,$symb,$recurse) = @_;
1.133     bowersj2 2621: 
1.485     foxr     2622: 
1.221     bowersj2 2623:     # Make sure the {USER_OPT} and {COURSE_OPT} hashes are populated
                   2624:     $self->generate_course_user_opt();
                   2625: 
1.320     albertel 2626:     my $cid=$env{'request.course.id'};
                   2627:     my $csec=$env{'request.course.sec'};
1.350     raeburn  2628:     my $cgroup='';
                   2629:     my @cgrps=split(/:/,$env{'request.course.groups'});
                   2630:     if (@cgrps > 0) {
                   2631:         @cgrps = sort(@cgrps);
                   2632:         $cgroup = $cgrps[0];
                   2633:     } 
1.453     www      2634:     my $uname=$self->{USERNAME};
                   2635:     my $udom=$self->{DOMAIN};
1.133     bowersj2 2636: 
1.405     albertel 2637:     unless ($symb) { return ['']; }
1.133     bowersj2 2638:     my $result='';
                   2639: 
1.226     www      2640:     my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb);
1.383     albertel 2641:     $mapname = &Apache::lonnet::deversion($mapname);
1.515     raeburn  2642:     my ($recursed,@recurseup); 
                   2643:     
1.133     bowersj2 2644: # ----------------------------------------------------- Cascading lookup scheme
                   2645:     my $rwhat=$what;
                   2646:     $what=~s/^parameter\_//;
                   2647:     $what=~s/\_/\./;
                   2648: 
                   2649:     my $symbparm=$symb.'.'.$what;
1.515     raeburn  2650:     my $recurseparm=$mapname.'___(rec).'.$what;
1.133     bowersj2 2651:     my $mapparm=$mapname.'___(all).'.$what;
1.325     albertel 2652:     my $usercourseprefix=$cid;
1.515     raeburn  2653:     
1.485     foxr     2654: 
                   2655: 
1.350     raeburn  2656:     my $grplevel=$usercourseprefix.'.['.$cgroup.'].'.$what;
                   2657:     my $grplevelr=$usercourseprefix.'.['.$cgroup.'].'.$symbparm;
1.515     raeburn  2658:     my $grpleveli=$usercourseprefix.'.['.$cgroup.'].'.$recurseparm;
1.350     raeburn  2659:     my $grplevelm=$usercourseprefix.'.['.$cgroup.'].'.$mapparm;
                   2660: 
1.485     foxr     2661: 
1.133     bowersj2 2662:     my $seclevel= $usercourseprefix.'.['.$csec.'].'.$what;
                   2663:     my $seclevelr=$usercourseprefix.'.['.$csec.'].'.$symbparm;
1.515     raeburn  2664:     my $secleveli=$usercourseprefix.'.['.$csec.'].'.$recurseparm;
1.133     bowersj2 2665:     my $seclevelm=$usercourseprefix.'.['.$csec.'].'.$mapparm;
                   2666: 
1.485     foxr     2667: 
1.133     bowersj2 2668:     my $courselevel= $usercourseprefix.'.'.$what;
                   2669:     my $courselevelr=$usercourseprefix.'.'.$symbparm;
1.515     raeburn  2670:     my $courseleveli=$usercourseprefix.'.'.$recurseparm;
1.133     bowersj2 2671:     my $courselevelm=$usercourseprefix.'.'.$mapparm;
                   2672: 
1.485     foxr     2673: 
1.133     bowersj2 2674:     my $useropt = $self->{USER_OPT};
                   2675:     my $courseopt = $self->{COURSE_OPT};
                   2676:     my $parmhash = $self->{PARM_HASH};
                   2677: 
                   2678: # ---------------------------------------------------------- first, check user
                   2679:     if ($uname and defined($useropt)) {
1.405     albertel 2680:         if (defined($$useropt{$courselevelr})) { return [$$useropt{$courselevelr},'resource']; }
                   2681:         if (defined($$useropt{$courselevelm})) { return [$$useropt{$courselevelm},'map']; }
1.515     raeburn  2682:         if (defined($$useropt{$courseleveli})) { return [$$useropt{$courseleveli},'map']; }
                   2683:         unless ($recursed) {
                   2684:             @recurseup = $self->recurseup_maps($mapname);
                   2685:             $recursed = 1;
                   2686:         }
                   2687:         foreach my $item (@recurseup) {
                   2688:             my $norecursechk=$usercourseprefix.'.'.$item.'___(all).'.$what;
                   2689:             last if (defined($$useropt{$norecursechk}));
                   2690:             my $recursechk=$usercourseprefix.'.'.$item.'___(rec).'.$what;
                   2691:             if (defined($$useropt{$recursechk})) { return [$$useropt{$recursechk},'map']; } 
                   2692:         }
1.405     albertel 2693:         if (defined($$useropt{$courselevel})) { return [$$useropt{$courselevel},'course']; }
1.133     bowersj2 2694:     }
                   2695: 
                   2696: # ------------------------------------------------------- second, check course
1.350     raeburn  2697:     if ($cgroup ne '' and defined($courseopt)) {
1.405     albertel 2698:         if (defined($$courseopt{$grplevelr})) { return [$$courseopt{$grplevelr},'resource']; }
                   2699:         if (defined($$courseopt{$grplevelm})) { return [$$courseopt{$grplevelm},'map']; }
1.515     raeburn  2700:         if (defined($$courseopt{$grpleveli})) { return [$$courseopt{$grpleveli},'map']; } 
                   2701:         unless ($recursed) {
                   2702:             @recurseup = $self->recurseup_maps($mapname);
                   2703:             $recursed = 1;
                   2704:         }
                   2705:         foreach my $item (@recurseup) {
                   2706:             my $norecursechk=$usercourseprefix.'.['.$cgroup.'].'.$item.'___(all).'.$what;
                   2707:             last if (defined($$courseopt{$norecursechk}));
                   2708:             my $recursechk=$usercourseprefix.'.['.$cgroup.'].'.$item.'___(rec).'.$what;
                   2709:             if (defined($$courseopt{$recursechk})) { return [$$courseopt{$recursechk},'map']; }      
                   2710:         }
1.405     albertel 2711:         if (defined($$courseopt{$grplevel})) { return [$$courseopt{$grplevel},'course']; }
1.350     raeburn  2712:     }
                   2713: 
1.515     raeburn  2714:     if ($csec ne '' and defined($courseopt)) {
1.405     albertel 2715:         if (defined($$courseopt{$seclevelr})) { return [$$courseopt{$seclevelr},'resource']; }
                   2716:         if (defined($$courseopt{$seclevelm})) { return [$$courseopt{$seclevelm},'map']; }
1.515     raeburn  2717:         if (defined($$courseopt{$secleveli})) { return [$$courseopt{$secleveli},'map']; } 
                   2718:         unless ($recursed) {
                   2719:             @recurseup = $self->recurseup_maps($mapname);
                   2720:             $recursed = 1;
                   2721:         }
                   2722:         foreach my $item (@recurseup) {
                   2723:             my $norecursechk=$usercourseprefix.'.['.$csec.'].'.$item.'___(all).'.$what;
                   2724:             last if (defined($$courseopt{$norecursechk}));
                   2725:             my $recursechk=$usercourseprefix.'.['.$csec.'].'.$item.'___(rec).'.$what;
                   2726:             if (defined($$courseopt{$recursechk})) { return [$$courseopt{$recursechk},'map']; }
                   2727:         }
1.405     albertel 2728:         if (defined($$courseopt{$seclevel})) { return [$$courseopt{$seclevel},'course']; }
1.133     bowersj2 2729:     }
                   2730: 
                   2731:     if (defined($courseopt)) {
1.405     albertel 2732:         if (defined($$courseopt{$courselevelr})) { return [$$courseopt{$courselevelr},'resource']; }
1.133     bowersj2 2733:     }
                   2734: 
                   2735: # ----------------------------------------------------- third, check map parms
                   2736: 
                   2737:     my $thisparm=$$parmhash{$symbparm};
1.405     albertel 2738:     if (defined($thisparm)) { return [$thisparm,'map']; }
1.133     bowersj2 2739: 
                   2740: # ----------------------------------------------------- fourth , check default
                   2741: 
1.246     albertel 2742:     my $meta_rwhat=$rwhat;
                   2743:     $meta_rwhat=~s/\./_/g;
                   2744:     my $default=&Apache::lonnet::metadata($fn,$meta_rwhat);
1.405     albertel 2745:     if (defined($default)) { return [$default,'resource']}
1.246     albertel 2746:     $default=&Apache::lonnet::metadata($fn,'parameter_'.$meta_rwhat);
1.405     albertel 2747:     if (defined($default)) { return [$default,'resource']}
1.315     albertel 2748: # --------------------------------------------------- fifth, check more course
                   2749:     if (defined($courseopt)) {
1.405     albertel 2750:         if (defined($$courseopt{$courselevelm})) { return [$$courseopt{$courselevelm},'map']; }
1.515     raeburn  2751:         if (defined($$courseopt{$courseleveli})) { return [$$courseopt{$courseleveli},'map']; }
                   2752:         unless ($recursed) {
                   2753:             @recurseup = $self->recurseup_maps($mapname);
                   2754:             $recursed = 1;
                   2755:         }
                   2756:         foreach my $item (@recurseup) {
                   2757:             my $norecursechk=$usercourseprefix.'.'.$item.'___(all).'.$what;
                   2758:             last if (defined($$courseopt{$norecursechk}));
                   2759:             my $recursechk=$usercourseprefix.'.'.$item.'___(rec).'.$what;
                   2760:             if (defined($$courseopt{$recursechk})) {
                   2761:                 return [$$courseopt{$recursechk},'map'];
                   2762:             }
                   2763:         }
1.405     albertel 2764:         if (defined($$courseopt{$courselevel})) {
                   2765:            my $ret = [$$courseopt{$courselevel},'course'];
                   2766:            return $ret;
                   2767:        }
1.315     albertel 2768:     }
                   2769: # --------------------------------------------------- sixth , cascade up parts
1.133     bowersj2 2770: 
                   2771:     my ($space,@qualifier)=split(/\./,$rwhat);
                   2772:     my $qualifier=join('.',@qualifier);
                   2773:     unless ($space eq '0') {
1.160     albertel 2774: 	my @parts=split(/_/,$space);
                   2775: 	my $id=pop(@parts);
                   2776: 	my $part=join('_',@parts);
                   2777: 	if ($part eq '') { $part='0'; }
1.405     albertel 2778:        my @partgeneral=$self->parmval($part.".$qualifier",$symb,1);
                   2779:        if (defined($partgeneral[0])) { return \@partgeneral; }
1.133     bowersj2 2780:     }
1.405     albertel 2781:     if ($recurse) { return []; }
1.399     albertel 2782:     my $pack_def=&Apache::lonnet::packages_tab_default($fn,'resource.'.$rwhat);
1.405     albertel 2783:     if (defined($pack_def)) { return [$pack_def,'resource']; }
                   2784:     return [''];
1.145     bowersj2 2785: }
1.515     raeburn  2786: 
                   2787: sub recurseup_maps {
                   2788:     my ($self,$mapname) = @_;
                   2789:     my @recurseup;
1.517     raeburn  2790:     if ($mapname) {
1.519     raeburn  2791:         my $res = $self->getResourceByUrl($mapname);
                   2792:         if (ref($res)) {
                   2793:             my @pcs = split(/,/,$res->map_hierarchy());
                   2794:             shift(@pcs);
                   2795:             pop(@pcs);
                   2796:             if (@pcs) {
                   2797:                 @recurseup = map { &Apache::lonnet::declutter($self->getByMapPc($_)->src()); } reverse(@pcs);
                   2798:             }
1.517     raeburn  2799:         }
1.515     raeburn  2800:     }
                   2801:     return @recurseup;
                   2802: }
                   2803: 
1.485     foxr     2804: #
                   2805: #  Determines the open/close dates for printing a map that
                   2806: #  encloses a resource.
                   2807: #
                   2808: sub map_printdates {
                   2809:     my ($self, $res, $part) = @_;
                   2810: 
                   2811: 
                   2812: 
                   2813: 
                   2814: 
1.518     raeburn  2815:     my $opendate = $self->get_mapparam($res->symb(),'',"$part.printstartdate");
                   2816:     my $closedate= $self->get_mapparam($res->symb(),'',"$part.printenddate");
1.485     foxr     2817: 
                   2818: 
                   2819:     return ($opendate, $closedate);
                   2820: }
                   2821: 
                   2822: sub get_mapparam {
1.518     raeburn  2823:     my ($self, $symb, $mapname, $what) = @_;
1.485     foxr     2824: 
                   2825:     # Ensure the course option hash is populated:
                   2826: 
                   2827:     $self->generate_course_user_opt();
                   2828: 
                   2829:     # Get the course id and section if there is one.
                   2830: 
                   2831:     my $cid=$env{'request.course.id'};
                   2832:     my $csec=$env{'request.course.sec'};
                   2833:     my $cgroup='';
                   2834:     my @cgrps=split(/:/,$env{'request.course.groups'});
                   2835:     if (@cgrps > 0) {
                   2836:         @cgrps = sort(@cgrps);
                   2837:         $cgroup = $cgrps[0];
                   2838:     } 
                   2839:     my $uname=$self->{USERNAME};
                   2840:     my $udom=$self->{DOMAIN};
                   2841: 
1.518     raeburn  2842:     unless ($symb || $mapname) { return; }
1.485     foxr     2843:     my $result='';
1.516     raeburn  2844:     my ($recursed,@recurseup);
1.485     foxr     2845: 
                   2846: 
                   2847:     # Figure out which map we are in.
                   2848: 
1.518     raeburn  2849:     if ($symb && !$mapname) {
                   2850:         my ($id,$fn);
                   2851:         ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb);
                   2852:         $mapname = &Apache::lonnet::deversion($mapname);
                   2853:     }
1.485     foxr     2854: 
                   2855: 
                   2856:     my $rwhat=$what;
                   2857:     $what=~s/^parameter\_//;
                   2858:     $what=~s/\_/\./;
                   2859: 
                   2860:     # Build the hash keys for the lookup:
                   2861: 
                   2862:     my $mapparm=$mapname.'___(all).'.$what;
1.515     raeburn  2863:     my $recurseparm=$mapname.'___(rec).'.$what; 
1.485     foxr     2864:     my $usercourseprefix=$cid;
                   2865: 
                   2866: 
1.515     raeburn  2867:     my $grplevelm    = "$usercourseprefix.[$cgroup].$mapparm";
                   2868:     my $seclevelm    = "$usercourseprefix.[$csec].$mapparm";
                   2869:     my $courselevelm = "$usercourseprefix.$mapparm";
                   2870: 
                   2871:     my $grpleveli    = "$usercourseprefix.[$cgroup].$recurseparm";
                   2872:     my $secleveli    = "$usercourseprefix.[$csec].$recurseparm";
                   2873:     my $courseleveli = "$usercourseprefix.$recurseparm";
1.485     foxr     2874: 
                   2875:     # Get handy references to the hashes we need in $self:
                   2876: 
                   2877:     my $useropt = $self->{USER_OPT};
                   2878:     my $courseopt = $self->{COURSE_OPT};
                   2879:     my $parmhash = $self->{PARM_HASH};
                   2880: 
                   2881:     # Check per user 
                   2882: 
                   2883: 
                   2884: 
                   2885:     if ($uname and defined($useropt)) {
1.515     raeburn  2886: 	if (defined($$useropt{$courselevelm})) {
                   2887: 	    return $$useropt{$courselevelm};
1.485     foxr     2888: 	}
1.515     raeburn  2889:         if (defined($$useropt{$courseleveli})) {
                   2890:             return $$useropt{$courseleveli};
                   2891:         }
1.516     raeburn  2892:         unless ($recursed) {
                   2893:             @recurseup = $self->recurseup_maps($mapname);
                   2894:             $recursed = 1;
                   2895:         }
                   2896:         foreach my $item (@recurseup) {
                   2897:             my $norecursechk=$usercourseprefix.'.'.$item.'___(all).'.$what;
                   2898:             last if (defined($$useropt{$norecursechk}));
                   2899:             my $recursechk=$usercourseprefix.'.'.$item.'___(rec).'.$what;
                   2900:             if (defined($$useropt{$recursechk})) {
                   2901:                 return $$useropt{$recursechk};
                   2902:             }
                   2903:         }
1.485     foxr     2904:     }
                   2905: 
                   2906:     # Check course -- group
                   2907: 
                   2908: 
                   2909: 
                   2910:     if ($cgroup ne '' and defined ($courseopt)) {
1.515     raeburn  2911: 	if (defined($$courseopt{$grplevelm})) {
                   2912: 	    return $$courseopt{$grplevelm};
1.485     foxr     2913: 	}
1.515     raeburn  2914:         if (defined($$courseopt{$grpleveli})) {
                   2915:             return $$courseopt{$grpleveli};
                   2916:         }
1.516     raeburn  2917:         unless ($recursed) {
                   2918:             @recurseup = $self->recurseup_maps($mapname);
                   2919:             $recursed = 1;
                   2920:         }
                   2921:         foreach my $item (@recurseup) {
                   2922:             my $norecursechk=$usercourseprefix.'.['.$cgroup.'].'.$item.'___(all).'.$what;
                   2923:             last if (defined($$courseopt{$norecursechk}));
                   2924:             my $recursechk=$usercourseprefix.'.['.$cgroup.'].'.$item.'___(rec).'.$what;
                   2925:             if (defined($$courseopt{$recursechk})) {
                   2926:                 return $$courseopt{$recursechk};
                   2927:             }
                   2928:         }
1.485     foxr     2929:     }
                   2930: 
                   2931:     # Check course -- section
                   2932: 
                   2933: 
1.516     raeburn  2934:     if ($csec ne '' and defined($courseopt)) {
1.515     raeburn  2935: 	if (defined($$courseopt{$seclevelm})) {
                   2936: 	    return $$courseopt{$seclevelm};
1.485     foxr     2937: 	}
1.515     raeburn  2938:         if (defined($$courseopt{$secleveli})) {
                   2939:             return $$courseopt{$secleveli};
                   2940:         }
1.516     raeburn  2941:         unless ($recursed) {
                   2942:             @recurseup = $self->recurseup_maps($mapname);
                   2943:             $recursed = 1;
                   2944:         }
                   2945:         foreach my $item (@recurseup) {
                   2946:             my $norecursechk=$usercourseprefix.'.['.$csec.'].'.$item.'___(all).'.$what;
                   2947:             last if (defined($$courseopt{$norecursechk}));
                   2948:             my $recursechk=$usercourseprefix.'.['.$csec.'].'.$item.'___(rec).'.$what;
                   2949:             if (defined($$courseopt{$recursechk})) {
                   2950:                 return $$courseopt{$recursechk};
                   2951:             }
                   2952:         }
1.485     foxr     2953:     }
                   2954:     # Check the map parameters themselves:
                   2955: 
1.518     raeburn  2956:     if ($symb) {
                   2957:         my $symbparm=$symb.'.'.$what;
                   2958:         my $thisparm = $$parmhash{$symbparm};
                   2959:         if (defined($thisparm)) {
                   2960: 	    return $thisparm;
                   2961:         }
1.485     foxr     2962:     }
                   2963: 
                   2964: 
                   2965:     # Additional course parameters:
                   2966: 
                   2967:     if (defined($courseopt)) {
1.515     raeburn  2968: 	if (defined($$courseopt{$courselevelm})) {
                   2969: 	    return $$courseopt{$courselevelm};
1.485     foxr     2970: 	}
1.516     raeburn  2971:         unless ($recursed) {
                   2972:             @recurseup = $self->recurseup_maps($mapname);
                   2973:             $recursed = 1;
                   2974:         }
                   2975:         if (@recurseup) {
                   2976:             foreach my $item (@recurseup) {
                   2977:                 my $norecursechk=$usercourseprefix.'.'.$item.'___(all).'.$what;
                   2978:                 last if (defined($$courseopt{$norecursechk}));
                   2979:                 my $recursechk=$usercourseprefix.'.'.$item.'___(rec).'.$what;
                   2980:                 if (defined($$courseopt{$recursechk})) {
                   2981:                     return $$courseopt{$recursechk};
                   2982:                 }
                   2983:             }
                   2984:         }
1.485     foxr     2985:     }
1.516     raeburn  2986:     return undef;		# Undefined if we got here.
1.485     foxr     2987: }
                   2988: 
                   2989: sub course_printdates {
                   2990:     my ($self, $symb,  $part) = @_;
                   2991: 
                   2992: 
                   2993:     my $opendate  = $self->getcourseparam($symb, $part . '.printstartdate');
                   2994:     my $closedate = $self->getcourseparam($symb, $part . '.printenddate');
                   2995:     return ($opendate, $closedate);
                   2996: 
                   2997: }
                   2998: 
                   2999: sub getcourseparam {
                   3000:     my ($self, $symb, $what) = @_;
                   3001: 
                   3002:     $self->generate_course_user_opt(); # If necessary populate the hashes.
                   3003: 
                   3004:     my $uname = $self->{USERNAME};
                   3005:     my $udom  = $self->{DOMAIN};
                   3006:     
                   3007:     # Course, section, group ids come from the env:
                   3008: 
                   3009:     my $cid   = $env{'request.course.id'};
                   3010:     my $csec  = $env{'request.course.sec'};
                   3011:     my $cgroup = '';		# Assume no group
                   3012: 
                   3013:     my @cgroups = split(/:/, $env{'request.course.groups'});
                   3014:     if(@cgroups > 0) {
                   3015: 	@cgroups = sort(@cgroups);
                   3016: 	$cgroup  = $cgroups[0];	# There is a course group. 
                   3017:    }
                   3018:     my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb);
                   3019:     $mapname = &Apache::lonnet::deversion($mapname);
                   3020: 
                   3021:     #
                   3022:     # Make the various lookup keys:
                   3023:     #
                   3024: 
                   3025:     $what=~s/^parameter\_//;
                   3026:     $what=~s/\_/\./;
                   3027: 
                   3028:     # Local refs to the hashes we're going to look at:
                   3029: 
                   3030:     my $useropt   = $self->{USER_OPT};
                   3031:     my $courseopt = $self->{COURSE_OPT};
                   3032: 
                   3033:     # 
                   3034:     # We want the course level stuff from the way
                   3035:     # parmval_real operates 
1.513     raeburn  3036:     # TODO: Factor some of this stuff out of
1.485     foxr     3037:     # both parmval_real and here
                   3038:     #
                   3039:     my $courselevel = $cid . '.' .  $what;
                   3040:     my $grplevel    = $cid . '.[' . $cgroup   . ']' . $what;
                   3041:     my $seclevel    = $cid . '.[' . $csec     . ']' . $what;
                   3042: 
                   3043: 
                   3044:     # Try for the user's course level option:
                   3045: 
                   3046:     if ($uname and defined($useropt)) {
                   3047: 	if (defined($$useropt{$courselevel})) {
                   3048: 	    return $$useropt{$courselevel};
                   3049: 	}
                   3050:     }
                   3051:     # Try for the group's course level option:
                   3052: 
1.513     raeburn  3053:     if ($cgroup ne '' and defined($courseopt)) {
1.485     foxr     3054: 	if (defined($$courseopt{$grplevel})) {
                   3055: 	    return $$courseopt{$grplevel};
                   3056: 	}
                   3057:     }
                   3058: 
                   3059:     #  Try for section level parameters:
                   3060: 
1.513     raeburn  3061:     if ($csec ne '' and defined($courseopt)) {
1.485     foxr     3062: 	if (defined($$courseopt{$seclevel})) {
                   3063: 	    return $$courseopt{$seclevel};
                   3064: 	}
                   3065:     }
1.513     raeburn  3066:     # Try for 'additional' course parameters:
1.485     foxr     3067: 
                   3068:     if (defined($courseopt)) {
                   3069: 	if (defined($$courseopt{$courselevel})) {
                   3070: 	    return $$courseopt{$courselevel};
                   3071: 	}
                   3072:     }
                   3073:     return undef;
                   3074: 
                   3075: }
                   3076: 
1.145     bowersj2 3077: 
1.174     albertel 3078: =pod
1.145     bowersj2 3079: 
1.352     raeburn  3080: =item * B<getResourceByUrl>(url,multiple):
1.145     bowersj2 3081: 
1.352     raeburn  3082: Retrieves a resource object by URL of the resource, unless the optional
1.398     banghart 3083: multiple parameter is included in which case an array of resource 
1.352     raeburn  3084: objects is returned. If passed a resource object, it will simply return  
                   3085: it, so it is safe to use this method in code like
                   3086: "$res = $navmap->getResourceByUrl($res)"
                   3087: if you're not sure if $res is already an object, or just a URL. If the
                   3088: resource appears multiple times in the course, only the first instance 
                   3089: will be returned (useful for maps), unless the multiple parameter has
                   3090: been included, in which case all instances are returned in an array.
1.174     albertel 3091: 
1.505     raeburn  3092: =item * B<retrieveResources>(map, filterFunc, recursive, bailout, showall, noblockcheck):
1.174     albertel 3093: 
                   3094: The map is a specification of a map to retreive the resources from,
                   3095: either as a url or as an object. The filterFunc is a reference to a
                   3096: function that takes a resource object as its one argument and returns
                   3097: true if the resource should be included, or false if it should not
                   3098: be. If recursive is true, the map will be recursively examined,
                   3099: otherwise it will not be. If bailout is true, the function will return
1.314     albertel 3100: as soon as it finds a resource, if false it will finish. If showall is
1.505     raeburn  3101: true it will not hide maps that contain nothing but one other map. The 
                   3102: noblockcheck arg is propagated to become the sixth arg in the call to
                   3103: lonnet::allowed when checking a resource's availability during collection
                   3104: of resources using the iterator. noblockcheck needs to be true if 
                   3105: retrieveResources() was called by a routine that itself was called by 
                   3106: lonnet::allowed, in order to avoid recursion.  By default the map  
                   3107: is the top-level map of the course, filterFunc is a function that 
                   3108: always returns 1, recursive is true, bailout is false, showall is
                   3109: false. The resources will be returned in a list containing the
                   3110: resource objects for the corresponding resources, with B<no structure 
                   3111: information> in the list; regardless of branching, recursion, etc.,
                   3112: it will be a flat list.
1.174     albertel 3113: 
                   3114: Thus, this is suitable for cases where you don't want the structure,
                   3115: just a list of all resources. It is also suitable for finding out how
                   3116: many resources match a given description; for this use, if all you
                   3117: want to know is if I<any> resources match the description, the bailout
                   3118: parameter will allow you to avoid potentially expensive enumeration of
                   3119: all matching resources.
1.145     bowersj2 3120: 
1.318     albertel 3121: =item * B<hasResource>(map, filterFunc, recursive, showall):
1.145     bowersj2 3122: 
1.398     banghart 3123: Convenience method for
1.146     bowersj2 3124: 
1.318     albertel 3125:  scalar(retrieveResources($map, $filterFunc, $recursive, 1, $showall)) > 0
1.146     bowersj2 3126: 
1.174     albertel 3127: which will tell whether the map has resources matching the description
                   3128: in the filter function.
1.146     bowersj2 3129: 
1.352     raeburn  3130: =item * B<usedVersion>(url):
                   3131: 
                   3132: Retrieves version infomation for a url. Returns the version (a number, or 
                   3133: the string "mostrecent") for resources which have version information in  
                   3134: the big hash.
1.448     raeburn  3135: 
1.145     bowersj2 3136: =cut
                   3137: 
1.277     matthew  3138: 
1.145     bowersj2 3139: sub getResourceByUrl {
                   3140:     my $self = shift;
                   3141:     my $resUrl = shift;
1.352     raeburn  3142:     my $multiple = shift;
1.145     bowersj2 3143: 
                   3144:     if (ref($resUrl)) { return $resUrl; }
                   3145: 
                   3146:     $resUrl = &Apache::lonnet::clutter($resUrl);
                   3147:     my $resId = $self->{NAV_HASH}->{'ids_' . $resUrl};
1.352     raeburn  3148:     if (!$resId) { return ''; }
                   3149:     if ($multiple) {
                   3150:         my @resources = ();
                   3151:         my @resIds = split (/,/, $resId);
                   3152:         foreach my $id (@resIds) {
1.353     raeburn  3153:             my $resourceId = $self->getById($id);
                   3154:             if ($resourceId) { 
                   3155:                 push(@resources,$resourceId);
1.352     raeburn  3156:             }
                   3157:         }
                   3158:         return @resources;
                   3159:     } else {
                   3160:         if ($resId =~ /,/) {
                   3161:             $resId = (split (/,/, $resId))[0];
                   3162:         }
                   3163:         return $self->getById($resId);
1.145     bowersj2 3164:     }
                   3165: }
                   3166: 
                   3167: sub retrieveResources {
                   3168:     my $self = shift;
                   3169:     my $map = shift;
                   3170:     my $filterFunc = shift;
                   3171:     if (!defined ($filterFunc)) {
                   3172:         $filterFunc = sub {return 1;};
                   3173:     }
                   3174:     my $recursive = shift;
                   3175:     if (!defined($recursive)) { $recursive = 1; }
                   3176:     my $bailout = shift;
                   3177:     if (!defined($bailout)) { $bailout = 0; }
1.314     albertel 3178:     my $showall = shift;
1.506     raeburn  3179:     my $noblockcheck = shift;
1.145     bowersj2 3180:     # Create the necessary iterator.
                   3181:     if (!ref($map)) { # assume it's a url of a map.
1.172     bowersj2 3182:         $map = $self->getResourceByUrl($map);
1.145     bowersj2 3183:     }
                   3184: 
1.213     bowersj2 3185:     # If nothing was passed, assume top-level map
                   3186:     if (!$map) {
                   3187: 	$map = $self->getById('0.0');
                   3188:     }
                   3189: 
1.145     bowersj2 3190:     # Check the map's validity.
1.213     bowersj2 3191:     if (!$map->is_map()) {
1.145     bowersj2 3192:         # Oh, to throw an exception.... how I'd love that!
                   3193:         return ();
                   3194:     }
                   3195: 
1.146     bowersj2 3196:     # Get an iterator.
                   3197:     my $it = $self->getIterator($map->map_start(), $map->map_finish(),
1.314     albertel 3198:                                 undef, $recursive, $showall);
1.146     bowersj2 3199: 
                   3200:     my @resources = ();
                   3201: 
1.400     albertel 3202:     if (&$filterFunc($map)) {
                   3203: 	push(@resources, $map);
                   3204:     }
                   3205: 
1.146     bowersj2 3206:     # Run down the iterator and collect the resources.
1.222     bowersj2 3207:     my $curRes;
                   3208: 
1.505     raeburn  3209:     while ($curRes = $it->next(undef,$noblockcheck)) {
1.146     bowersj2 3210:         if (ref($curRes)) {
                   3211:             if (!&$filterFunc($curRes)) {
                   3212:                 next;
                   3213:             }
                   3214: 
1.400     albertel 3215:             push(@resources, $curRes);
1.146     bowersj2 3216: 
                   3217:             if ($bailout) {
                   3218:                 return @resources;
                   3219:             }
                   3220:         }
                   3221: 
                   3222:     }
                   3223: 
                   3224:     return @resources;
                   3225: }
                   3226: 
                   3227: sub hasResource {
                   3228:     my $self = shift;
                   3229:     my $map = shift;
                   3230:     my $filterFunc = shift;
                   3231:     my $recursive = shift;
1.318     albertel 3232:     my $showall = shift;
1.146     bowersj2 3233:     
1.318     albertel 3234:     return scalar($self->retrieveResources($map, $filterFunc, $recursive, 1, $showall)) > 0;
1.133     bowersj2 3235: }
1.51      bowersj2 3236: 
1.352     raeburn  3237: sub usedVersion {
                   3238:     my $self = shift;
                   3239:     my $linkurl = shift;
                   3240:     return $self->navhash("version_$linkurl");
                   3241: }
                   3242: 
1.51      bowersj2 3243: 1;
                   3244: 
                   3245: package Apache::lonnavmaps::iterator;
1.365     albertel 3246: use Scalar::Util qw(weaken);
1.320     albertel 3247: use Apache::lonnet;
                   3248: 
1.51      bowersj2 3249: =pod
                   3250: 
                   3251: =back
                   3252: 
1.174     albertel 3253: =head1 Object: navmap Iterator
1.51      bowersj2 3254: 
1.174     albertel 3255: An I<iterator> encapsulates the logic required to traverse a data
                   3256: structure. navmap uses an iterator to traverse the course map
                   3257: according to the criteria you wish to use.
                   3258: 
                   3259: To obtain an iterator, call the B<getIterator>() function of a
                   3260: B<navmap> object. (Do not instantiate Apache::lonnavmaps::iterator
                   3261: directly.) This will return a reference to the iterator:
1.51      bowersj2 3262: 
                   3263: C<my $resourceIterator = $navmap-E<gt>getIterator();>
                   3264: 
                   3265: To get the next thing from the iterator, call B<next>:
                   3266: 
                   3267: C<my $nextThing = $resourceIterator-E<gt>next()>
                   3268: 
                   3269: getIterator behaves as follows:
                   3270: 
                   3271: =over 4
                   3272: 
1.174     albertel 3273: =item * B<getIterator>(firstResource, finishResource, filterHash, condition, forceTop, returnTopMap):
                   3274: 
                   3275: All parameters are optional. firstResource is a resource reference
                   3276: corresponding to where the iterator should start. It defaults to
                   3277: navmap->firstResource() for the corresponding nav map. finishResource
                   3278: corresponds to where you want the iterator to end, defaulting to
                   3279: navmap->finishResource(). filterHash is a hash used as a set
                   3280: containing strings representing the resource IDs, defaulting to
                   3281: empty. Condition is a 1 or 0 that sets what to do with the filter
1.205     bowersj2 3282: hash: If a 0, then only resources that exist IN the filterHash will be
1.174     albertel 3283: recursed on. If it is a 1, only resources NOT in the filterHash will
                   3284: be recursed on. Defaults to 0. forceTop is a boolean value. If it is
                   3285: false (default), the iterator will only return the first level of map
                   3286: that is not just a single, 'redirecting' map. If true, the iterator
                   3287: will return all information, starting with the top-level map,
                   3288: regardless of content. returnTopMap, if true (default false), will
                   3289: cause the iterator to return the top-level map object (resource 0.0)
                   3290: before anything else.
                   3291: 
                   3292: Thus, by default, only top-level resources will be shown. Change the
                   3293: condition to a 1 without changing the hash, and all resources will be
                   3294: shown. Changing the condition to 1 and including some values in the
                   3295: hash will allow you to selectively suppress parts of the navmap, while
                   3296: leaving it on 0 and adding things to the hash will allow you to
                   3297: selectively add parts of the nav map. See the handler code for
                   3298: examples.
                   3299: 
                   3300: The iterator will return either a reference to a resource object, or a
                   3301: token representing something in the map, such as the beginning of a
                   3302: new branch. The possible tokens are:
                   3303: 
                   3304: =over 4
1.51      bowersj2 3305: 
1.222     bowersj2 3306: =item * B<END_ITERATOR>:
                   3307: 
                   3308: The iterator has returned all that it's going to. Further calls to the
                   3309: iterator will just produce more of these. This is a "false" value, and
                   3310: is the only false value the iterator which will be returned, so it can
                   3311: be used as a loop sentinel.
                   3312: 
1.217     bowersj2 3313: =item * B<BEGIN_MAP>:
1.51      bowersj2 3314: 
1.174     albertel 3315: A new map is being recursed into. This is returned I<after> the map
                   3316: resource itself is returned.
1.51      bowersj2 3317: 
1.217     bowersj2 3318: =item * B<END_MAP>:
1.174     albertel 3319: 
                   3320: The map is now done.
1.51      bowersj2 3321: 
1.217     bowersj2 3322: =item * B<BEGIN_BRANCH>:
1.70      bowersj2 3323: 
1.174     albertel 3324: A branch is now starting. The next resource returned will be the first
                   3325: in that branch.
1.70      bowersj2 3326: 
1.217     bowersj2 3327: =item * B<END_BRANCH>:
1.70      bowersj2 3328: 
1.174     albertel 3329: The branch is now done.
1.51      bowersj2 3330: 
                   3331: =back
                   3332: 
1.174     albertel 3333: The tokens are retreivable via methods on the iterator object, i.e.,
                   3334: $iterator->END_MAP.
1.70      bowersj2 3335: 
1.174     albertel 3336: Maps can contain empty resources. The iterator will automatically skip
                   3337: over such resources, but will still treat the structure
                   3338: correctly. Thus, a complicated map with several branches, but
                   3339: consisting entirely of empty resources except for one beginning or
                   3340: ending resource, will cause a lot of BRANCH_STARTs and BRANCH_ENDs,
                   3341: but only one resource will be returned.
1.116     bowersj2 3342: 
1.242     matthew  3343: =back
                   3344: 
1.222     bowersj2 3345: =head2 Normal Usage
                   3346: 
                   3347: Normal usage of the iterator object is to do the following:
                   3348: 
                   3349:  my $it = $navmap->getIterator([your params here]);
                   3350:  my $curRes;
                   3351:  while ($curRes = $it->next()) {
                   3352:    [your logic here]
                   3353:  }
                   3354: 
                   3355: Note that inside of the loop, it's frequently useful to check if
                   3356: "$curRes" is a reference or not with the reference function; only
                   3357: resource objects will be references, and any non-references will 
                   3358: be the tokens described above.
                   3359: 
1.505     raeburn  3360: The next() routine can take two (optional) arguments:
                   3361: closeAllPages - if true will not recurse down a .page
                   3362: noblockcheck - passed to browsePriv() for passing as sixth arg to
                   3363: call to lonnet::allowed. This needs to be set if retrieveResources
                   3364: was already called from another routine called within lonnet::allowed, 
                   3365: so as to prevent recursion.
                   3366: 
1.506     raeburn  3367: Also note there is some old code floating around that tries to track
1.222     bowersj2 3368: the depth of the iterator to see when it's done; do not copy that 
1.398     banghart 3369: code. It is difficult to get right and harder to understand than
1.222     bowersj2 3370: this. They should be migrated to this new style.
1.51      bowersj2 3371: 
                   3372: =cut
                   3373: 
                   3374: # Here are the tokens for the iterator:
                   3375: 
1.222     bowersj2 3376: sub END_ITERATOR { return 0; }
1.51      bowersj2 3377: sub BEGIN_MAP { return 1; }    # begining of a new map
                   3378: sub END_MAP { return 2; }      # end of the map
                   3379: sub BEGIN_BRANCH { return 3; } # beginning of a branch
                   3380: sub END_BRANCH { return 4; }   # end of a branch
1.89      bowersj2 3381: sub FORWARD { return 1; }      # go forward
                   3382: sub BACKWARD { return 2; }
1.51      bowersj2 3383: 
1.96      bowersj2 3384: sub min {
                   3385:     (my $a, my $b) = @_;
                   3386:     if ($a < $b) { return $a; } else { return $b; }
                   3387: }
                   3388: 
1.94      bowersj2 3389: sub new {
                   3390:     # magic invocation to create a class instance
                   3391:     my $proto = shift;
                   3392:     my $class = ref($proto) || $proto;
                   3393:     my $self = {};
                   3394: 
1.300     albertel 3395:     weaken($self->{NAV_MAP} = shift);
1.94      bowersj2 3396:     return undef unless ($self->{NAV_MAP});
                   3397: 
1.454     www      3398:     $self->{USERNAME} = $self->{NAV_MAP}->{USERNAME};
                   3399:     $self->{DOMAIN}   = $self->{NAV_MAP}->{DOMAIN};
                   3400: 
1.94      bowersj2 3401:     # Handle the parameters
                   3402:     $self->{FIRST_RESOURCE} = shift || $self->{NAV_MAP}->firstResource();
                   3403:     $self->{FINISH_RESOURCE} = shift || $self->{NAV_MAP}->finishResource();
                   3404: 
                   3405:     # If the given resources are just the ID of the resource, get the
                   3406:     # objects
                   3407:     if (!ref($self->{FIRST_RESOURCE})) { $self->{FIRST_RESOURCE} = 
                   3408:              $self->{NAV_MAP}->getById($self->{FIRST_RESOURCE}); }
                   3409:     if (!ref($self->{FINISH_RESOURCE})) { $self->{FINISH_RESOURCE} = 
                   3410:              $self->{NAV_MAP}->getById($self->{FINISH_RESOURCE}); }
                   3411: 
                   3412:     $self->{FILTER} = shift;
                   3413: 
                   3414:     # A hash, used as a set, of resource already seen
                   3415:     $self->{ALREADY_SEEN} = shift;
                   3416:     if (!defined($self->{ALREADY_SEEN})) { $self->{ALREADY_SEEN} = {} };
                   3417:     $self->{CONDITION} = shift;
                   3418: 
1.116     bowersj2 3419:     # Do we want to automatically follow "redirection" maps?
                   3420:     $self->{FORCE_TOP} = shift;
                   3421: 
1.162     bowersj2 3422:     # Do we want to return the top-level map object (resource 0.0)?
                   3423:     $self->{RETURN_0} = shift;
                   3424:     # have we done that yet?
                   3425:     $self->{HAVE_RETURNED_0} = 0;
                   3426: 
1.94      bowersj2 3427:     # Now, we need to pre-process the map, by walking forward and backward
                   3428:     # over the parts of the map we're going to look at.
1.96      bowersj2 3429: 
1.97      bowersj2 3430:     # The processing steps are exactly the same, except for a few small 
                   3431:     # changes, so I bundle those up in the following list of two elements:
                   3432:     # (direction_to_iterate, VAL_name, next_resource_method_to_call,
                   3433:     # first_resource).
                   3434:     # This prevents writing nearly-identical code twice.
                   3435:     my @iterations = ( [FORWARD(), 'TOP_DOWN_VAL', 'getNext', 
                   3436:                         'FIRST_RESOURCE'],
                   3437:                        [BACKWARD(), 'BOT_UP_VAL', 'getPrevious', 
                   3438:                         'FINISH_RESOURCE'] );
                   3439: 
1.98      bowersj2 3440:     my $maxDepth = 0; # tracks max depth
                   3441: 
1.116     bowersj2 3442:     # If there is only one resource in this map, and it's a map, we
                   3443:     # want to remember that, so the user can ask for the first map
                   3444:     # that isn't just a redirector.
                   3445:     my $resource; my $resourceCount = 0;
                   3446: 
1.209     bowersj2 3447:     # Documentation on this algorithm can be found in the CVS repository at 
                   3448:     # /docs/lonnavdocs; these "**#**" markers correspond to documentation
                   3449:     # in that file.
1.107     bowersj2 3450:     # **1**
                   3451: 
1.97      bowersj2 3452:     foreach my $pass (@iterations) {
                   3453:         my $direction = $pass->[0];
                   3454:         my $valName = $pass->[1];
                   3455:         my $nextResourceMethod = $pass->[2];
                   3456:         my $firstResourceName = $pass->[3];
                   3457: 
                   3458:         my $iterator = Apache::lonnavmaps::DFSiterator->new($self->{NAV_MAP}, 
                   3459:                                                             $self->{FIRST_RESOURCE},
                   3460:                                                             $self->{FINISH_RESOURCE},
                   3461:                                                             {}, undef, 0, $direction);
1.96      bowersj2 3462:     
1.97      bowersj2 3463:         # prime the recursion
                   3464:         $self->{$firstResourceName}->{DATA}->{$valName} = 0;
1.222     bowersj2 3465: 	$iterator->next();
1.97      bowersj2 3466:         my $curRes = $iterator->next();
1.222     bowersj2 3467: 	my $depth = 1;
                   3468:         while ($depth > 0) {
                   3469: 	    if ($curRes == $iterator->BEGIN_MAP()) { $depth++; }
                   3470: 	    if ($curRes == $iterator->END_MAP()) { $depth--; }
                   3471: 
1.97      bowersj2 3472:             if (ref($curRes)) {
1.116     bowersj2 3473:                 # If there's only one resource, this will save it
1.117     bowersj2 3474:                 # we have to filter empty resources from consideration here,
                   3475:                 # or even "empty", redirecting maps have two (start & finish)
                   3476:                 # or three (start, finish, plus redirector)
                   3477:                 if($direction == FORWARD && $curRes->src()) { 
                   3478:                     $resource = $curRes; $resourceCount++; 
                   3479:                 }
1.97      bowersj2 3480:                 my $resultingVal = $curRes->{DATA}->{$valName};
                   3481:                 my $nextResources = $curRes->$nextResourceMethod();
1.116     bowersj2 3482:                 my $nextCount = scalar(@{$nextResources});
1.104     bowersj2 3483: 
1.116     bowersj2 3484:                 if ($nextCount == 1) { # **3**
1.97      bowersj2 3485:                     my $current = $nextResources->[0]->{DATA}->{$valName} || 999999999;
                   3486:                     $nextResources->[0]->{DATA}->{$valName} = min($resultingVal, $current);
                   3487:                 }
                   3488:                 
1.116     bowersj2 3489:                 if ($nextCount > 1) { # **4**
1.97      bowersj2 3490:                     foreach my $res (@{$nextResources}) {
                   3491:                         my $current = $res->{DATA}->{$valName} || 999999999;
                   3492:                         $res->{DATA}->{$valName} = min($current, $resultingVal + 1);
                   3493:                     }
                   3494:                 }
                   3495:             }
1.96      bowersj2 3496:             
1.107     bowersj2 3497:             # Assign the final val (**2**)
1.97      bowersj2 3498:             if (ref($curRes) && $direction == BACKWARD()) {
1.98      bowersj2 3499:                 my $finalDepth = min($curRes->{DATA}->{TOP_DOWN_VAL},
                   3500:                                      $curRes->{DATA}->{BOT_UP_VAL});
                   3501:                 
                   3502:                 $curRes->{DATA}->{DISPLAY_DEPTH} = $finalDepth;
                   3503:                 if ($finalDepth > $maxDepth) {$maxDepth = $finalDepth;}
1.190     bowersj2 3504:             }
1.222     bowersj2 3505: 
                   3506: 	    $curRes = $iterator->next();
1.96      bowersj2 3507:         }
                   3508:     }
1.94      bowersj2 3509: 
1.116     bowersj2 3510:     # Check: Was this only one resource, a map?
1.255     albertel 3511:     if ($resourceCount == 1 && $resource->is_sequence() && !$self->{FORCE_TOP}) { 
1.116     bowersj2 3512:         my $firstResource = $resource->map_start();
                   3513:         my $finishResource = $resource->map_finish();
1.500     raeburn  3514: 	return Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource,
                   3515: 						 $finishResource, $self->{FILTER},
                   3516: 						 $self->{ALREADY_SEEN}, 
                   3517: 						 $self->{CONDITION},
                   3518: 						 $self->{FORCE_TOP});
1.116     bowersj2 3519:     }
                   3520: 
1.98      bowersj2 3521:     # Set up some bookkeeping information.
                   3522:     $self->{CURRENT_DEPTH} = 0;
                   3523:     $self->{MAX_DEPTH} = $maxDepth;
                   3524:     $self->{STACK} = [];
                   3525:     $self->{RECURSIVE_ITERATOR_FLAG} = 0;
1.222     bowersj2 3526:     $self->{FINISHED} = 0; # When true, the iterator has finished
1.98      bowersj2 3527: 
                   3528:     for (my $i = 0; $i <= $self->{MAX_DEPTH}; $i++) {
                   3529:         push @{$self->{STACK}}, [];
                   3530:     }
                   3531: 
1.107     bowersj2 3532:     # Prime the recursion w/ the first resource **5**
1.98      bowersj2 3533:     push @{$self->{STACK}->[0]}, $self->{FIRST_RESOURCE};
                   3534:     $self->{ALREADY_SEEN}->{$self->{FIRST_RESOURCE}->{ID}} = 1;
                   3535: 
                   3536:     bless ($self);
                   3537:     return $self;
                   3538: }
                   3539: 
                   3540: sub next {
                   3541:     my $self = shift;
1.256     albertel 3542:     my $closeAllPages=shift;
1.505     raeburn  3543:     my $noblockcheck = shift;
1.222     bowersj2 3544:     if ($self->{FINISHED}) {
                   3545: 	return END_ITERATOR();
                   3546:     }
                   3547: 
1.162     bowersj2 3548:     # If we want to return the top-level map object, and haven't yet,
                   3549:     # do so.
                   3550:     if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0}) {
                   3551:         $self->{HAVE_RETURNED_0} = 1;
1.470     foxr     3552: 	my $nextTopLevel = $self->{NAV_MAP}->getById('0.0');
1.162     bowersj2 3553:         return $self->{NAV_MAP}->getById('0.0');
                   3554:     }
1.401     albertel 3555:     if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0_BEGIN_MAP}) {
                   3556: 	$self->{HAVE_RETURNED_0_BEGIN_MAP} = 1;
                   3557: 	return $self->BEGIN_MAP();
                   3558:     }
1.98      bowersj2 3559: 
                   3560:     if ($self->{RECURSIVE_ITERATOR_FLAG}) {
                   3561:         # grab the next from the recursive iterator 
1.256     albertel 3562:         my $next = $self->{RECURSIVE_ITERATOR}->next($closeAllPages);
1.98      bowersj2 3563: 
                   3564:         # is it a begin or end map? If so, update the depth
                   3565:         if ($next == BEGIN_MAP() ) { $self->{RECURSIVE_DEPTH}++; }
                   3566:         if ($next == END_MAP() ) { $self->{RECURSIVE_DEPTH}--; }
                   3567: 
                   3568:         # Are we back at depth 0? If so, stop recursing
                   3569:         if ($self->{RECURSIVE_DEPTH} == 0) {
                   3570:             $self->{RECURSIVE_ITERATOR_FLAG} = 0;
                   3571:         }
                   3572:         return $next;
                   3573:     }
                   3574: 
                   3575:     if (defined($self->{FORCE_NEXT})) {
                   3576:         my $tmp = $self->{FORCE_NEXT};
                   3577:         $self->{FORCE_NEXT} = undef;
                   3578:         return $tmp;
                   3579:     }
                   3580: 
                   3581:     # Have we not yet begun? If not, return BEGIN_MAP and
                   3582:     # remember we've started.
                   3583:     if ( !$self->{STARTED} ) { 
                   3584:         $self->{STARTED} = 1;
                   3585:         return $self->BEGIN_MAP();
                   3586:     }
                   3587: 
                   3588:     # Here's the guts of the iterator.
                   3589:     
                   3590:     # Find the next resource, if any.
                   3591:     my $found = 0;
                   3592:     my $i = $self->{MAX_DEPTH};
                   3593:     my $newDepth;
                   3594:     my $here;
                   3595:     while ( $i >= 0 && !$found ) {
1.107     bowersj2 3596:         if ( scalar(@{$self->{STACK}->[$i]}) > 0 ) { # **6**
                   3597:             $here = pop @{$self->{STACK}->[$i]}; # **7**
1.98      bowersj2 3598:             $found = 1;
                   3599:             $newDepth = $i;
                   3600:         }
                   3601:         $i--;
                   3602:     }
                   3603: 
                   3604:     # If we still didn't find anything, we're done.
                   3605:     if ( !$found ) {
                   3606:         # We need to get back down to the correct branch depth
                   3607:         if ( $self->{CURRENT_DEPTH} > 0 ) {
                   3608:             $self->{CURRENT_DEPTH}--;
                   3609:             return END_BRANCH();
                   3610:         } else {
1.222     bowersj2 3611: 	    $self->{FINISHED} = 1;
1.98      bowersj2 3612:             return END_MAP();
                   3613:         }
                   3614:     }
                   3615: 
1.104     bowersj2 3616:     # If this is not a resource, it must be an END_BRANCH marker we want
                   3617:     # to return directly.
1.107     bowersj2 3618:     if (!ref($here)) { # **8**
1.104     bowersj2 3619:         if ($here == END_BRANCH()) { # paranoia, in case of later extension
                   3620:             $self->{CURRENT_DEPTH}--;
                   3621:             return $here;
                   3622:         }
                   3623:     }
                   3624: 
                   3625:     # Otherwise, it is a resource and it's safe to store in $self->{HERE}
                   3626:     $self->{HERE} = $here;
                   3627: 
1.98      bowersj2 3628:     # Get to the right level
                   3629:     if ( $self->{CURRENT_DEPTH} > $newDepth ) {
                   3630:         push @{$self->{STACK}->[$newDepth]}, $here;
                   3631:         $self->{CURRENT_DEPTH}--;
                   3632:         return END_BRANCH();
                   3633:     }
                   3634:     if ( $self->{CURRENT_DEPTH} < $newDepth) {
                   3635:         push @{$self->{STACK}->[$newDepth]}, $here;
                   3636:         $self->{CURRENT_DEPTH}++;
                   3637:         return BEGIN_BRANCH();
                   3638:     }
                   3639: 
                   3640:     # If we made it here, we have the next resource, and we're at the
                   3641:     # right branch level. So let's examine the resource for where
                   3642:     # we can get to from here.
                   3643: 
                   3644:     # So we need to look at all the resources we can get to from here,
                   3645:     # categorize them if we haven't seen them, remember if we have a new
                   3646:     my $nextUnfiltered = $here->getNext();
1.470     foxr     3647: 
                   3648: 
1.104     bowersj2 3649:     my $maxDepthAdded = -1;
                   3650:     
1.98      bowersj2 3651:     for (@$nextUnfiltered) {
                   3652:         if (!defined($self->{ALREADY_SEEN}->{$_->{ID}})) {
1.104     bowersj2 3653:             my $depth = $_->{DATA}->{DISPLAY_DEPTH};
                   3654:             push @{$self->{STACK}->[$depth]}, $_;
1.98      bowersj2 3655:             $self->{ALREADY_SEEN}->{$_->{ID}} = 1;
1.104     bowersj2 3656:             if ($maxDepthAdded < $depth) { $maxDepthAdded = $depth; }
1.98      bowersj2 3657:         }
                   3658:     }
1.104     bowersj2 3659: 
                   3660:     # Is this the end of a branch? If so, all of the resources examined above
1.398     banghart 3661:     # led to lower levels than the one we are currently at, so we push a END_BRANCH
1.104     bowersj2 3662:     # marker onto the stack so we don't forget.
                   3663:     # Example: For the usual A(BC)(DE)F case, when the iterator goes down the
                   3664:     # BC branch and gets to C, it will see F as the only next resource, but it's
                   3665:     # one level lower. Thus, this is the end of the branch, since there are no
                   3666:     # more resources added to this level or above.
1.111     bowersj2 3667:     # We don't do this if the examined resource is the finish resource,
                   3668:     # because the condition given above is true, but the "END_MAP" will
                   3669:     # take care of things and we should already be at depth 0.
1.104     bowersj2 3670:     my $isEndOfBranch = $maxDepthAdded < $self->{CURRENT_DEPTH};
1.111     bowersj2 3671:     if ($isEndOfBranch && $here != $self->{FINISH_RESOURCE}) { # **9**
1.104     bowersj2 3672:         push @{$self->{STACK}->[$self->{CURRENT_DEPTH}]}, END_BRANCH();
                   3673:     }
                   3674: 
1.98      bowersj2 3675:     # That ends the main iterator logic. Now, do we want to recurse
                   3676:     # down this map (if this resource is a map)?
1.256     albertel 3677:     if ( ($self->{HERE}->is_sequence() || (!$closeAllPages && $self->{HERE}->is_page())) &&
1.98      bowersj2 3678:         (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) {
                   3679:         $self->{RECURSIVE_ITERATOR_FLAG} = 1;
                   3680:         my $firstResource = $self->{HERE}->map_start();
                   3681:         my $finishResource = $self->{HERE}->map_finish();
                   3682:         $self->{RECURSIVE_ITERATOR} = 
                   3683:             Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource,
                   3684:                                               $finishResource, $self->{FILTER},
1.314     albertel 3685:                                               $self->{ALREADY_SEEN},
                   3686: 					      $self->{CONDITION},
                   3687: 					      $self->{FORCE_TOP});
1.98      bowersj2 3688:     }
                   3689: 
1.116     bowersj2 3690:     # If this is a blank resource, don't actually return it.
1.117     bowersj2 3691:     # Should you ever find you need it, make sure to add an option to the code
                   3692:     #  that you can use; other things depend on this behavior.
1.505     raeburn  3693:     my $browsePriv = $self->{HERE}->browsePriv($noblockcheck);
1.138     bowersj2 3694:     if (!$self->{HERE}->src() || 
                   3695:         (!($browsePriv eq 'F') && !($browsePriv eq '2')) ) {
1.256     albertel 3696:         return $self->next($closeAllPages);
1.116     bowersj2 3697:     }
                   3698: 
1.98      bowersj2 3699:     return $self->{HERE};
                   3700: 
                   3701: }
                   3702: 
                   3703: =pod
                   3704: 
1.174     albertel 3705: The other method available on the iterator is B<getStack>, which
                   3706: returns an array populated with the current 'stack' of maps, as
                   3707: references to the resource objects. Example: This is useful when
                   3708: making the navigation map, as we need to check whether we are under a
                   3709: page map to see if we need to link directly to the resource, or to the
                   3710: page. The first elements in the array will correspond to the top of
                   3711: the stack (most inclusive map).
1.98      bowersj2 3712: 
                   3713: =cut
                   3714: 
                   3715: sub getStack {
                   3716:     my $self=shift;
                   3717: 
                   3718:     my @stack;
                   3719: 
                   3720:     $self->populateStack(\@stack);
                   3721: 
                   3722:     return \@stack;
                   3723: }
                   3724: 
                   3725: # Private method: Calls the iterators recursively to populate the stack.
                   3726: sub populateStack {
                   3727:     my $self=shift;
                   3728:     my $stack = shift;
                   3729: 
                   3730:     push @$stack, $self->{HERE} if ($self->{HERE});
                   3731: 
                   3732:     if ($self->{RECURSIVE_ITERATOR_FLAG}) {
                   3733:         $self->{RECURSIVE_ITERATOR}->populateStack($stack);
                   3734:     }
1.94      bowersj2 3735: }
                   3736: 
                   3737: 1;
                   3738: 
                   3739: package Apache::lonnavmaps::DFSiterator;
1.365     albertel 3740: use Scalar::Util qw(weaken);
1.320     albertel 3741: use Apache::lonnet;
                   3742: 
1.100     bowersj2 3743: # Not documented in the perldoc: This is a simple iterator that just walks
                   3744: #  through the nav map and presents the resources in a depth-first search
                   3745: #  fashion, ignorant of conditionals, randomized resources, etc. It presents
                   3746: #  BEGIN_MAP and END_MAP, but does not understand branches at all. It is
                   3747: #  useful for pre-processing of some kind, and is in fact used by the main
                   3748: #  iterator that way, but that's about it.
                   3749: # One could imagine merging this into the init routine of the main iterator,
1.251     www      3750: #  but this might as well be left separate, since it is possible some other
1.100     bowersj2 3751: #  use might be found for it. - Jeremy
1.94      bowersj2 3752: 
1.117     bowersj2 3753: # Unlike the main iterator, this DOES return all resources, even blank ones.
                   3754: #  The main iterator needs them to correctly preprocess the map.
                   3755: 
1.94      bowersj2 3756: sub BEGIN_MAP { return 1; }    # begining of a new map
                   3757: sub END_MAP { return 2; }      # end of the map
                   3758: sub FORWARD { return 1; }      # go forward
                   3759: sub BACKWARD { return 2; }
                   3760: 
1.100     bowersj2 3761: # Params: Nav map ref, first resource id/ref, finish resource id/ref,
                   3762: #         filter hash ref (or undef), already seen hash or undef, condition
                   3763: #         (as in main iterator), direction FORWARD or BACKWARD (undef->forward).
1.51      bowersj2 3764: sub new {
                   3765:     # magic invocation to create a class instance
                   3766:     my $proto = shift;
                   3767:     my $class = ref($proto) || $proto;
                   3768:     my $self = {};
                   3769: 
1.300     albertel 3770:     weaken($self->{NAV_MAP} = shift);
1.51      bowersj2 3771:     return undef unless ($self->{NAV_MAP});
                   3772: 
1.454     www      3773:     $self->{USERNAME} = $self->{NAV_MAP}->{USERNAME};
                   3774:     $self->{DOMAIN}   = $self->{NAV_MAP}->{DOMAIN};
                   3775: 
1.51      bowersj2 3776:     $self->{FIRST_RESOURCE} = shift || $self->{NAV_MAP}->firstResource();
                   3777:     $self->{FINISH_RESOURCE} = shift || $self->{NAV_MAP}->finishResource();
                   3778: 
                   3779:     # If the given resources are just the ID of the resource, get the
                   3780:     # objects
                   3781:     if (!ref($self->{FIRST_RESOURCE})) { $self->{FIRST_RESOURCE} = 
                   3782:              $self->{NAV_MAP}->getById($self->{FIRST_RESOURCE}); }
                   3783:     if (!ref($self->{FINISH_RESOURCE})) { $self->{FINISH_RESOURCE} = 
                   3784:              $self->{NAV_MAP}->getById($self->{FINISH_RESOURCE}); }
                   3785: 
                   3786:     $self->{FILTER} = shift;
                   3787: 
                   3788:     # A hash, used as a set, of resource already seen
                   3789:     $self->{ALREADY_SEEN} = shift;
1.140     bowersj2 3790:      if (!defined($self->{ALREADY_SEEN})) { $self->{ALREADY_SEEN} = {} };
1.63      bowersj2 3791:     $self->{CONDITION} = shift;
1.89      bowersj2 3792:     $self->{DIRECTION} = shift || FORWARD();
1.51      bowersj2 3793: 
1.100     bowersj2 3794:     # Flag: Have we started yet?
1.51      bowersj2 3795:     $self->{STARTED} = 0;
                   3796: 
                   3797:     # Should we continue calling the recursive iterator, if any?
                   3798:     $self->{RECURSIVE_ITERATOR_FLAG} = 0;
                   3799:     # The recursive iterator, if any
                   3800:     $self->{RECURSIVE_ITERATOR} = undef;
                   3801:     # Are we recursing on a map, or a branch?
                   3802:     $self->{RECURSIVE_MAP} = 1; # we'll manually unset this when recursing on branches
                   3803:     # And the count of how deep it is, so that this iterator can keep track of
                   3804:     # when to pick back up again.
                   3805:     $self->{RECURSIVE_DEPTH} = 0;
                   3806: 
                   3807:     # For keeping track of our branches, we maintain our own stack
1.100     bowersj2 3808:     $self->{STACK} = [];
1.51      bowersj2 3809: 
                   3810:     # Start with the first resource
1.89      bowersj2 3811:     if ($self->{DIRECTION} == FORWARD) {
1.100     bowersj2 3812:         push @{$self->{STACK}}, $self->{FIRST_RESOURCE};
1.89      bowersj2 3813:     } else {
1.100     bowersj2 3814:         push @{$self->{STACK}}, $self->{FINISH_RESOURCE};
1.89      bowersj2 3815:     }
1.51      bowersj2 3816: 
                   3817:     bless($self);
                   3818:     return $self;
                   3819: }
                   3820: 
                   3821: sub next {
                   3822:     my $self = shift;
                   3823:     
                   3824:     # Are we using a recursive iterator? If so, pull from that and
                   3825:     # watch the depth; we want to resume our level at the correct time.
1.98      bowersj2 3826:     if ($self->{RECURSIVE_ITERATOR_FLAG}) {
1.51      bowersj2 3827:         # grab the next from the recursive iterator
                   3828:         my $next = $self->{RECURSIVE_ITERATOR}->next();
                   3829:         
                   3830:         # is it a begin or end map? Update depth if so
                   3831:         if ($next == BEGIN_MAP() ) { $self->{RECURSIVE_DEPTH}++; }
                   3832:         if ($next == END_MAP() ) { $self->{RECURSIVE_DEPTH}--; }
                   3833: 
                   3834:         # Are we back at depth 0? If so, stop recursing.
                   3835:         if ($self->{RECURSIVE_DEPTH} == 0) {
                   3836:             $self->{RECURSIVE_ITERATOR_FLAG} = 0;
                   3837:         }
                   3838:         
                   3839:         return $next;
                   3840:     }
                   3841: 
                   3842:     # Is there a current resource to grab? If not, then return
1.100     bowersj2 3843:     # END_MAP, which will end the iterator.
                   3844:     if (scalar(@{$self->{STACK}}) == 0) {
                   3845:         return $self->END_MAP();
1.51      bowersj2 3846:     }
                   3847: 
                   3848:     # Have we not yet begun? If not, return BEGIN_MAP and 
                   3849:     # remember that we've started.
                   3850:     if ( !$self->{STARTED} ) {
                   3851:         $self->{STARTED} = 1;
                   3852:         return $self->BEGIN_MAP;
                   3853:     }
                   3854: 
                   3855:     # Get the next resource in the branch
1.100     bowersj2 3856:     $self->{HERE} = pop @{$self->{STACK}};
1.52      bowersj2 3857: 
1.100     bowersj2 3858:     # remember that we've seen this, so we don't return it again later
1.51      bowersj2 3859:     $self->{ALREADY_SEEN}->{$self->{HERE}->{ID}} = 1;
                   3860:     
                   3861:     # Get the next possible resources
1.90      bowersj2 3862:     my $nextUnfiltered;
1.89      bowersj2 3863:     if ($self->{DIRECTION} == FORWARD()) {
1.90      bowersj2 3864:         $nextUnfiltered = $self->{HERE}->getNext();
1.89      bowersj2 3865:     } else {
1.90      bowersj2 3866:         $nextUnfiltered = $self->{HERE}->getPrevious();
1.89      bowersj2 3867:     }
1.51      bowersj2 3868:     my $next = [];
                   3869: 
                   3870:     # filter the next possibilities to remove things we've 
1.100     bowersj2 3871:     # already seen.
1.394     raeburn  3872:     foreach my $item (@$nextUnfiltered) {
                   3873:         if (!defined($self->{ALREADY_SEEN}->{$item->{ID}})) {
                   3874:             push @$next, $item;
1.52      bowersj2 3875:         }
1.51      bowersj2 3876:     }
                   3877: 
                   3878:     while (@$next) {
1.100     bowersj2 3879:         # copy the next possibilities over to the stack
                   3880:         push @{$self->{STACK}}, shift @$next;
1.51      bowersj2 3881:     }
                   3882: 
                   3883:     # If this is a map and we want to recurse down it... (not filtered out)
1.70      bowersj2 3884:     if ($self->{HERE}->is_map() && 
1.63      bowersj2 3885:          (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) { 
1.51      bowersj2 3886:         $self->{RECURSIVE_ITERATOR_FLAG} = 1;
                   3887:         my $firstResource = $self->{HERE}->map_start();
                   3888:         my $finishResource = $self->{HERE}->map_finish();
                   3889: 
                   3890:         $self->{RECURSIVE_ITERATOR} =
1.94      bowersj2 3891:           Apache::lonnavmaps::DFSiterator->new ($self->{NAV_MAP}, $firstResource, 
1.63      bowersj2 3892:                      $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN},
1.91      bowersj2 3893:                                              $self->{CONDITION}, $self->{DIRECTION});
1.51      bowersj2 3894:     }
                   3895: 
                   3896:     return $self->{HERE};
1.190     bowersj2 3897: }
                   3898: 
                   3899: # Identical to the full iterator methods of the same name. Hate to copy/paste
                   3900: # but I also hate to "inherit" either iterator from the other.
                   3901: 
                   3902: sub getStack {
                   3903:     my $self=shift;
                   3904: 
                   3905:     my @stack;
                   3906: 
                   3907:     $self->populateStack(\@stack);
                   3908: 
                   3909:     return \@stack;
                   3910: }
                   3911: 
                   3912: # Private method: Calls the iterators recursively to populate the stack.
                   3913: sub populateStack {
                   3914:     my $self=shift;
                   3915:     my $stack = shift;
                   3916: 
                   3917:     push @$stack, $self->{HERE} if ($self->{HERE});
                   3918: 
                   3919:     if ($self->{RECURSIVE_ITERATOR_FLAG}) {
                   3920:         $self->{RECURSIVE_ITERATOR}->populateStack($stack);
                   3921:     }
1.51      bowersj2 3922: }
                   3923: 
1.1       www      3924: 1;
1.2       www      3925: 
1.51      bowersj2 3926: package Apache::lonnavmaps::resource;
1.365     albertel 3927: use Scalar::Util qw(weaken);
1.51      bowersj2 3928: use Apache::lonnet;
                   3929: 
                   3930: =pod
                   3931: 
1.217     bowersj2 3932: =head1 Object: resource 
1.51      bowersj2 3933: 
1.217     bowersj2 3934: X<resource, navmap object>
1.174     albertel 3935: A resource object encapsulates a resource in a resource map, allowing
                   3936: easy manipulation of the resource, querying the properties of the
                   3937: resource (including user properties), and represents a reference that
                   3938: can be used as the canonical representation of the resource by
                   3939: lonnavmap clients like renderers.
                   3940: 
                   3941: A resource only makes sense in the context of a navmap, as some of the
                   3942: data is stored in the navmap object.
                   3943: 
                   3944: You will probably never need to instantiate this object directly. Use
                   3945: Apache::lonnavmaps::navmap, and use the "start" method to obtain the
                   3946: starting resource.
1.51      bowersj2 3947: 
1.188     bowersj2 3948: Resource objects respect the parameter_hiddenparts, which suppresses 
                   3949: various parts according to the wishes of the map author. As of this
                   3950: writing, there is no way to override this parameter, and suppressed
                   3951: parts will never be returned, nor will their response types or ids be
                   3952: stored.
                   3953: 
1.217     bowersj2 3954: =head2 Overview
1.51      bowersj2 3955: 
1.217     bowersj2 3956: A B<Resource> is the most granular type of object in LON-CAPA that can
                   3957: be included in a course. It can either be a particular resource, like
                   3958: an HTML page, external resource, problem, etc., or it can be a
                   3959: container sequence, such as a "page" or a "map".
                   3960: 
                   3961: To see a sequence from the user's point of view, please see the
                   3962: B<Creating a Course: Maps and Sequences> chapter of the Author's
                   3963: Manual.
                   3964: 
                   3965: A Resource Object, once obtained from a navmap object via a B<getBy*>
                   3966: method of the navmap, or from an iterator, allows you to query
                   3967: information about that resource.
                   3968: 
                   3969: Generally, you do not ever want to create a resource object yourself,
                   3970: so creation has been left undocumented. Always retrieve resources
                   3971: from navmap objects.
                   3972: 
                   3973: =head3 Identifying Resources
                   3974: 
                   3975: X<big hash>Every resource is identified by a Resource ID in the big hash that is
                   3976: unique to that resource for a given course. X<resource ID, in big hash>
                   3977: The Resource ID has the form #.#, where the first number is the same
                   3978: for every resource in a map, and the second is unique. For instance,
                   3979: for a course laid out like this:
                   3980: 
                   3981:  * Problem 1
                   3982:  * Map
                   3983:    * Resource 2
                   3984:    * Resource 3
                   3985: 
                   3986: C<Problem 1> and C<Map> will share a first number, and C<Resource 2>
                   3987: C<Resource 3> will share a first number. The second number may end up
                   3988: re-used between the two groups.
                   3989: 
                   3990: The resource ID is only used in the big hash, but can be used in the
                   3991: context of a course to identify a resource easily. (For instance, the
                   3992: printing system uses it to record which resources from a sequence you 
                   3993: wish to print.)
                   3994: 
                   3995: X<symb> X<resource, symb>
                   3996: All resources also have B<symb>s, which uniquely identify a resource
                   3997: in a course. Many internal LON-CAPA functions expect a symb. A symb
                   3998: carries along with it the URL of the resource, and the map it appears
1.398     banghart 3999: in. Symbs are much larger than resource IDs.
1.51      bowersj2 4000: 
                   4001: =cut
                   4002: 
                   4003: sub new {
                   4004:     # magic invocation to create a class instance
                   4005:     my $proto = shift;
                   4006:     my $class = ref($proto) || $proto;
                   4007:     my $self = {};
                   4008: 
1.300     albertel 4009:     weaken($self->{NAV_MAP} = shift);
1.51      bowersj2 4010:     $self->{ID} = shift;
                   4011: 
1.454     www      4012:     $self->{USERNAME} = $self->{NAV_MAP}->{USERNAME};
                   4013:     $self->{DOMAIN}   = $self->{NAV_MAP}->{DOMAIN};
                   4014: 
1.51      bowersj2 4015:     # Store this new resource in the parent nav map's cache.
                   4016:     $self->{NAV_MAP}->{RESOURCE_CACHE}->{$self->{ID}} = $self;
1.66      bowersj2 4017:     $self->{RESOURCE_ERROR} = 0;
1.51      bowersj2 4018: 
1.479     raeburn  4019:     $self->{DUEDATE_CACHE} = undef;
                   4020: 
1.51      bowersj2 4021:     # A hash that can be used by two-pass algorithms to store data
                   4022:     # about this resource in. Not used by the resource object
                   4023:     # directly.
                   4024:     $self->{DATA} = {};
1.508     damieng  4025:     
1.51      bowersj2 4026:     bless($self);
                   4027:     
1.508     damieng  4028:     # This is a speed optimization, to avoid calling symb() too often.
                   4029:     $self->{SYMB} = $self->symb();
1.515     raeburn  4030: 
1.51      bowersj2 4031:     return $self;
                   4032: }
                   4033: 
1.70      bowersj2 4034: # private function: simplify the NAV_HASH lookups we keep doing
                   4035: # pass the name, and to automatically append my ID, pass a true val on the
                   4036: # second param
                   4037: sub navHash {
                   4038:     my $self = shift;
                   4039:     my $param = shift;
                   4040:     my $id = shift;
1.441     raeburn  4041:     my $arg = $param . ($id?$self->{ID}:"");
1.451     raeburn  4042:     if (ref($self) && ref($self->{NAV_MAP}) && defined($arg)) {
1.441     raeburn  4043:         return $self->{NAV_MAP}->navhash($arg);
                   4044:     }
                   4045:     return;
1.70      bowersj2 4046: }
                   4047: 
1.51      bowersj2 4048: =pod
                   4049: 
1.217     bowersj2 4050: =head2 Methods
1.70      bowersj2 4051: 
1.217     bowersj2 4052: Once you have a resource object, here's what you can do with it:
                   4053: 
                   4054: =head3 Attribute Retrieval
                   4055: 
                   4056: Every resource has certain attributes that can be retrieved and used:
1.70      bowersj2 4057: 
                   4058: =over 4
                   4059: 
1.217     bowersj2 4060: =item * B<ID>: Every resource has an ID that is unique for that
                   4061:     resource in the course it is in. The ID is actually in the hash
                   4062:     representing the resource, so for a resource object $res, obtain
                   4063:     it via C<$res->{ID}).
                   4064: 
1.174     albertel 4065: =item * B<compTitle>:
                   4066: 
                   4067: Returns a "composite title", that is equal to $res->title() if the
                   4068: resource has a title, and is otherwise the last part of the URL (e.g.,
                   4069: "problem.problem").
                   4070: 
                   4071: =item * B<ext>:
                   4072: 
                   4073: Returns true if the resource is external.
                   4074: 
                   4075: =item * B<kind>:
                   4076: 
                   4077: Returns the kind of the resource from the compiled nav map.
                   4078: 
                   4079: =item * B<randomout>:
1.106     bowersj2 4080: 
1.174     albertel 4081: Returns true if this resource was chosen to NOT be shown to the user
                   4082: by the random map selection feature. In other words, this is usually
                   4083: false.
1.70      bowersj2 4084: 
1.174     albertel 4085: =item * B<randompick>:
1.70      bowersj2 4086: 
1.400     albertel 4087: Returns the number of randomly picked items for a map if the randompick
                   4088: feature is being used on the map. 
                   4089: 
                   4090: =item * B<randomorder>:
                   4091: 
                   4092: Returns true for a map if the randomorder feature is being used on the
                   4093: map.
1.70      bowersj2 4094: 
1.174     albertel 4095: =item * B<src>:
1.51      bowersj2 4096: 
1.174     albertel 4097: Returns the source for the resource.
1.70      bowersj2 4098: 
1.174     albertel 4099: =item * B<symb>:
1.70      bowersj2 4100: 
1.174     albertel 4101: Returns the symb for the resource.
1.70      bowersj2 4102: 
1.174     albertel 4103: =item * B<title>:
1.70      bowersj2 4104: 
1.174     albertel 4105: Returns the title of the resource.
                   4106: 
1.51      bowersj2 4107: =back
                   4108: 
                   4109: =cut
                   4110: 
                   4111: # These info functions can be used directly, as they don't return
                   4112: # resource information.
1.85      bowersj2 4113: sub comesfrom { my $self=shift; return $self->navHash("comesfrom_", 1); }
1.303     albertel 4114: sub encrypted { my $self=shift; return $self->navHash("encrypted_", 1); }
1.70      bowersj2 4115: sub ext { my $self=shift; return $self->navHash("ext_", 1) eq 'true:'; }
1.85      bowersj2 4116: sub from { my $self=shift; return $self->navHash("from_", 1); }
1.217     bowersj2 4117: # considered private and undocumented
1.51      bowersj2 4118: sub goesto { my $self=shift; return $self->navHash("goesto_", 1); }
                   4119: sub kind { my $self=shift; return $self->navHash("kind_", 1); }
1.68      bowersj2 4120: sub randomout { my $self=shift; return $self->navHash("randomout_", 1); }
                   4121: sub randompick { 
                   4122:     my $self = shift;
1.410     raeburn  4123:     my $randompick = $self->parmval('randompick');
                   4124:     return $randompick;
1.68      bowersj2 4125: }
1.400     albertel 4126: sub randomorder { 
                   4127:     my $self = shift;
1.410     raeburn  4128:     my $randomorder = $self->parmval('randomorder');
                   4129:     return ($randomorder =~ /^yes$/i);
1.400     albertel 4130: }
1.303     albertel 4131: sub link {
                   4132:     my $self=shift;
                   4133:     if ($self->encrypted()) { return &Apache::lonenc::encrypted($self->src); }
                   4134:     return $self->src;
                   4135: }
1.51      bowersj2 4136: sub src { 
                   4137:     my $self=shift;
                   4138:     return $self->navHash("src_", 1);
                   4139: }
1.303     albertel 4140: sub shown_symb {
                   4141:     my $self=shift;
1.508     damieng  4142:     if ($self->encrypted()) {return &Apache::lonenc::encrypted($self->{SYMB});}
                   4143:     return $self->{SYMB};
1.303     albertel 4144: }
1.328     www      4145: sub id {
                   4146:     my $self=shift;
                   4147:     return $self->{ID};
                   4148: }
                   4149: sub enclosing_map_src {
                   4150:     my $self=shift;
                   4151:     (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/;
                   4152:     return $self->navHash('map_id_'.$first);
                   4153: }
1.51      bowersj2 4154: sub symb {
                   4155:     my $self=shift;
                   4156:     (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/;
                   4157:     my $symbSrc = &Apache::lonnet::declutter($self->src());
1.223     albertel 4158:     my $symb = &Apache::lonnet::declutter($self->navHash('map_id_'.$first)) 
1.51      bowersj2 4159:         . '___' . $second . '___' . $symbSrc;
1.223     albertel 4160:     return &Apache::lonnet::symbclean($symb);
1.51      bowersj2 4161: }
1.321     raeburn  4162: sub wrap_symb {
                   4163:     my $self = shift;
1.508     damieng  4164:     return $self->{NAV_MAP}->wrap_symb($self->{SYMB});
1.321     raeburn  4165: }
1.213     bowersj2 4166: sub title { 
                   4167:     my $self=shift; 
                   4168:     if ($self->{ID} eq '0.0') {
                   4169: 	# If this is the top-level map, return the title of the course
                   4170: 	# since this map can not be titled otherwise.
1.320     albertel 4171: 	return $env{'course.'.$env{'request.course.id'}.'.description'};
1.213     bowersj2 4172:     }
                   4173:     return $self->navHash("title_", 1); }
1.217     bowersj2 4174: # considered private and undocumented
1.70      bowersj2 4175: sub to { my $self=shift; return $self->navHash("to_", 1); }
1.301     albertel 4176: sub condition {
                   4177:     my $self=shift;
                   4178:     my $undercond=$self->navHash("undercond_", 1);
                   4179:     if (!defined($undercond)) { return 1; };
                   4180:     my $condid=$self->navHash("condid_$undercond");
                   4181:     if (!defined($condid)) { return 1; };
                   4182:     my $condition=&Apache::lonnet::directcondval($condid);
                   4183:     return $condition;
                   4184: }
1.342     albertel 4185: sub condval {
                   4186:     my $self=shift;
1.359     albertel 4187:     my ($pathname,$filename) = 
                   4188: 	&Apache::lonnet::split_uri_for_cond($self->src());
1.342     albertel 4189: 
                   4190:     my $match=($env{'acc.res.'.$env{'request.course.id'}.'.'.$pathname}=~
                   4191: 	       /\&\Q$filename\E\:([\d\|]+)\&/);
                   4192:     if ($match) {
                   4193: 	return &Apache::lonnet::condval($1);
                   4194:     }
                   4195:     return 0;
                   4196: }
1.106     bowersj2 4197: sub compTitle {
                   4198:     my $self = shift;
                   4199:     my $title = $self->title();
1.176     www      4200:     $title=~s/\&colon\;/\:/gs;
1.106     bowersj2 4201:     if (!$title) {
                   4202:         $title = $self->src();
                   4203:         $title = substr($title, rindex($title, '/') + 1);
                   4204:     }
                   4205:     return $title;
                   4206: }
1.399     albertel 4207: 
1.70      bowersj2 4208: =pod
                   4209: 
                   4210: B<Predicate Testing the Resource>
                   4211: 
                   4212: These methods are shortcuts to deciding if a given resource has a given property.
                   4213: 
                   4214: =over 4
                   4215: 
1.174     albertel 4216: =item * B<is_map>:
                   4217: 
                   4218: Returns true if the resource is a map type.
                   4219: 
                   4220: =item * B<is_problem>:
1.70      bowersj2 4221: 
1.174     albertel 4222: Returns true if the resource is a problem type, false
                   4223: otherwise. (Looks at the extension on the src field; might need more
                   4224: to work correctly.)
1.70      bowersj2 4225: 
1.174     albertel 4226: =item * B<is_page>:
1.70      bowersj2 4227: 
1.174     albertel 4228: Returns true if the resource is a page.
                   4229: 
                   4230: =item * B<is_sequence>:
                   4231: 
                   4232: Returns true if the resource is a sequence.
1.70      bowersj2 4233: 
                   4234: =back
                   4235: 
                   4236: =cut
                   4237: 
1.256     albertel 4238: sub hasResource {
                   4239:    my $self = shift;
                   4240:    return $self->{NAV_MAP}->hasResource(@_);
                   4241: }
                   4242: 
                   4243: sub retrieveResources {
                   4244:    my $self = shift;
                   4245:    return $self->{NAV_MAP}->retrieveResources(@_);
                   4246: }
1.70      bowersj2 4247: 
1.369     albertel 4248: sub is_exam {
                   4249:     my ($self,$part) = @_;
1.410     raeburn  4250:     my $type = $self->parmval('type',$part);
                   4251:     if ($type eq 'exam') {
1.369     albertel 4252:         return 1;
                   4253:     }
                   4254:     if ($self->src() =~ /\.(exam)$/) {
                   4255:         return 1;
                   4256:     }
                   4257:     return 0;
                   4258: }
1.70      bowersj2 4259: sub is_html {
1.51      bowersj2 4260:     my $self=shift;
                   4261:     my $src = $self->src();
1.70      bowersj2 4262:     return ($src =~ /html$/);
1.51      bowersj2 4263: }
1.70      bowersj2 4264: sub is_map { my $self=shift; return defined($self->navHash("is_map_", 1)); }
                   4265: sub is_page {
1.51      bowersj2 4266:     my $self=shift;
                   4267:     my $src = $self->src();
1.205     bowersj2 4268:     return $self->navHash("is_map_", 1) && 
                   4269: 	$self->navHash("map_type_" . $self->map_pc()) eq 'page';
1.51      bowersj2 4270: }
1.361     albertel 4271: sub is_practice {
                   4272:     my $self=shift;
                   4273:     my ($part) = @_;
1.410     raeburn  4274:     my $type = $self->parmval('type',$part);
                   4275:     if ($type eq 'practice') {
1.361     albertel 4276:         return 1;
                   4277:     }
                   4278:     return 0;
                   4279: }
1.70      bowersj2 4280: sub is_problem {
1.51      bowersj2 4281:     my $self=shift;
                   4282:     my $src = $self->src();
1.466     www      4283:     if ($src =~ /$LONCAPA::assess_re/) {
1.361     albertel 4284: 	return !($self->is_practice());
                   4285:     }
                   4286:     return 0;
1.256     albertel 4287: }
1.443     foxr     4288: #
                   4289: #  The has below is the set of status that are considered 'incomplete'
                   4290: #
                   4291: my %incomplete_hash = 
                   4292: (
                   4293:  TRIES_LEFT()     => 1,
                   4294:  OPEN()           => 1,
                   4295:  ATTEMPTED()      => 1
                   4296: 
                   4297:  );
                   4298: #
                   4299: #  Return tru if a problem is incomplete... for now incomplete means that
                   4300: #  any part of the problem is incomplete. 
                   4301: #  Note that if the resources is not a problem, 0 is returned.
                   4302: #
                   4303: sub is_incomplete {
                   4304:     my $self = shift;
                   4305:     if ($self->is_problem()) {
                   4306: 	foreach my $part (@{$self->parts()}) {
                   4307: 	    if (exists($incomplete_hash{$self->status($part)})) {
                   4308: 		return 1;
                   4309: 	    }
                   4310: 	}
                   4311:     }
                   4312:     return 0;
                   4313:        
                   4314: }
1.413     www      4315: sub is_raw_problem {
                   4316:     my $self=shift;
                   4317:     my $src = $self->src();
1.466     www      4318:     if ($src =~ /$LONCAPA::assess_re/) {
1.413     www      4319:         return 1;
                   4320:     }
                   4321:     return 0;
                   4322: }
                   4323: 
1.256     albertel 4324: sub contains_problem {
                   4325:     my $self=shift;
                   4326:     if ($self->is_page()) {
                   4327: 	my $hasProblem=$self->hasResource($self,sub { $_[0]->is_problem() },1);
                   4328: 	return $hasProblem;
                   4329:     }
                   4330:     return 0;
1.51      bowersj2 4331: }
1.401     albertel 4332: sub map_contains_problem {
                   4333:     my $self=shift;
                   4334:     if ($self->is_map()) {
                   4335: 	my $has_problem=
                   4336: 	    $self->hasResource($self,sub { $_[0]->is_problem() },1);
                   4337: 	return $has_problem;
                   4338:     }
                   4339:     return 0;
                   4340: }
1.70      bowersj2 4341: sub is_sequence {
1.51      bowersj2 4342:     my $self=shift;
1.205     bowersj2 4343:     return $self->navHash("is_map_", 1) && 
1.426     droeschl 4344:     $self->navHash("map_type_" . $self->map_pc()) eq 'sequence';
1.51      bowersj2 4345: }
1.261     matthew  4346: sub is_survey {
                   4347:     my $self = shift();
                   4348:     my $part = shift();
1.410     raeburn  4349:     my $type = $self->parmval('type',$part);
1.444     raeburn  4350:     if (($type eq 'survey') || ($type eq 'surveycred')) {
1.261     matthew  4351:         return 1;
                   4352:     }
1.263     albertel 4353:     if ($self->src() =~ /\.(survey)$/) {
1.261     matthew  4354:         return 1;
                   4355:     }
                   4356:     return 0;
                   4357: }
1.444     raeburn  4358: sub is_anonsurvey {
                   4359:     my $self = shift();
                   4360:     my $part = shift();
                   4361:     my $type = $self->parmval('type',$part);
                   4362:     if (($type eq 'anonsurvey') || ($type eq 'anonsurveycred')) {
                   4363:         return 1;
                   4364:     }
                   4365:     return 0;
                   4366: }
1.360     albertel 4367: sub is_task {
                   4368:     my $self=shift;
                   4369:     my $src = $self->src();
                   4370:     return ($src =~ /\.(task)$/)
                   4371: }
1.51      bowersj2 4372: 
1.266     raeburn  4373: sub is_empty_sequence {
                   4374:     my $self=shift;
                   4375:     my $src = $self->src();
                   4376:     return !$self->is_page() && $self->navHash("is_map_", 1) && !$self->navHash("map_type_" . $self->map_pc());
                   4377: }
                   4378: 
1.101     bowersj2 4379: # Private method: Shells out to the parmval in the nav map, handler parts.
1.51      bowersj2 4380: sub parmval {
                   4381:     my $self = shift;
                   4382:     my $what = shift;
1.185     bowersj2 4383:     my $part = shift;
                   4384:     if (!defined($part)) { 
                   4385:         $part = '0'; 
                   4386:     }
1.508     damieng  4387:     return $self->{NAV_MAP}->parmval($part.'.'.$what, $self->{SYMB});
1.51      bowersj2 4388: }
                   4389: 
1.70      bowersj2 4390: =pod
                   4391: 
                   4392: B<Map Methods>
                   4393: 
1.174     albertel 4394: These methods are useful for getting information about the map
                   4395: properties of the resource, if the resource is a map (B<is_map>).
1.70      bowersj2 4396: 
                   4397: =over 4
                   4398: 
1.174     albertel 4399: =item * B<map_finish>:
                   4400: 
                   4401: Returns a reference to a resource object corresponding to the finish
                   4402: resource of the map.
1.70      bowersj2 4403: 
1.174     albertel 4404: =item * B<map_pc>:
1.70      bowersj2 4405: 
1.174     albertel 4406: Returns the pc value of the map, which is the first number that
                   4407: appears in the resource ID of the resources in the map, and is the
                   4408: number that appears around the middle of the symbs of the resources in
                   4409: that map.
1.70      bowersj2 4410: 
1.174     albertel 4411: =item * B<map_start>:
                   4412: 
                   4413: Returns a reference to a resource object corresponding to the start
                   4414: resource of the map.
                   4415: 
                   4416: =item * B<map_type>:
                   4417: 
                   4418: Returns a string with the type of the map in it.
1.70      bowersj2 4419: 
1.497     raeburn  4420: =item * B<map_hierarchy>:
1.448     raeburn  4421: 
                   4422: Returns a string with a comma-separated ordered list of map_pc IDs
                   4423: for the hierarchy of maps containing a map, with the top level
                   4424: map first, then descending to deeper levels, with the enclosing map last.
                   4425: 
1.70      bowersj2 4426: =back
1.51      bowersj2 4427: 
1.70      bowersj2 4428: =cut
1.51      bowersj2 4429: 
                   4430: sub map_finish {
                   4431:     my $self = shift;
                   4432:     my $src = $self->src();
1.381     albertel 4433:     $src = &Apache::lonnet::clutter($src);
1.51      bowersj2 4434:     my $res = $self->navHash("map_finish_$src", 0);
                   4435:     $res = $self->{NAV_MAP}->getById($res);
                   4436:     return $res;
                   4437: }
1.70      bowersj2 4438: sub map_pc {
                   4439:     my $self = shift;
1.381     albertel 4440:     my $src = $self->src();
1.70      bowersj2 4441:     return $self->navHash("map_pc_$src", 0);
                   4442: }
1.51      bowersj2 4443: sub map_start {
                   4444:     my $self = shift;
                   4445:     my $src = $self->src();
1.381     albertel 4446:     $src = &Apache::lonnet::clutter($src);
1.51      bowersj2 4447:     my $res = $self->navHash("map_start_$src", 0);
                   4448:     $res = $self->{NAV_MAP}->getById($res);
                   4449:     return $res;
                   4450: }
                   4451: sub map_type {
                   4452:     my $self = shift;
                   4453:     my $pc = $self->map_pc();
                   4454:     return $self->navHash("map_type_$pc", 0);
                   4455: }
1.448     raeburn  4456: sub map_hierarchy {
                   4457:     my $self = shift;
                   4458:     my $pc = $self->map_pc();
                   4459:     return $self->navHash("map_hierarchy_$pc", 0);
                   4460: }
1.51      bowersj2 4461: 
                   4462: #####
                   4463: # Property queries
                   4464: #####
                   4465: 
                   4466: # These functions will be responsible for returning the CORRECT
                   4467: # VALUE for the parameter, no matter what. So while they may look
1.398     banghart 4468: # like direct calls to parmval, they can be more than that.
1.51      bowersj2 4469: # So, for instance, the duedate function should use the "duedatetype"
1.398     banghart 4470: # information, rather than the resource object user.
1.51      bowersj2 4471: 
                   4472: =pod
                   4473: 
                   4474: =head2 Resource Parameters
                   4475: 
1.174     albertel 4476: In order to use the resource parameters correctly, the nav map must
                   4477: have been instantiated with genCourseAndUserOptions set to true, so
                   4478: the courseopt and useropt is read correctly. Then, you can call these
                   4479: functions to get the relevant parameters for the resource. Each
                   4480: function defaults to part "0", but can be directed to another part by
                   4481: passing the part as the parameter.
                   4482: 
                   4483: These methods are responsible for getting the parameter correct, not
                   4484: merely reflecting the contents of the GDBM hashes. As we move towards
                   4485: dates relative to other dates, these methods should be updated to
                   4486: reflect that. (Then, anybody using these methods will not have to update
                   4487: their code.)
                   4488: 
                   4489: =over 4
                   4490: 
1.485     foxr     4491: 
1.459     foxr     4492: =item * B<printable>
                   4493: 
                   4494: returns true if the current date is such that the 
                   4495: specified resource part is printable.
                   4496: 
1.485     foxr     4497: 
1.459     foxr     4498: =item * B<resprintable>
                   4499: 
                   4500: Returns true if all parts in the resource are printable making the
                   4501: entire resource printable.
                   4502: 
                   4503: =item * B<acc>
1.174     albertel 4504: 
                   4505: Get the Client IP/Name Access Control information.
1.51      bowersj2 4506: 
1.174     albertel 4507: =item * B<answerdate>:
1.51      bowersj2 4508: 
1.174     albertel 4509: Get the answer-reveal date for the problem.
                   4510: 
1.211     bowersj2 4511: =item * B<awarded>: 
                   4512: 
                   4513: Gets the awarded value for the problem part. Requires genUserData set to
                   4514: true when the navmap object was created.
                   4515: 
1.174     albertel 4516: =item * B<duedate>:
                   4517: 
                   4518: Get the due date for the problem.
                   4519: 
                   4520: =item * B<tries>:
                   4521: 
                   4522: Get the number of tries the student has used on the problem.
                   4523: 
                   4524: =item * B<maxtries>:
                   4525: 
                   4526: Get the number of max tries allowed.
                   4527: 
                   4528: =item * B<opendate>:
1.51      bowersj2 4529: 
1.174     albertel 4530: Get the open date for the problem.
1.51      bowersj2 4531: 
1.174     albertel 4532: =item * B<sig>:
1.51      bowersj2 4533: 
1.174     albertel 4534: Get the significant figures setting.
1.51      bowersj2 4535: 
1.174     albertel 4536: =item * B<tol>:
1.51      bowersj2 4537: 
1.174     albertel 4538: Get the tolerance for the problem.
1.51      bowersj2 4539: 
1.174     albertel 4540: =item * B<tries>:
1.51      bowersj2 4541: 
1.174     albertel 4542: Get the number of tries the user has already used on the problem.
1.51      bowersj2 4543: 
1.174     albertel 4544: =item * B<type>:
1.51      bowersj2 4545: 
1.174     albertel 4546: Get the question type for the problem.
1.70      bowersj2 4547: 
1.174     albertel 4548: =item * B<weight>:
1.51      bowersj2 4549: 
1.174     albertel 4550: Get the weight for the problem.
1.51      bowersj2 4551: 
                   4552: =back
                   4553: 
                   4554: =cut
                   4555: 
1.485     foxr     4556: 
                   4557: 
                   4558: 
1.459     foxr     4559: sub printable {
                   4560: 
                   4561:     my ($self, $part) = @_;
                   4562: 
                   4563:     #  The following cases apply:
1.487     foxr     4564:     #  - If a start date is not set, it is replaced by the open date.
                   4565:     #  - Ditto for start/open replaced by content open.
                   4566:     #  - If neither start nor printdates are set the part is printable.
1.459     foxr     4567:     #  - Start date set but no end date: Printable if now >= start date.
                   4568:     #  - End date set but no start date: Printable if now <= end date.
                   4569:     #  - both defined: printable if start <= now <= end
                   4570:     #
1.487     foxr     4571: 
                   4572:     # Get the print open/close dates for the resource.
                   4573: 
                   4574:     my $start = $self->parmval("printstartdate", $part);
                   4575:     my $end   = $self->parmval("printenddate", $part);
                   4576: 
                   4577:     if (!$start) {
                   4578: 	$start = $self->parmval("opendate", $part);
                   4579:     }
                   4580:     if (!$start) {
                   4581: 	$start = $self->parmval("contentopen", $part);
                   4582:     }
                   4583: 
                   4584: 
1.459     foxr     4585:     my $now  = time();
                   4586: 
1.487     foxr     4587: 
1.459     foxr     4588:     my $startok = 1;
                   4589:     my $endok   = 1;
                   4590: 
1.460     foxr     4591:     if ((defined $start) && ($start ne '')) {
1.459     foxr     4592: 	$startok = $start <= $now;
                   4593:     }
1.460     foxr     4594:     if ((defined $end) && ($end != '')) {
1.459     foxr     4595: 	$endok = $end >= $now;
                   4596:     }
                   4597:     return $startok && $endok;
                   4598: }
                   4599: 
                   4600: sub resprintable {
                   4601:     my $self = shift;
                   4602: 
                   4603:     # get parts...or realize there are no parts.
                   4604: 
1.460     foxr     4605:     my $partsref = $self->parts();
                   4606:     my @parts    = @$partsref;
                   4607: 
1.489     raeburn  4608:     if (!@parts) {
1.459     foxr     4609: 	return $self->printable(0);
                   4610:     } else {
1.460     foxr     4611: 	foreach my $part  (@parts) {
                   4612: 	    if (!$self->printable($part)) { 
                   4613: 		return 0; 
                   4614: 	    }
1.459     foxr     4615: 	}
                   4616: 	return 1;
                   4617:     }
                   4618: }
                   4619: 
1.51      bowersj2 4620: sub acc {
                   4621:     (my $self, my $part) = @_;
1.410     raeburn  4622:     my $acc = $self->parmval("acc", $part);
                   4623:     return $acc;
1.51      bowersj2 4624: }
                   4625: sub answerdate {
                   4626:     (my $self, my $part) = @_;
                   4627:     # Handle intervals
1.410     raeburn  4628:     my $answerdatetype = $self->parmval("answerdate.type", $part);
                   4629:     my $answerdate = $self->parmval("answerdate", $part);
                   4630:     my $duedate = $self->parmval("duedate", $part);
                   4631:     if ($answerdatetype eq 'date_interval') {
                   4632:         $answerdate = $duedate + $answerdate; 
1.51      bowersj2 4633:     }
1.410     raeburn  4634:     return $answerdate;
1.106     bowersj2 4635: }
1.211     bowersj2 4636: sub awarded { 
                   4637:     my $self = shift; my $part = shift;
1.221     bowersj2 4638:     $self->{NAV_MAP}->get_user_data();
1.211     bowersj2 4639:     if (!defined($part)) { $part = '0'; }
1.508     damieng  4640:     return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.awarded'};
1.211     bowersj2 4641: }
1.432     raeburn  4642: sub taskversion {
                   4643:     my $self = shift; my $part = shift;
                   4644:     $self->{NAV_MAP}->get_user_data();
                   4645:     if (!defined($part)) { $part = '0'; }
1.508     damieng  4646:     return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.version'};
1.432     raeburn  4647: }
                   4648: sub taskstatus {
                   4649:     my $self = shift; my $part = shift;
                   4650:     $self->{NAV_MAP}->get_user_data();
                   4651:     if (!defined($part)) { $part = '0'; }
1.508     damieng  4652:     return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$self->taskversion($part).'.'.$part.'.status'};
1.432     raeburn  4653: }
                   4654: sub solved {
                   4655:     my $self = shift; my $part = shift;
                   4656:     $self->{NAV_MAP}->get_user_data();
                   4657:     if (!defined($part)) { $part = '0'; }
1.508     damieng  4658:     return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.solved'};
1.432     raeburn  4659: }
                   4660: sub checkedin {
                   4661:     my $self = shift; my $part = shift;
                   4662:     $self->{NAV_MAP}->get_user_data();
                   4663:     if (!defined($part)) { $part = '0'; }
                   4664:     if ($self->is_task()) {
                   4665:         my $version = $self->taskversion($part);
1.508     damieng  4666:         return ($self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$version .'.'.$part.'.checkedin'},$self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$version .'.'.$part.'.checkedin.slot'});
1.432     raeburn  4667:     } else {
1.508     damieng  4668:         return ($self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.checkedin'},$self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.checkedin.slot'});
1.432     raeburn  4669:     }
                   4670: }
1.382     albertel 4671: # this should work exactly like the copy in lonhomework.pm
1.459     foxr     4672: # Why is there a copy in lonhomework?  Why not centralized?
                   4673: #
                   4674: #  TODO: Centralize duedate.
                   4675: #
                   4676: 
1.51      bowersj2 4677: sub duedate {
                   4678:     (my $self, my $part) = @_;
1.479     raeburn  4679:     if (defined ($self->{DUEDATE_CACHE}->{$part})) {
                   4680:         return $self->{DUEDATE_CACHE}->{$part};
                   4681:     }
1.382     albertel 4682:     my $date;
1.406     albertel 4683:     my @interval=$self->parmval("interval", $part);
1.382     albertel 4684:     my $due_date=$self->parmval("duedate", $part);
1.520   ! raeburn  4685:     if ($interval[0] =~ /^(\d+)/) {
        !          4686:         my $timelimit = $1;
        !          4687:         my $first_access=&Apache::lonnet::get_first_access($interval[1],
        !          4688:                                                            $self->{SYMB});
1.382     albertel 4689: 	if (defined($first_access)) {
1.520   ! raeburn  4690:             my $interval = $first_access+$timelimit;
1.404     albertel 4691: 	    $date = (!$due_date || $interval < $due_date) ? $interval 
                   4692:                                                           : $due_date;
1.382     albertel 4693: 	} else {
                   4694: 	    $date = $due_date;
                   4695: 	}
                   4696:     } else {
                   4697: 	$date = $due_date;
1.260     albertel 4698:     }
1.479     raeburn  4699:     $self->{DUEDATE_CACHE}->{$part} = $date;
1.382     albertel 4700:     return $date;
1.51      bowersj2 4701: }
1.334     albertel 4702: sub handgrade {
                   4703:     (my $self, my $part) = @_;
1.399     albertel 4704:     my @response_ids = $self->responseIds($part);
                   4705:     if (@response_ids) {
                   4706: 	foreach my $response_id (@response_ids) {
1.410     raeburn  4707:             my $handgrade = $self->parmval("handgrade",$part.'_'.$response_id);
                   4708: 	    if (lc($handgrade) eq 'yes') {
1.399     albertel 4709: 		return 'yes';
                   4710: 	    }
                   4711: 	}
                   4712:     }
1.410     raeburn  4713:     my $handgrade = $self->parmval("handgrade", $part);
                   4714:     return $handgrade;
1.334     albertel 4715: }
1.51      bowersj2 4716: sub maxtries {
                   4717:     (my $self, my $part) = @_;
1.410     raeburn  4718:     my $maxtries = $self->parmval("maxtries", $part);
                   4719:     return $maxtries;
1.51      bowersj2 4720: }
                   4721: sub opendate {
                   4722:     (my $self, my $part) = @_;
1.410     raeburn  4723:     my $opendatetype = $self->parmval("opendate.type", $part);
                   4724:     my $opendate = $self->parmval("opendate", $part); 
                   4725:     if ($opendatetype eq 'date_interval') {
                   4726:         my $duedate = $self->duedate($part);
                   4727:         $opendate = $duedate - $opendate; 
1.51      bowersj2 4728:     }
1.410     raeburn  4729:     return $opendate;
1.51      bowersj2 4730: }
1.185     bowersj2 4731: sub problemstatus {
                   4732:     (my $self, my $part) = @_;
1.410     raeburn  4733:     my $problemstatus = $self->parmval("problemstatus", $part);
                   4734:     return lc($problemstatus);
1.185     bowersj2 4735: }
1.51      bowersj2 4736: sub sig {
                   4737:     (my $self, my $part) = @_;
1.410     raeburn  4738:     my $sig = $self->parmval("sig", $part);
                   4739:     return $sig;
1.51      bowersj2 4740: }
                   4741: sub tol {
                   4742:     (my $self, my $part) = @_;
1.410     raeburn  4743:     my $tol = $self->parmval("tol", $part);
                   4744:     return $tol;
1.51      bowersj2 4745: }
1.410     raeburn  4746: sub tries {
1.108     bowersj2 4747:     my $self = shift; 
                   4748:     my $tries = $self->queryRestoreHash('tries', shift);
                   4749:     if (!defined($tries)) { return '0';}
1.51      bowersj2 4750:     return $tries;
                   4751: }
1.70      bowersj2 4752: sub type {
                   4753:     (my $self, my $part) = @_;
1.410     raeburn  4754:     my $type = $self->parmval("type", $part);
                   4755:     return $type;
1.70      bowersj2 4756: }
1.109     bowersj2 4757: sub weight { 
                   4758:     my $self = shift; my $part = shift;
1.211     bowersj2 4759:     if (!defined($part)) { $part = '0'; }
1.409     raeburn  4760:     my $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight',
1.508     damieng  4761:                                 $self->{SYMB}, $self->{DOMAIN},
1.453     www      4762:                                 $self->{USERNAME},
1.409     raeburn  4763:                                 $env{'request.course.sec'});
                   4764:     return $weight;
1.274     matthew  4765: }
                   4766: sub part_display {
                   4767:     my $self= shift(); my $partID = shift();
                   4768:     if (! defined($partID)) { $partID = '0'; }
                   4769:     my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display',
1.514     raeburn  4770:                                      $self->{SYMB});
1.274     matthew  4771:     if (! defined($display) || $display eq '') {
                   4772:         $display = $partID;
                   4773:     }
                   4774:     return $display;
1.70      bowersj2 4775: }
1.432     raeburn  4776: sub slot_control {
                   4777:     my $self=shift(); my $part = shift();
                   4778:     if (!defined($part)) { $part = '0'; }
                   4779:     my $useslots = $self->parmval("useslots", $part);
                   4780:     my $availablestudent = $self->parmval("availablestudent", $part);
                   4781:     my $available = $self->parmval("available", $part); 
                   4782:     return ($useslots,$availablestudent,$available);
                   4783: }
1.51      bowersj2 4784: 
                   4785: # Multiple things need this
                   4786: sub getReturnHash {
                   4787:     my $self = shift;
                   4788:     
                   4789:     if (!defined($self->{RETURN_HASH})) {
1.508     damieng  4790:         my %tmpHash  = &Apache::lonnet::restore($self->{SYMB},undef,$self->{DOMAIN},$self->{USERNAME});
1.51      bowersj2 4791:         $self->{RETURN_HASH} = \%tmpHash;
                   4792:     }
                   4793: }       
                   4794: 
                   4795: ######
                   4796: # Status queries
                   4797: ######
                   4798: 
                   4799: # These methods query the status of problems.
                   4800: 
                   4801: # If we need to count parts, this function determines the number of
                   4802: # parts from the metadata. When called, it returns a reference to a list
                   4803: # of strings corresponding to the parts. (Thus, using it in a scalar context
                   4804: # tells you how many parts you have in the problem:
                   4805: # $partcount = scalar($resource->countParts());
                   4806: # Don't use $self->{PARTS} directly because you don't know if it's been
                   4807: # computed yet.
                   4808: 
                   4809: =pod
                   4810: 
                   4811: =head2 Resource misc
                   4812: 
                   4813: Misc. functions for the resource.
                   4814: 
                   4815: =over 4
                   4816: 
1.174     albertel 4817: =item * B<hasDiscussion>:
1.59      bowersj2 4818: 
1.174     albertel 4819: Returns a false value if there has been discussion since the user last
                   4820: logged in, true if there has. Always returns false if the discussion
                   4821: data was not extracted when the nav map was constructed.
                   4822: 
1.366     albertel 4823: =item * B<last_post_time>:
                   4824: 
                   4825: Returns a false value if there hasn't been discussion otherwise returns
                   4826: unix timestamp of last time a discussion posting (or edit) was made.
                   4827: 
1.393     raeburn  4828: =item * B<discussion_info>:
1.366     albertel 4829: 
1.393     raeburn  4830: optional argument is a filter (currently can be 'unread');
                   4831: returns in scalar context the count of the number of discussion postings.
1.366     albertel 4832: 
                   4833: returns in list context both the count of postings and a hash ref
1.393     raeburn  4834: containing information about the postings (subject, id, timestamp) in a hash.
                   4835: 
                   4836: Default is to return counts for all postings.  However if called with a second argument set to 'unread', will return information about only unread postings.
1.366     albertel 4837: 
1.174     albertel 4838: =item * B<getFeedback>:
                   4839: 
                   4840: Gets the feedback for the resource and returns the raw feedback string
                   4841: for the resource, or the null string if there is no feedback or the
                   4842: email data was not extracted when the nav map was constructed. Usually
                   4843: used like this:
1.59      bowersj2 4844: 
1.394     raeburn  4845:  for my $url (split(/\,/, $res->getFeedback())) {
                   4846:     my $link = &escape($url);
1.59      bowersj2 4847:     ...
                   4848: 
                   4849: and use the link as appropriate.
                   4850: 
                   4851: =cut
                   4852: 
                   4853: sub hasDiscussion {
                   4854:     my $self = shift;
1.508     damieng  4855:     return $self->{NAV_MAP}->hasDiscussion($self->{SYMB});
1.59      bowersj2 4856: }
                   4857: 
1.366     albertel 4858: sub last_post_time {
                   4859:     my $self = shift;
1.508     damieng  4860:     return $self->{NAV_MAP}->last_post_time($self->{SYMB});
1.366     albertel 4861: }
                   4862: 
1.393     raeburn  4863: sub discussion_info {
                   4864:     my ($self,$filter) = @_;
1.508     damieng  4865:     return $self->{NAV_MAP}->discussion_info($self->{SYMB},$filter);
1.366     albertel 4866: }
                   4867: 
1.59      bowersj2 4868: sub getFeedback {
                   4869:     my $self = shift;
1.124     bowersj2 4870:     my $source = $self->src();
1.508     damieng  4871:     my $symb = $self->{SYMB};
1.125     bowersj2 4872:     if ($source =~ /^\/res\//) { $source = substr $source, 5; }
1.395     raeburn  4873:     return $self->{NAV_MAP}->getFeedback($symb,$source);
1.124     bowersj2 4874: }
                   4875: 
                   4876: sub getErrors {
                   4877:     my $self = shift;
                   4878:     my $source = $self->src();
1.508     damieng  4879:     my $symb = $self->{SYMB};
1.124     bowersj2 4880:     if ($source =~ /^\/res\//) { $source = substr $source, 5; }
1.395     raeburn  4881:     return $self->{NAV_MAP}->getErrors($symb,$source);
1.59      bowersj2 4882: }
                   4883: 
                   4884: =pod
                   4885: 
1.174     albertel 4886: =item * B<parts>():
1.51      bowersj2 4887: 
1.174     albertel 4888: Returns a list reference containing sorted strings corresponding to
1.193     bowersj2 4889: each part of the problem. Single part problems have only a part '0'.
                   4890: Multipart problems do not return their part '0', since they typically
                   4891: do not really matter. 
1.174     albertel 4892: 
                   4893: =item * B<countParts>():
                   4894: 
                   4895: Returns the number of parts of the problem a student can answer. Thus,
                   4896: for single part problems, returns 1. For multipart, it returns the
1.193     bowersj2 4897: number of parts in the problem, not including psuedo-part 0. 
1.51      bowersj2 4898: 
1.290     matthew  4899: =item * B<countResponses>():
                   4900: 
                   4901: Returns the total number of responses in the problem a student can answer.
                   4902: 
                   4903: =item * B<responseTypes>():
                   4904: 
                   4905: Returns a hash whose keys are the response types.  The values are the number 
                   4906: of times each response type is used.  This is for the I<entire> problem, not 
                   4907: just a single part.
                   4908: 
1.192     bowersj2 4909: =item * B<multipart>():
                   4910: 
1.193     bowersj2 4911: Returns true if the problem is multipart, false otherwise. Use this instead
                   4912: of countParts if all you want is multipart/not multipart.
1.192     bowersj2 4913: 
1.187     bowersj2 4914: =item * B<responseType>($part):
                   4915: 
                   4916: Returns the response type of the part, without the word "response" on the
                   4917: end. Example return values: 'string', 'essay', 'numeric', etc.
                   4918: 
1.193     bowersj2 4919: =item * B<responseIds>($part):
1.187     bowersj2 4920: 
1.193     bowersj2 4921: Retreives the response IDs for the given part as an array reference containing
                   4922: strings naming the response IDs. This may be empty.
1.187     bowersj2 4923: 
1.51      bowersj2 4924: =back
                   4925: 
                   4926: =cut
                   4927: 
                   4928: sub parts {
                   4929:     my $self = shift;
                   4930: 
1.192     bowersj2 4931:     if ($self->ext) { return []; }
1.67      bowersj2 4932: 
1.51      bowersj2 4933:     $self->extractParts();
                   4934:     return $self->{PARTS};
                   4935: }
                   4936: 
                   4937: sub countParts {
                   4938:     my $self = shift;
                   4939:     
                   4940:     my $parts = $self->parts();
1.191     bowersj2 4941: 
                   4942:     # If I left this here, then it's not necessary.
                   4943:     #my $delta = 0;
                   4944:     #for my $part (@$parts) {
                   4945:     #    if ($part eq '0') { $delta--; }
                   4946:     #}
1.66      bowersj2 4947: 
                   4948:     if ($self->{RESOURCE_ERROR}) {
                   4949:         return 0;
                   4950:     }
                   4951: 
1.191     bowersj2 4952:     return scalar(@{$parts}); # + $delta;
1.192     bowersj2 4953: }
                   4954: 
1.290     matthew  4955: sub countResponses {
                   4956:     my $self = shift;
                   4957:     my $count;
1.293     matthew  4958:     foreach my $part (@{$self->parts()}) {
                   4959:         $count+= scalar($self->responseIds($part));
1.290     matthew  4960:     }
                   4961:     return $count;
                   4962: }
                   4963: 
                   4964: sub responseTypes {
                   4965:     my $self = shift;
1.291     albertel 4966:     my %responses;
1.368     raeburn  4967:     foreach my $part (@{$self->parts()}) {
1.290     matthew  4968:         foreach my $responsetype ($self->responseType($part)) {
1.291     albertel 4969:             $responses{$responsetype}++ if (defined($responsetype));
1.290     matthew  4970:         }
                   4971:     }
1.291     albertel 4972:     return %responses;
1.290     matthew  4973: }
                   4974: 
1.192     bowersj2 4975: sub multipart {
                   4976:     my $self = shift;
                   4977:     return $self->countParts() > 1;
1.51      bowersj2 4978: }
                   4979: 
1.225     bowersj2 4980: sub singlepart {
                   4981:     my $self = shift;
                   4982:     return $self->countParts() == 1;
                   4983: }
                   4984: 
1.187     bowersj2 4985: sub responseType {
1.184     bowersj2 4986:     my $self = shift;
                   4987:     my $part = shift;
                   4988: 
                   4989:     $self->extractParts();
1.235     albertel 4990:     if (defined($self->{RESPONSE_TYPES}->{$part})) {
                   4991: 	return @{$self->{RESPONSE_TYPES}->{$part}};
                   4992:     } else {
                   4993: 	return undef;
                   4994:     }
1.187     bowersj2 4995: }
                   4996: 
1.193     bowersj2 4997: sub responseIds {
1.187     bowersj2 4998:     my $self = shift;
                   4999:     my $part = shift;
                   5000: 
                   5001:     $self->extractParts();
1.235     albertel 5002:     if (defined($self->{RESPONSE_IDS}->{$part})) {
                   5003: 	return @{$self->{RESPONSE_IDS}->{$part}};
                   5004:     } else {
                   5005: 	return undef;
                   5006:     }
1.184     bowersj2 5007: }
                   5008: 
                   5009: # Private function: Extracts the parts information, both part names and
1.187     bowersj2 5010: # part types, and saves it. 
1.51      bowersj2 5011: sub extractParts { 
                   5012:     my $self = shift;
                   5013:     
1.153     bowersj2 5014:     return if (defined($self->{PARTS}));
1.67      bowersj2 5015:     return if ($self->ext);
1.51      bowersj2 5016: 
                   5017:     $self->{PARTS} = [];
                   5018: 
1.181     bowersj2 5019:     my %parts;
                   5020: 
1.82      bowersj2 5021:     # Retrieve part count, if this is a problem
                   5022:     if ($self->is_problem()) {
1.240     albertel 5023: 	my $partorder = &Apache::lonnet::metadata($self->src(), 'partorder');
1.152     matthew  5024:         my $metadata = &Apache::lonnet::metadata($self->src(), 'packages');
1.181     bowersj2 5025: 
1.239     bowersj2 5026: 	if ($partorder) {
                   5027: 	    my @parts;
                   5028: 	    for my $part (split (/,/,$partorder)) {
1.508     damieng  5029: 		if (!Apache::loncommon::check_if_partid_hidden($part, $self->{SYMB})) {
1.239     bowersj2 5030: 		    push @parts, $part;
1.241     albertel 5031: 		    $parts{$part} = 1;
1.239     bowersj2 5032: 		}
                   5033: 	    }
                   5034: 	    $self->{PARTS} = \@parts;
                   5035: 	} else {
                   5036: 	    if (!$metadata) {
                   5037: 		$self->{RESOURCE_ERROR} = 1;
                   5038: 		$self->{PARTS} = [];
                   5039: 		$self->{PART_TYPE} = {};
                   5040: 		return;
                   5041: 	    }
1.394     raeburn  5042: 	    foreach my $entry (split(/\,/,$metadata)) {
                   5043: 		if ($entry =~ /^(?:part|Task)_(.*)$/) {
1.239     bowersj2 5044: 		    my $part = $1;
                   5045: 		    # This floods the logs if it blows up
                   5046: 		    if (defined($parts{$part})) {
1.508     damieng  5047: 			&Apache::lonnet::logthis("$part multiply defined in metadata for " . $self->{SYMB});
1.241     albertel 5048: 		    }
1.239     bowersj2 5049: 		    
                   5050: 		    # check to see if part is turned off.
                   5051: 		    
1.508     damieng  5052: 		    if (!Apache::loncommon::check_if_partid_hidden($part, $self->{SYMB})) {
1.239     bowersj2 5053: 			$parts{$part} = 1;
                   5054: 		    }
                   5055: 		}
                   5056: 	    }
1.504     raeburn  5057: 	    my @sortedParts = sort(keys(%parts));
1.239     bowersj2 5058: 	    $self->{PARTS} = \@sortedParts;
1.51      bowersj2 5059:         }
1.82      bowersj2 5060:         
1.187     bowersj2 5061: 
1.284     matthew  5062:         # These hashes probably do not need names that end with "Hash"....
1.187     bowersj2 5063:         my %responseIdHash;
                   5064:         my %responseTypeHash;
                   5065: 
1.189     bowersj2 5066: 
                   5067:         # Init the responseIdHash
1.394     raeburn  5068:         foreach my $part (@{$self->{PARTS}}) {
                   5069:             $responseIdHash{$part} = [];
1.189     bowersj2 5070:         }
                   5071: 
1.187     bowersj2 5072:         # Now, the unfortunate thing about this is that parts, part name, and
1.239     bowersj2 5073:         # response id are delimited by underscores, but both the part
1.187     bowersj2 5074:         # name and response id can themselves have underscores in them.
                   5075:         # So we have to use our knowlege of part names to figure out 
                   5076:         # where the part names begin and end, and even then, it is possible
                   5077:         # to construct ambiguous situations.
1.504     raeburn  5078:         foreach my $data (split(/,/, $metadata)) {
1.379     albertel 5079:             if ($data =~ /^([a-zA-Z]+)response_(.*)/
                   5080: 		|| $data =~ /^(Task)_(.*)/) {
1.187     bowersj2 5081:                 my $responseType = $1;
                   5082:                 my $partStuff = $2;
                   5083:                 my $partIdSoFar = '';
1.504     raeburn  5084:                 my @partChunks = split(/_/, $partStuff);
1.187     bowersj2 5085:                 my $i = 0;
                   5086:                 for ($i = 0; $i < scalar(@partChunks); $i++) {
                   5087:                     if ($partIdSoFar) { $partIdSoFar .= '_'; }
                   5088:                     $partIdSoFar .= $partChunks[$i];
                   5089:                     if ($parts{$partIdSoFar}) {
                   5090:                         my @otherChunks = @partChunks[$i+1..$#partChunks];
                   5091:                         my $responseId = join('_', @otherChunks);
1.379     albertel 5092: 			if ($self->is_task()) {
                   5093: 			    push(@{$responseIdHash{$partIdSoFar}},
                   5094: 				 $partIdSoFar);
                   5095: 			} else {
                   5096: 			    push(@{$responseIdHash{$partIdSoFar}},
                   5097: 				 $responseId);
                   5098: 			}
                   5099:                         push(@{$responseTypeHash{$partIdSoFar}},
                   5100: 			     $responseType);
1.187     bowersj2 5101:                     }
                   5102:                 }
                   5103:             }
                   5104:         }
1.264     albertel 5105: 	my $resorder = &Apache::lonnet::metadata($self->src(),'responseorder');
1.284     matthew  5106:         #
                   5107:         # Reorder the arrays in the %responseIdHash and %responseTypeHash
1.264     albertel 5108: 	if ($resorder) {
                   5109: 	    my @resorder=split(/,/,$resorder);
                   5110: 	    foreach my $part (keys(%responseIdHash)) {
1.286     albertel 5111: 		my $i=0;
                   5112: 		my %resids = map { ($_,$i++) } @{ $responseIdHash{$part} };
1.264     albertel 5113: 		my @neworder;
                   5114: 		foreach my $possibleid (@resorder) {
                   5115: 		    if (exists($resids{$possibleid})) {
1.286     albertel 5116: 			push(@neworder,$resids{$possibleid});
1.264     albertel 5117: 		    }
                   5118: 		}
1.286     albertel 5119: 		my @ids;
                   5120: 		my @type;
                   5121: 		foreach my $element (@neworder) {
                   5122: 		    push (@ids,$responseIdHash{$part}->[$element]);
                   5123: 		    push (@type,$responseTypeHash{$part}->[$element]);
                   5124: 		}
                   5125: 		$responseIdHash{$part}=\@ids;
                   5126: 		$responseTypeHash{$part}=\@type;
1.264     albertel 5127: 	    }
                   5128: 	}
1.187     bowersj2 5129:         $self->{RESPONSE_IDS} = \%responseIdHash;
                   5130:         $self->{RESPONSE_TYPES} = \%responseTypeHash;
1.154     bowersj2 5131:     }
                   5132: 
1.51      bowersj2 5133:     return;
                   5134: }
                   5135: 
                   5136: =pod
                   5137: 
                   5138: =head2 Resource Status
                   5139: 
1.174     albertel 5140: Problem resources have status information, reflecting their various
                   5141: dates and completion statuses.
1.51      bowersj2 5142: 
1.174     albertel 5143: There are two aspects to the status: the date-related information and
                   5144: the completion information.
1.51      bowersj2 5145: 
1.174     albertel 5146: Idiomatic usage of these two methods would probably look something
                   5147: like
1.51      bowersj2 5148: 
1.394     raeburn  5149:  foreach my $part ($resource->parts()) {
                   5150:     my $dateStatus = $resource->getDateStatus($part);
                   5151:     my $completionStatus = $resource->getCompletionStatus($part);
1.51      bowersj2 5152: 
1.70      bowersj2 5153:     or
                   5154: 
1.394     raeburn  5155:     my $status = $resource->status($part);
1.70      bowersj2 5156: 
1.51      bowersj2 5157:     ... use it here ...
                   5158:  }
                   5159: 
1.174     albertel 5160: Which you use depends on exactly what you are looking for. The
                   5161: status() function has been optimized for the nav maps display and may
                   5162: not precisely match what you need elsewhere.
1.101     bowersj2 5163: 
1.174     albertel 5164: The symbolic constants shown below can be accessed through the
                   5165: resource object: C<$res->OPEN>.
1.101     bowersj2 5166: 
1.51      bowersj2 5167: =over 4
                   5168: 
1.174     albertel 5169: =item * B<getDateStatus>($part):
                   5170: 
                   5171: ($part defaults to 0). A convenience function that returns a symbolic
                   5172: constant telling you about the date status of the part. The possible
                   5173: return values are:
1.51      bowersj2 5174: 
                   5175: =back
                   5176: 
                   5177: B<Date Codes>
                   5178: 
                   5179: =over 4
                   5180: 
1.174     albertel 5181: =item * B<OPEN_LATER>:
                   5182: 
                   5183: The problem will be opened later.
                   5184: 
                   5185: =item * B<OPEN>:
                   5186: 
                   5187: Open and not yet due.
                   5188: 
1.51      bowersj2 5189: 
1.174     albertel 5190: =item * B<PAST_DUE_ANSWER_LATER>:
1.51      bowersj2 5191: 
1.174     albertel 5192: The due date has passed, but the answer date has not yet arrived.
1.59      bowersj2 5193: 
1.174     albertel 5194: =item * B<PAST_DUE_NO_ANSWER>:
1.54      bowersj2 5195: 
1.174     albertel 5196: The due date has passed and there is no answer opening date set.
1.51      bowersj2 5197: 
1.174     albertel 5198: =item * B<ANSWER_OPEN>:
1.51      bowersj2 5199: 
1.174     albertel 5200: The answer date is here.
                   5201: 
                   5202: =item * B<NETWORK_FAILURE>:
                   5203: 
                   5204: The information is unknown due to network failure.
1.51      bowersj2 5205: 
                   5206: =back
                   5207: 
                   5208: =cut
                   5209: 
                   5210: # Apparently the compiler optimizes these into constants automatically
1.54      bowersj2 5211: sub OPEN_LATER             { return 0; }
                   5212: sub OPEN                   { return 1; }
                   5213: sub PAST_DUE_NO_ANSWER     { return 2; }
                   5214: sub PAST_DUE_ANSWER_LATER  { return 3; }
                   5215: sub ANSWER_OPEN            { return 4; }
1.432     raeburn  5216: sub NOTHING_SET            { return 5; }
1.54      bowersj2 5217: sub NETWORK_FAILURE        { return 100; }
                   5218: 
                   5219: # getDateStatus gets the date status for a given problem part. 
                   5220: # Because answer date, due date, and open date are fully independent
                   5221: # (i.e., it is perfectly possible to *only* have an answer date), 
                   5222: # we have to completely cover the 3x3 maxtrix of (answer, due, open) x
                   5223: # (past, future, none given). This function handles this with a decision
                   5224: # tree. Read the comments to follow the decision tree.
1.51      bowersj2 5225: 
                   5226: sub getDateStatus {
                   5227:     my $self = shift;
                   5228:     my $part = shift;
                   5229:     $part = "0" if (!defined($part));
1.54      bowersj2 5230: 
                   5231:     # Always return network failure if there was one.
1.51      bowersj2 5232:     return $self->NETWORK_FAILURE if ($self->{NAV_MAP}->{NETWORK_FAILURE});
                   5233: 
                   5234:     my $now = time();
                   5235: 
1.53      bowersj2 5236:     my $open = $self->opendate($part);
                   5237:     my $due = $self->duedate($part);
                   5238:     my $answer = $self->answerdate($part);
                   5239: 
                   5240:     if (!$open && !$due && !$answer) {
                   5241:         # no data on the problem at all
                   5242:         # should this be the same as "open later"? think multipart.
                   5243:         return $self->NOTHING_SET;
                   5244:     }
1.59      bowersj2 5245:     if (!$open || $now < $open) {return $self->OPEN_LATER}
                   5246:     if (!$due || $now < $due) {return $self->OPEN}
                   5247:     if ($answer && $now < $answer) {return $self->PAST_DUE_ANSWER_LATER}
                   5248:     if ($answer) { return $self->ANSWER_OPEN; }
1.54      bowersj2 5249:     return PAST_DUE_NO_ANSWER;
1.51      bowersj2 5250: }
                   5251: 
                   5252: =pod
                   5253: 
                   5254: B<>
                   5255: 
                   5256: =over 4
                   5257: 
1.174     albertel 5258: =item * B<getCompletionStatus>($part):
1.51      bowersj2 5259: 
1.174     albertel 5260: ($part defaults to 0.) A convenience function that returns a symbolic
                   5261: constant telling you about the completion status of the part, with the
                   5262: following possible results:
                   5263: 
                   5264: =back
1.51      bowersj2 5265: 
                   5266: B<Completion Codes>
                   5267: 
                   5268: =over 4
                   5269: 
1.174     albertel 5270: =item * B<NOT_ATTEMPTED>:
                   5271: 
                   5272: Has not been attempted at all.
                   5273: 
                   5274: =item * B<INCORRECT>:
                   5275: 
                   5276: Attempted, but wrong by student.
1.51      bowersj2 5277: 
1.174     albertel 5278: =item * B<INCORRECT_BY_OVERRIDE>:
1.51      bowersj2 5279: 
1.174     albertel 5280: Attempted, but wrong by instructor override.
1.51      bowersj2 5281: 
1.174     albertel 5282: =item * B<CORRECT>:
1.51      bowersj2 5283: 
1.174     albertel 5284: Correct or correct by instructor.
1.51      bowersj2 5285: 
1.174     albertel 5286: =item * B<CORRECT_BY_OVERRIDE>:
1.51      bowersj2 5287: 
1.174     albertel 5288: Correct by instructor override.
1.51      bowersj2 5289: 
1.174     albertel 5290: =item * B<EXCUSED>:
                   5291: 
                   5292: Excused. Not yet implemented.
                   5293: 
                   5294: =item * B<NETWORK_FAILURE>:
                   5295: 
                   5296: Information not available due to network failure.
                   5297: 
                   5298: =item * B<ATTEMPTED>:
                   5299: 
                   5300: Attempted, and not yet graded.
1.69      bowersj2 5301: 
1.450     raeburn  5302: =item * B<CREDIT_ATTEMPTED>:
                   5303: 
                   5304: Attempted, and credit received for attempt (survey and anonymous survey only).
                   5305: 
1.51      bowersj2 5306: =back
                   5307: 
                   5308: =cut
                   5309: 
1.53      bowersj2 5310: sub NOT_ATTEMPTED         { return 10; }
                   5311: sub INCORRECT             { return 11; }
                   5312: sub INCORRECT_BY_OVERRIDE { return 12; }
                   5313: sub CORRECT               { return 13; }
                   5314: sub CORRECT_BY_OVERRIDE   { return 14; }
                   5315: sub EXCUSED               { return 15; }
1.69      bowersj2 5316: sub ATTEMPTED             { return 16; }
1.450     raeburn  5317: sub CREDIT_ATTEMPTED      { return 17; }
1.51      bowersj2 5318: 
                   5319: sub getCompletionStatus {
                   5320:     my $self = shift;
1.332     albertel 5321:     my $part = shift;
1.51      bowersj2 5322:     return $self->NETWORK_FAILURE if ($self->{NAV_MAP}->{NETWORK_FAILURE});
                   5323: 
1.332     albertel 5324:     my $status = $self->queryRestoreHash('solved', $part);
1.51      bowersj2 5325: 
1.251     www      5326:     # Left as separate if statements in case we ever do more with this
1.51      bowersj2 5327:     if ($status eq 'correct_by_student') {return $self->CORRECT;}
1.288     albertel 5328:     if ($status eq 'correct_by_scantron') {return $self->CORRECT;}
1.332     albertel 5329:     if ($status eq 'correct_by_override') {
                   5330: 	return $self->CORRECT_BY_OVERRIDE;
                   5331:     }
1.51      bowersj2 5332:     if ($status eq 'incorrect_attempted') {return $self->INCORRECT; }
                   5333:     if ($status eq 'incorrect_by_override') {return $self->INCORRECT_BY_OVERRIDE; }
                   5334:     if ($status eq 'excused') {return $self->EXCUSED; }
1.69      bowersj2 5335:     if ($status eq 'ungraded_attempted') {return $self->ATTEMPTED; }
1.450     raeburn  5336:     if ($status eq 'credit_attempted') {
                   5337:         if ($self->is_anonsurvey($part) || $self->is_survey($part)) {
                   5338:             return $self->CREDIT_ATTEMPTED;
                   5339:         } else {
                   5340:             return $self->ATTEMPTED;
                   5341:         }
                   5342:     }
1.51      bowersj2 5343:     return $self->NOT_ATTEMPTED;
1.108     bowersj2 5344: }
                   5345: 
                   5346: sub queryRestoreHash {
                   5347:     my $self = shift;
                   5348:     my $hashentry = shift;
                   5349:     my $part = shift;
1.185     bowersj2 5350:     $part = "0" if (!defined($part) || $part eq '');
1.108     bowersj2 5351:     return $self->NETWORK_FAILURE if ($self->{NAV_MAP}->{NETWORK_FAILURE});
                   5352: 
                   5353:     $self->getReturnHash();
                   5354: 
                   5355:     return $self->{RETURN_HASH}->{'resource.'.$part.'.'.$hashentry};
1.51      bowersj2 5356: }
                   5357: 
                   5358: =pod
                   5359: 
                   5360: B<Composite Status>
                   5361: 
1.174     albertel 5362: Along with directly returning the date or completion status, the
                   5363: resource object includes a convenience function B<status>() that will
                   5364: combine the two status tidbits into one composite status that can
1.191     bowersj2 5365: represent the status of the resource as a whole. This method represents
                   5366: the concept of the thing we want to display to the user on the nav maps
                   5367: screen, which is a combination of completion and open status. The precise logic is
1.174     albertel 5368: documented in the comments of the status method. The following results
                   5369: may be returned, all available as methods on the resource object
1.185     bowersj2 5370: ($res->NETWORK_FAILURE): In addition to the return values that match
                   5371: the date or completion status, this function can return "ANSWER_SUBMITTED"
                   5372: if that problemstatus parameter value is set to No, suppressing the
                   5373: incorrect/correct feedback.
1.51      bowersj2 5374: 
                   5375: =over 4
                   5376: 
1.174     albertel 5377: =item * B<NETWORK_FAILURE>:
                   5378: 
                   5379: The network has failed and the information is not available.
1.51      bowersj2 5380: 
1.174     albertel 5381: =item * B<NOTHING_SET>:
1.53      bowersj2 5382: 
1.174     albertel 5383: No dates have been set for this problem (part) at all. (Because only
                   5384: certain parts of a multi-part problem may be assigned, this can not be
                   5385: collapsed into "open later", as we do not know a given part will EVER
                   5386: be opened. For single part, this is the same as "OPEN_LATER".)
1.51      bowersj2 5387: 
1.174     albertel 5388: =item * B<CORRECT>:
1.51      bowersj2 5389: 
1.174     albertel 5390: For any reason at all, the part is considered correct.
1.54      bowersj2 5391: 
1.174     albertel 5392: =item * B<EXCUSED>:
1.51      bowersj2 5393: 
1.174     albertel 5394: For any reason at all, the problem is excused.
1.51      bowersj2 5395: 
1.174     albertel 5396: =item * B<PAST_DUE_NO_ANSWER>:
1.51      bowersj2 5397: 
1.174     albertel 5398: The problem is past due, not considered correct, and no answer date is
                   5399: set.
1.51      bowersj2 5400: 
1.174     albertel 5401: =item * B<PAST_DUE_ANSWER_LATER>:
1.51      bowersj2 5402: 
1.174     albertel 5403: The problem is past due, not considered correct, and an answer date in
                   5404: the future is set.
1.51      bowersj2 5405: 
1.174     albertel 5406: =item * B<ANSWER_OPEN>:
                   5407: 
                   5408: The problem is past due, not correct, and the answer is now available.
                   5409: 
                   5410: =item * B<OPEN_LATER>:
                   5411: 
                   5412: The problem is not yet open.
                   5413: 
                   5414: =item * B<TRIES_LEFT>:
                   5415: 
                   5416: The problem is open, has been tried, is not correct, but there are
                   5417: tries left.
                   5418: 
                   5419: =item * B<INCORRECT>:
                   5420: 
                   5421: The problem is open, and all tries have been used without getting the
                   5422: correct answer.
                   5423: 
                   5424: =item * B<OPEN>:
                   5425: 
                   5426: The item is open and not yet tried.
                   5427: 
                   5428: =item * B<ATTEMPTED>:
                   5429: 
                   5430: The problem has been attempted.
1.69      bowersj2 5431: 
1.450     raeburn  5432: =item * B<CREDIT_ATTEMPTED>:
                   5433: 
                   5434: The problem has been attempted, and credit given for the attempt (survey and anonymous survey only).
                   5435: 
1.185     bowersj2 5436: =item * B<ANSWER_SUBMITTED>:
                   5437: 
                   5438: An answer has been submitted, but the student should not see it.
                   5439: 
1.51      bowersj2 5440: =back
                   5441: 
                   5442: =cut
                   5443: 
1.432     raeburn  5444: sub TRIES_LEFT        { return 20; }
                   5445: sub ANSWER_SUBMITTED  { return 21; }
                   5446: sub PARTIALLY_CORRECT { return 22; }
                   5447: 
                   5448: sub RESERVED_LATER    { return 30; }
                   5449: sub RESERVED          { return 31; }
                   5450: sub RESERVED_LOCATION { return 32; }
                   5451: sub RESERVABLE        { return 33; }
                   5452: sub RESERVABLE_LATER  { return 34; }
                   5453: sub NOTRESERVABLE     { return 35; }
                   5454: sub NOT_IN_A_SLOT     { return 36; }
                   5455: sub NEEDS_CHECKIN     { return 37; }
                   5456: sub WAITING_FOR_GRADE { return 38; }
                   5457: sub UNKNOWN           { return 39; }
1.51      bowersj2 5458: 
                   5459: sub status {
                   5460:     my $self = shift;
                   5461:     my $part = shift;
                   5462:     if (!defined($part)) { $part = "0"; }
                   5463:     my $completionStatus = $self->getCompletionStatus($part);
                   5464:     my $dateStatus = $self->getDateStatus($part);
                   5465: 
                   5466:     # What we have is a two-dimensional matrix with 4 entries on one
                   5467:     # dimension and 5 entries on the other, which we want to colorize,
1.54      bowersj2 5468:     # plus network failure and "no date data at all".
1.51      bowersj2 5469: 
1.222     bowersj2 5470:     #if ($self->{RESOURCE_ERROR}) { return NETWORK_FAILURE; }
1.53      bowersj2 5471:     if ($completionStatus == NETWORK_FAILURE) { return NETWORK_FAILURE; }
1.51      bowersj2 5472: 
1.408     raeburn  5473:     my $suppressFeedback = 0;
                   5474:     if (($self->problemstatus($part) eq 'no') ||
                   5475:         ($self->problemstatus($part) eq 'no_feedback_ever')) {
                   5476:         $suppressFeedback = 1;
                   5477:     }
1.236     bowersj2 5478:     # If there's an answer date and we're past it, don't
                   5479:     # suppress the feedback; student should know
1.330     albertel 5480:     if ($self->duedate($part) && $self->duedate($part) < time() &&
                   5481: 	$self->answerdate($part) && $self->answerdate($part) < time()) {
1.236     bowersj2 5482: 	$suppressFeedback = 0;
                   5483:     }
1.185     bowersj2 5484: 
1.51      bowersj2 5485:     # There are a few whole rows we can dispose of:
1.53      bowersj2 5486:     if ($completionStatus == CORRECT ||
                   5487:         $completionStatus == CORRECT_BY_OVERRIDE ) {
1.332     albertel 5488: 	if ( $suppressFeedback ) { return ANSWER_SUBMITTED }
                   5489: 	my $awarded=$self->awarded($part);
                   5490: 	if ($awarded < 1 && $awarded > 0) {
                   5491:             return PARTIALLY_CORRECT;
                   5492: 	} elsif ($awarded<1) {
                   5493: 	    return INCORRECT;
                   5494: 	}
                   5495: 	return CORRECT; 
1.69      bowersj2 5496:     }
                   5497: 
1.343     albertel 5498:     # If it's WRONG... and not open
                   5499:     if ( ($completionStatus == INCORRECT || 
                   5500: 	  $completionStatus == INCORRECT_BY_OVERRIDE)
                   5501: 	 && (!$self->opendate($part) ||  $self->opendate($part) > time()) ) {
                   5502: 	return INCORRECT;
                   5503:     }
                   5504: 
1.69      bowersj2 5505:     if ($completionStatus == ATTEMPTED) {
                   5506:         return ATTEMPTED;
1.53      bowersj2 5507:     }
                   5508: 
1.450     raeburn  5509:     if ($completionStatus == CREDIT_ATTEMPTED) {
                   5510:         return CREDIT_ATTEMPTED;
                   5511:     }
                   5512: 
1.53      bowersj2 5513:     # If it's EXCUSED, then return that no matter what
                   5514:     if ($completionStatus == EXCUSED) {
                   5515:         return EXCUSED; 
1.51      bowersj2 5516:     }
                   5517: 
1.53      bowersj2 5518:     if ($dateStatus == NOTHING_SET) {
                   5519:         return NOTHING_SET;
1.51      bowersj2 5520:     }
                   5521: 
1.69      bowersj2 5522:     # Now we're down to a 4 (incorrect, incorrect_override, not_attempted)
                   5523:     # by 4 matrix (date statuses).
1.51      bowersj2 5524: 
1.54      bowersj2 5525:     if ($dateStatus == PAST_DUE_ANSWER_LATER ||
1.59      bowersj2 5526:         $dateStatus == PAST_DUE_NO_ANSWER ) {
1.272     albertel 5527:         return $suppressFeedback ? ANSWER_SUBMITTED : $dateStatus; 
1.51      bowersj2 5528:     }
                   5529: 
1.53      bowersj2 5530:     if ($dateStatus == ANSWER_OPEN) {
                   5531:         return ANSWER_OPEN;
1.51      bowersj2 5532:     }
                   5533: 
                   5534:     # Now: (incorrect, incorrect_override, not_attempted) x 
                   5535:     # (open_later), (open)
                   5536:     
1.53      bowersj2 5537:     if ($dateStatus == OPEN_LATER) {
                   5538:         return OPEN_LATER;
1.51      bowersj2 5539:     }
                   5540: 
                   5541:     # If it's WRONG...
1.53      bowersj2 5542:     if ($completionStatus == INCORRECT || $completionStatus == INCORRECT_BY_OVERRIDE) {
1.51      bowersj2 5543:         # and there are TRIES LEFT:
1.79      bowersj2 5544:         if ($self->tries($part) < $self->maxtries($part) || !$self->maxtries($part)) {
1.222     bowersj2 5545:             return $suppressFeedback ? ANSWER_SUBMITTED : TRIES_LEFT;
1.51      bowersj2 5546:         }
1.185     bowersj2 5547:         return $suppressFeedback ? ANSWER_SUBMITTED : INCORRECT; # otherwise, return orange; student can't fix this
1.51      bowersj2 5548:     }
                   5549: 
                   5550:     # Otherwise, it's untried and open
1.473     raeburn  5551:     return OPEN;
1.224     bowersj2 5552: }
                   5553: 
1.432     raeburn  5554: sub check_for_slot {
                   5555:     my $self = shift;
                   5556:     my $part = shift;
1.508     damieng  5557:     my $symb = $self->{SYMB};
1.432     raeburn  5558:     my ($use_slots,$available,$availablestudent) = $self->slot_control($part);
                   5559:     if (($use_slots ne '') && ($use_slots !~ /^\s*no\s*$/i)) {
                   5560:         my @slots = (split(/:/,$availablestudent),split(/:/,$available));
                   5561:         my $cid=$env{'request.course.id'};
                   5562:         my $cdom=$env{'course.'.$cid.'.domain'};
                   5563:         my $cnum=$env{'course.'.$cid.'.num'};
                   5564:         my $now = time;
1.473     raeburn  5565:         my $num_usable_slots = 0;
1.432     raeburn  5566:         if (@slots > 0) {
                   5567:             my %slots=&Apache::lonnet::get('slots',[@slots],$cdom,$cnum);
                   5568:             if (&Apache::lonnet::error(%slots)) {
                   5569:                 return (UNKNOWN);
                   5570:             }
1.473     raeburn  5571:             my @sorted_slots = &Apache::loncommon::sorted_slots(\@slots,\%slots,'starttime');
1.432     raeburn  5572:             my ($checkedin,$checkedinslot);
                   5573:             foreach my $slot_name (@sorted_slots) {
1.473     raeburn  5574:                 next if (!defined($slots{$slot_name}) || !ref($slots{$slot_name}));
1.432     raeburn  5575:                 my $end = $slots{$slot_name}->{'endtime'};
                   5576:                 my $start = $slots{$slot_name}->{'starttime'};
                   5577:                 my $ip = $slots{$slot_name}->{'ip'};
                   5578:                 if ($self->simpleStatus() == OPEN) {
                   5579:                     if ($end > $now) {
1.473     raeburn  5580:                         if ($start > $now) {
                   5581:                             return (RESERVED_LATER,$start,$slot_name);
                   5582:                         } else {
                   5583:                             if ($ip ne '') {
                   5584:                                 if (!&Apache::loncommon::check_ip_acc($ip)) {
1.503     raeburn  5585:                                     return (RESERVED_LOCATION,$end,$slot_name);
1.432     raeburn  5586:                                 }
                   5587:                             }
1.473     raeburn  5588:                             my @proctors;
                   5589:                             if ($slots{$slot_name}->{'proctor'} ne '') {
                   5590:                                 @proctors = split(',',$slots{$slot_name}->{'proctor'});
                   5591:                             }
                   5592:                             if (@proctors > 0) {
                   5593:                                 ($checkedin,$checkedinslot) = $self->checkedin();
                   5594:                                 unless ((grep(/^\Q$checkedin\E/,@proctors)) &&
                   5595:                                         ($checkedinslot eq $slot_name)) {
                   5596:                                     return (NEEDS_CHECKIN,undef,$slot_name); 
1.472     raeburn  5597:                                 }
1.432     raeburn  5598:                             }
1.473     raeburn  5599:                             return (RESERVED,$end,$slot_name);
1.432     raeburn  5600:                         }
                   5601:                     }
1.473     raeburn  5602:                 } elsif ($end > $now) {
                   5603:                     $num_usable_slots ++;
1.432     raeburn  5604:                 }
                   5605:             }
                   5606:             my ($is_correct,$got_grade);
                   5607:             if ($self->is_task()) {
                   5608:                 my $taskstatus = $self->taskstatus();
                   5609:                 $is_correct = (($taskstatus eq 'pass') || 
                   5610:                                ($self->solved() =~ /^correct_/));
1.457     raeburn  5611:                 $got_grade = ($taskstatus =~ /^(?:pass|fail)$/);
1.432     raeburn  5612:             } else {
                   5613:                 $got_grade = 1;
                   5614:                 $is_correct = ($self->solved() =~ /^correct_/);   
                   5615:             }
                   5616:             ($checkedin,$checkedinslot) = $self->checkedin();
                   5617:             if ($checkedin) {
                   5618:                 if (!$got_grade) {
                   5619:                     return (WAITING_FOR_GRADE);
                   5620:                 } elsif ($is_correct) {
                   5621:                     return (CORRECT); 
                   5622:                 }
                   5623:             }
1.473     raeburn  5624:             if ($num_usable_slots) {
                   5625:                 return(NOT_IN_A_SLOT);
                   5626:             }
                   5627:         }
                   5628:         my $reservable = &Apache::lonnet::get_reservable_slots($cnum,$cdom,$env{'user.name'},
                   5629:                                                                $env{'user.domain'});
                   5630:         if (ref($reservable) eq 'HASH') {
1.511     raeburn  5631:             my ($map) = &Apache::lonnet::decode_symb($symb);
1.473     raeburn  5632:             if ((ref($reservable->{'now_order'}) eq 'ARRAY') && (ref($reservable->{'now'}) eq 'HASH')) {
1.474     raeburn  5633:                 foreach my $slot (reverse (@{$reservable->{'now_order'}})) {
1.510     raeburn  5634:                     my $canuse;
                   5635:                     if ($reservable->{'now'}{$slot}{'symb'} eq '') {
                   5636:                         $canuse = 1;
                   5637:                     } else {
                   5638:                         my %oksymbs;
                   5639:                         my @slotsymbs = split(/\s*,\s*/,$reservable->{'now'}{$slot}{'symb'});
                   5640:                         map { $oksymbs{$_} = 1; } @slotsymbs;
                   5641:                         if ($oksymbs{$symb}) {
                   5642:                             $canuse = 1;
                   5643:                         } else {
                   5644:                             foreach my $item (@slotsymbs) {
                   5645:                                 if ($item =~ /\.(page|sequence)$/) {
                   5646:                                     (undef,undef, my $sloturl) = &Apache::lonnet::decode_symb($item);
                   5647:                                     if (($map ne '') && ($map eq $sloturl)) {
                   5648:                                         $canuse = 1;
                   5649:                                         last;
                   5650:                                     }
                   5651:                                 }
                   5652:                             }
                   5653:                         }
                   5654:                     }
                   5655:                     if ($canuse) {
1.473     raeburn  5656:                         return(RESERVABLE,$reservable->{'now'}{$slot}{'endreserve'});
                   5657:                     }
                   5658:                 }
1.432     raeburn  5659:             }
1.473     raeburn  5660:             if ((ref($reservable->{'future_order'}) eq 'ARRAY') && (ref($reservable->{'future'}) eq 'HASH')) {
1.474     raeburn  5661:                 foreach my $slot (@{$reservable->{'future_order'}}) {
1.510     raeburn  5662:                     my $canuse;
                   5663:                     if ($reservable->{'future'}{$slot}{'symb'} eq '') {
                   5664:                         $canuse = 1;
                   5665:                     } elsif ($reservable->{'future'}{$slot}{'symb'} =~ /,/) {
                   5666:                         my %oksymbs;
                   5667:                         my @slotsymbs = split(/\s*,\s*/,$reservable->{'future'}{$slot}{'symb'});
                   5668:                         map { $oksymbs{$_} = 1; } @slotsymbs;
                   5669:                         if ($oksymbs{$symb}) {
                   5670:                             $canuse = 1;
                   5671:                         } else {
                   5672:                             foreach my $item (@slotsymbs) {
                   5673:                                 if ($item =~ /\.(page|sequence)$/) {
                   5674:                                     (undef,undef, my $sloturl) = &Apache::lonnet::decode_symb($item);
                   5675:                                     if (($map ne '') && ($map eq $sloturl)) {
                   5676:                                         $canuse = 1;
                   5677:                                         last;
                   5678:                                     }
                   5679:                                 }
                   5680:                             }
                   5681:                         }
                   5682:                     } elsif ($reservable->{'future'}{$slot}{'symb'} eq $symb) {
                   5683:                         $canuse = 1;
                   5684:                     }
                   5685:                     if ($canuse) {
1.473     raeburn  5686:                         return(RESERVABLE_LATER,$reservable->{'future'}{$slot}{'startreserve'});
                   5687:                     }
                   5688:                 }
1.432     raeburn  5689:             }
                   5690:         }
1.473     raeburn  5691:         return(NOTRESERVABLE);
1.432     raeburn  5692:     }
                   5693:     return;
                   5694: }
                   5695: 
1.224     bowersj2 5696: sub CLOSED { return 23; }
                   5697: sub ERROR { return 24; }
                   5698: 
                   5699: =pod
                   5700: 
                   5701: B<Simple Status>
                   5702: 
                   5703: Convenience method B<simpleStatus> provides a "simple status" for the resource.
                   5704: "Simple status" corresponds to "which icon is shown on the
                   5705: Navmaps". There are six "simple" statuses:
                   5706: 
                   5707: =over 4
                   5708: 
                   5709: =item * B<CLOSED>: The problem is currently closed. (No icon shown.)
                   5710: 
                   5711: =item * B<OPEN>: The problem is open and unattempted.
                   5712: 
                   5713: =item * B<CORRECT>: The problem is correct for any reason.
                   5714: 
                   5715: =item * B<INCORRECT>: The problem is incorrect and can still be
                   5716: completed successfully.
                   5717: 
                   5718: =item * B<ATTEMPTED>: The problem has been attempted, but the student
                   5719: does not know if they are correct. (The ellipsis icon.)
                   5720: 
                   5721: =item * B<ERROR>: There is an error retrieving information about this
                   5722: problem.
                   5723: 
                   5724: =back
                   5725: 
                   5726: =cut
                   5727: 
                   5728: # This hash maps the composite status to this simple status, and
                   5729: # can be used directly, if you like
                   5730: my %compositeToSimple = 
                   5731:     (
                   5732:       NETWORK_FAILURE()       => ERROR,
                   5733:       NOTHING_SET()           => CLOSED,
                   5734:       CORRECT()               => CORRECT,
1.332     albertel 5735:       PARTIALLY_CORRECT()     => PARTIALLY_CORRECT,
1.224     bowersj2 5736:       EXCUSED()               => CORRECT,
                   5737:       PAST_DUE_NO_ANSWER()    => INCORRECT,
                   5738:       PAST_DUE_ANSWER_LATER() => INCORRECT,
                   5739:       ANSWER_OPEN()           => INCORRECT,
                   5740:       OPEN_LATER()            => CLOSED,
                   5741:       TRIES_LEFT()            => OPEN,
                   5742:       INCORRECT()             => INCORRECT,
                   5743:       OPEN()                  => OPEN,
                   5744:       ATTEMPTED()             => ATTEMPTED,
1.450     raeburn  5745:       CREDIT_ATTEMPTED()      => CORRECT,
1.224     bowersj2 5746:       ANSWER_SUBMITTED()      => ATTEMPTED
                   5747:      );
                   5748: 
                   5749: sub simpleStatus {
                   5750:     my $self = shift;
                   5751:     my $part = shift;
                   5752:     my $status = $self->status($part);
                   5753:     return $compositeToSimple{$status};
1.225     bowersj2 5754: }
                   5755: 
                   5756: =pod
                   5757: 
                   5758: B<simpleStatusCount> will return an array reference containing, in
                   5759: this order, the number of OPEN, CLOSED, CORRECT, INCORRECT, ATTEMPTED,
                   5760: and ERROR parts the given problem has.
                   5761: 
                   5762: =cut
                   5763:     
                   5764: # This maps the status to the slot we want to increment
                   5765: my %statusToSlotMap = 
                   5766:     (
                   5767:      OPEN()      => 0,
                   5768:      CLOSED()    => 1,
                   5769:      CORRECT()   => 2,
                   5770:      INCORRECT() => 3,
                   5771:      ATTEMPTED() => 4,
                   5772:      ERROR()     => 5
                   5773:      );
                   5774: 
                   5775: sub statusToSlot { return $statusToSlotMap{shift()}; }
                   5776: 
                   5777: sub simpleStatusCount {
                   5778:     my $self = shift;
                   5779: 
                   5780:     my @counts = (0, 0, 0, 0, 0, 0, 0);
                   5781:     foreach my $part (@{$self->parts()}) {
                   5782: 	$counts[$statusToSlotMap{$self->simpleStatus($part)}]++;
                   5783:     }
                   5784: 
                   5785:     return \@counts;
1.191     bowersj2 5786: }
                   5787: 
                   5788: =pod
                   5789: 
                   5790: B<Completable>
                   5791: 
                   5792: The completable method represents the concept of I<whether the student can
                   5793: currently do the problem>. If the student can do the problem, which means
                   5794: that it is open, there are tries left, and if the problem is manually graded
                   5795: or the grade is suppressed via problemstatus, the student has not tried it
                   5796: yet, then the method returns 1. Otherwise, it returns 0, to indicate that 
                   5797: either the student has tried it and there is no feedback, or that for
                   5798: some reason it is no longer completable (not open yet, successfully completed,
                   5799: out of tries, etc.). As an example, this is used as the filter for the
                   5800: "Uncompleted Homework" option for the nav maps.
                   5801: 
                   5802: If this does not quite meet your needs, do not fiddle with it (unless you are
                   5803: fixing it to better match the student's conception of "completable" because
                   5804: it's broken somehow)... make a new method.
                   5805: 
                   5806: =cut
                   5807: 
                   5808: sub completable {
                   5809:     my $self = shift;
                   5810:     if (!$self->is_problem()) { return 0; }
                   5811:     my $partCount = $self->countParts();
                   5812: 
                   5813:     foreach my $part (@{$self->parts()}) {
                   5814:         if ($part eq '0' && $partCount != 1) { next; }
                   5815:         my $status = $self->status($part);
                   5816:         # "If any of the parts are open, or have tries left (implies open),
                   5817:         # and it is not "attempted" (manually graded problem), it is
                   5818:         # not "complete"
1.216     bowersj2 5819: 	if ($self->getCompletionStatus($part) == ATTEMPTED() ||
1.450     raeburn  5820:             $self->getCompletionStatus($part) == CREDIT_ATTEMPTED() ||
1.216     bowersj2 5821: 	    $status == ANSWER_SUBMITTED() ) {
                   5822: 	    # did this part already, as well as we can
                   5823: 	    next;
                   5824: 	}
                   5825: 	if ($status == OPEN() || $status == TRIES_LEFT()) {
                   5826: 	    return 1;
                   5827: 	}
1.191     bowersj2 5828:     }
                   5829:         
                   5830:     # If all the parts were complete, so was this problem.
1.216     bowersj2 5831:     return 0;
1.51      bowersj2 5832: }
                   5833: 
                   5834: =pod
                   5835: 
1.507     raeburn  5836: B<Answerable>
                   5837: 
                   5838: The answerable method differs from the completable method in its handling of problem parts
                   5839: for which feedback on correctness is suppressed, but the student still has tries left, and
                   5840: the problem part is not past due, (i.e., the student could submit a different answer if
                   5841: he/she so chose). For that case completable will return 0, whereas answerable will return 1.
                   5842: 
                   5843: =cut
                   5844: 
                   5845: sub answerable {
                   5846:     my $self = shift;
                   5847:     if (!$self->is_problem()) { return 0; }
                   5848:     my $partCount = $self->countParts();
                   5849:     foreach my $part (@{$self->parts()}) {
                   5850:         if ($part eq '0' && $partCount != 1) { next; }
                   5851:         my $status = $self->status($part);
                   5852:         if ($self->getCompletionStatus($part) == ATTEMPTED() ||
                   5853:             $self->getCompletionStatus($part) == CREDIT_ATTEMPTED() ||
                   5854:             $status == ANSWER_SUBMITTED() ) {
                   5855:             if ($self->tries($part) < $self->maxtries($part) || !$self->maxtries($part)) {
                   5856:                 return 1;
                   5857:             }
                   5858:         }
                   5859:         if ($status == OPEN() || $status == TRIES_LEFT() || $status == NETWORK_FAILURE()) {
                   5860:             return 1;
                   5861:         }
                   5862:     }
                   5863:     # None of the parts were answerable, so neither is this problem.
                   5864:     return 0;
                   5865: }
                   5866: 
                   5867: =pod
                   5868: 
1.51      bowersj2 5869: =head2 Resource/Nav Map Navigation
                   5870: 
                   5871: =over 4
                   5872: 
1.174     albertel 5873: =item * B<getNext>():
                   5874: 
                   5875: Retreive an array of the possible next resources after this
                   5876: one. Always returns an array, even in the one- or zero-element case.
                   5877: 
                   5878: =item * B<getPrevious>():
1.85      bowersj2 5879: 
1.174     albertel 5880: Retreive an array of the possible previous resources from this
                   5881: one. Always returns an array, even in the one- or zero-element case.
1.51      bowersj2 5882: 
                   5883: =cut
                   5884: 
                   5885: sub getNext {
                   5886:     my $self = shift;
                   5887:     my @branches;
                   5888:     my $to = $self->to();
1.85      bowersj2 5889:     foreach my $branch ( split(/,/, $to) ) {
1.51      bowersj2 5890:         my $choice = $self->{NAV_MAP}->getById($branch);
1.342     albertel 5891:         #if (!$choice->condition()) { next; }
1.51      bowersj2 5892:         my $next = $choice->goesto();
                   5893:         $next = $self->{NAV_MAP}->getById($next);
                   5894: 
1.131     bowersj2 5895:         push @branches, $next;
1.85      bowersj2 5896:     }
                   5897:     return \@branches;
                   5898: }
                   5899: 
                   5900: sub getPrevious {
                   5901:     my $self = shift;
                   5902:     my @branches;
                   5903:     my $from = $self->from();
1.504     raeburn  5904:     foreach my $branch ( split(/,/, $from)) {
1.85      bowersj2 5905:         my $choice = $self->{NAV_MAP}->getById($branch);
                   5906:         my $prev = $choice->comesfrom();
                   5907:         $prev = $self->{NAV_MAP}->getById($prev);
                   5908: 
1.131     bowersj2 5909:         push @branches, $prev;
1.51      bowersj2 5910:     }
                   5911:     return \@branches;
1.131     bowersj2 5912: }
                   5913: 
                   5914: sub browsePriv {
                   5915:     my $self = shift;
1.505     raeburn  5916:     my $noblockcheck = shift;
1.131     bowersj2 5917:     if (defined($self->{BROWSE_PRIV})) {
                   5918:         return $self->{BROWSE_PRIV};
                   5919:     }
                   5920: 
1.311     albertel 5921:     $self->{BROWSE_PRIV} = &Apache::lonnet::allowed('bre',$self->src(),
1.508     damieng  5922: 						    $self->{SYMB},undef,
1.505     raeburn  5923:                                                     undef,$noblockcheck);
1.51      bowersj2 5924: }
                   5925: 
                   5926: =pod
1.2       www      5927: 
1.51      bowersj2 5928: =back
1.2       www      5929: 
1.51      bowersj2 5930: =cut
1.2       www      5931: 
1.51      bowersj2 5932: 1;
1.2       www      5933: 
1.51      bowersj2 5934: __END__
1.2       www      5935: 
                   5936: 

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>
500 Internal Server Error

Internal Server Error

The server encountered an internal error or misconfiguration and was unable to complete your request.

Please contact the server administrator at root@localhost to inform them of the time this error occurred, and the actions you performed just before this error.

More information about this error may be available in the server error log.