LP# 1086934 - TPAC: Complete column sorting in some screens
authorDan Pearl <dpearl@cwmars.org>
Wed, 14 Nov 2012 17:14:11 +0000 (12:14 -0500)
committerBen Shum <bshum@biblio.org>
Wed, 19 Aug 2015 19:21:20 +0000 (15:21 -0400)
(specifically holds, circs, and circs_history)

An earlier LP issue #1010277 concerned the halfway implementation of
the column sort facility, and was addressed at the time by ripping out any hint of
column sort capability, among other cleanup issues.

The sorting capability has now been implemented with the following functionality:
  * Clicking on the appropriate column heads now sorts the contents from
    "ascending" to "descending" to "no sort".  (The "no sort" will restore the
    original list as presented to the patron.)

  * The sort indicator (an up or down arrow) is placed to the right
    of the column head, as appropriate.

  * The combined "Title/Author" column in the circ screens is now separated into two
    independently sortable columns (Title and Author).

  * Title sorting is done with the 'filing' characters (leading "the", "a",
    "an", and other langugage equivalents) removed. To clarify the
    behavior for the patron, the leading articles are rendered in
    a smaller font, so as to keep the main entry prominent.  In
    addition to the filing characters removed for the sort, leading
    non-alphanumeric characters are ignored in the sort.

This commit only affects those screens and columns that suggested column sorting
capability.

Signed-off-by: Dan Pearl <dpearl@cwmars.org>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>
Signed-off-by: Ben Shum <bshum@biblio.org>

Conflicts:
Open-ILS/src/templates/opac/parts/topnav.tt2

Conflicts:
Open-ILS/src/templates/opac/css/style.css.tt2

Signed-off-by: Kathy Lussier <klussier@masslnc.org>

Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/templates/opac/css/style.css.tt2
Open-ILS/src/templates/opac/myopac/circ_history.tt2
Open-ILS/src/templates/opac/myopac/circs.tt2
Open-ILS/src/templates/opac/myopac/holds.tt2
Open-ILS/src/templates/opac/parts/misc_util.tt2
Open-ILS/src/templates/opac/parts/myopac/base.tt2
Open-ILS/src/templates/opac/parts/myopac/column_sort_support.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/topnav.tt2
docs/RELEASE_NOTES_NEXT/OPAC/column_sort.txt [new file with mode: 0644]

index 3dae3bf..30e0756 100644 (file)
@@ -892,7 +892,14 @@ sub load_myopac_holds {
     my $hold_handle_result;
     $hold_handle_result = $self->handle_hold_update($action) if $action;
 
-    my $holds_object = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available, $limit, $offset);
+    my $holds_object;
+    if ($self->cgi->param('sort') ne "") {
+        $holds_object = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available);
+    }
+    else {
+        $holds_object = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available, $limit, $offset);
+    }
+
     if($holds_object->{holds}) {
         $ctx->{holds} = $holds_object->{holds};
     }
@@ -1519,7 +1526,22 @@ sub load_myopac_circ_history {
     $ctx->{circ_history_limit} = $limit;
     $ctx->{circ_history_offset} = $offset;
 
-    my $circ_ids = $e->json_query({
+    my $circ_ids;
+    if ($self->cgi->param('sort') ne "") {             # Defer limitation to circ_history.tt2
+       $circ_ids = $e->json_query({
+        select => {
+            au => [{
+                column => 'id', 
+                transform => 'action.usr_visible_circs', 
+                result_field => 'id'
+            }]
+        },
+        from => 'au',
+        where => {id => $e->requestor->id}  
+        });
+
+    } else {
+       $circ_ids = $e->json_query({
         select => {
             au => [{
                 column => 'id', 
@@ -1531,7 +1553,8 @@ sub load_myopac_circ_history {
         where => {id => $e->requestor->id}, 
         limit => $limit,
         offset => $offset
-    });
+        });
+    }
 
     $ctx->{circs} = $self->fetch_user_circs(1, [map { $_->{id} } @$circ_ids]);
     return Apache2::Const::OK;
index cafccb6..ff8c6ad 100644 (file)
@@ -2108,3 +2108,9 @@ label[for*=expert_]
 {
     font-weight: bold;
 }  
+} 
+
+.sort_deemphasize {
+    font-weight: lighter;
+    font-size: 70%;
+}
index 4775b93..5339b25 100644 (file)
@@ -1,5 +1,6 @@
 [%  PROCESS "opac/parts/header.tt2";
     PROCESS "opac/parts/misc_util.tt2";
+    PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "circs"
     limit = ctx.circ_history_limit;
     
     <div id="acct_checked_tabs">
         <div class="align">
-            <a href='[% mkurl('circs') %]'>[% l("Current Items Checked Out") %]</a>
+            <a href='[% mkurl('circs',{},1) %]'>[% l("Current Items Checked Out") %]</a>
         </div>
         <div class="align selected">
             <a href="#">[% l("Check Out History") %]</a>
         </div>
     </div>
 
+    [% 
+    # In the sorting case, the size is the size of ALL the circ items.  In the non-sorting case,
+    # the size is simply the size of the chunk passed in.  See the TODO below for the still-lingering
+    # bug.
+    sort_field = CGI.param('sort');
+    IF (sort_field);
+        no_next = ctx.circs.size - offset <= limit;
+    ELSE;
+        no_next = ctx.circs.size < limit;
+    END;
+    %]
+
     <div class="header_middle">
         <span class="float-left">[% l('Previously Checked Out Items') %]</span>
         <span class='float-left' style='padding-left: 10px;'>
@@ -25,7 +38,7 @@
                 [% IF offset == 0 %] class='invisible' [% END %]><span class="nav_arrow_fix">&#9668;</span>[% l('Previous') %]</a>
             [%# TODO: get total to prevent paging off then end of the list.. %]
             <a href='[% mkurl('circ_history', {limit => limit, offset => (offset + limit)}) %]'
-               [% IF ctx.circs.size < limit %] class='invisible' [% END %] >[% l('Next') %]<span class="nav_arrow_fix">&#9658;</span></a>
+               [% IF no_next %] class='invisible' [% END %] >[% l('Next') %]<span class="nav_arrow_fix">&#9658;</span></a>
         </span>
         <div class="float-left">
             <form action="[% mkurl(ctx.opac_root _ '/myopac/circ_history/export') %]" method="post">
             title="[% l('History of Items Checked Out') %]">
             <thead>
                 <tr>
-                    <th>[% l('Title / Author') %]</th>
-                    <th>[% l('Checkout Date') %]</th>
-                    <th>[% l('Due Date') %]</th>
-                    <th>[% l('Date Returned') %]</th>
-                    <th>[% l('Barcode') %]</th>
-                    <th>[% l('Call Number') %]</th>
+                    <th>[% sort_head("sort_title", l("Title")) %]</th>
+                    <th>[% sort_head("author", l("Author")) %]</th>
+                    <th>[% sort_head("checkout", l("Checkout Date")) %]</th>
+                    <th>[% sort_head("due", l("Due Date")) %]</th>
+                    <th>[% sort_head("returned", l("Date Returned")) %]</th>
+                    <th>[% sort_head("barcode", l("Barcode")) %]</th>
+                    <th>[% sort_head("callnum", l("Call Number")) %]</th>
                 </tr>
             </thead>
             <tbody>
-                [% FOR circ IN ctx.circs;
-                    attrs = {marc_xml => circ.marc_xml};
-                    PROCESS get_marc_attrs args=attrs; %]
+                [%# Copy the ctx.circs into a local array, then add a SORT field
+                    that contains the value to sort on.  Since we need the item attrs,
+                    invoke it and save the result in ATTRS.
+               %]
+               [% 
+                circ_items = ctx.circs;  # Array assignment
+
+                FOR circ IN circ_items;
+                    circ.ATTRS = {marc_xml => circ.marc_xml};
+                    PROCESS get_marc_attrs args=circ.ATTRS;
+               
+                    SWITCH sort_field;
+
+                       CASE "sort_title";
+                          circ.SORTING = circ.ATTRS.sort_title;
+
+                       CASE "author";
+                          circ.SORTING = circ.ATTRS.author;
+
+                       CASE "checkout";
+                          circ.SORTING = circ.circ.xact_start;
+
+                       CASE "due";
+                          circ.SORTING = circ.circ.due_date;
+
+                       CASE "returned";
+                          circ.SORTING = circ.circ.checkin_time;
+
+                       CASE "barcode";
+                          circ.SORTING = circ.circ.target_copy.barcode;
+
+                       CASE "callnum";
+                          circ.SORTING = circ.circ.target_copy.call_number.label;
+
+                       CASE;
+                          sort_field = "";
+                    END; # SWITCH
+                END; #FOR circ
+
+                IF (sort_field != "sort_title");
+                   deemphasize_class = "";
+                ELSE;
+                   deemphasize_class = " class=\"sort_deemphasize\"";
+                END;
+                       
+                # Apply sorting to circ_items
+                IF (sort_field);
+                   circ_items = circ_items.sort("SORTING");
+                   IF (CGI.param("sort_type") == "desc");
+                      circ_items = circ_items.reverse;
+                   END;
+
+                   # Shorten the circ_items list per offset/limit/cout
+                   hi = offset + limit - 1;
+                   hi = hi > circ_items.max ? circ_items.max : hi;
+
+                   circ_items = circ_items.slice(offset, hi);
+                END;
+
+                # circ_items list is now sorted.  Traverse and dump the information.
+
+                FOR circ IN circ_items; %]
                     <tr>
                         <td>
-                            <a href="[% mkurl(ctx.opac_root _ '/record/' _ circ.circ.target_copy.call_number.record.id) %]" 
-                                [% html_text_attr('title', l('Catalog record [_1]', attrs.title)) %]>
-                                [% attrs.title | html %]
-                            </a>
-                            [% IF attrs.author %] /
+                            <a href="[% mkurl(ctx.opac_root _ '/record/' _ 
+                                circ.circ.target_copy.call_number.record.id, {}, 1) %]"
+                                name="[% l('Catalog record') %]"><span[%- deemphasize_class -%]>
+                                [%- circ.ATTRS.title.substr(0,circ.ATTRS.nonfiling_characters) | html %]</span>
+                                [%- circ.ATTRS.title.substr(circ.ATTRS.nonfiling_characters) | html %]</a>
+                        </td>
+                        <td>
                             <a href="[% mkurl(ctx.opac_root _ '/results',
-                                {qtype => 'author', query => attrs.author.replace('[,\.:;]', '')}
-                            )%]">[% attrs.author | html %]</a>
-                            [% END %]
+                                {qtype => 'author', query => circ.ATTRS.author.replace('[,\.:;]', '')},
+                                1
+                            ) %]">[% circ.ATTRS.author | html %]</a>
                         </td>
                         <td>
                             [% date.format(ctx.parse_datetime(circ.circ.xact_start),DATE_FORMAT); %]
                             [% date.format(ctx.parse_datetime(circ.circ.due_date),DATE_FORMAT); %]
                         </td>
                         <td>
-                            [% 
-                                IF circ.circ.checkin_time; 
+                            [% IF circ.circ.checkin_time; 
                                     date.format(ctx.parse_datetime(circ.circ.checkin_time),DATE_FORMAT); 
                                 ELSE; %]
                                 <span style='color:blue;'>*</span><!-- meh -->
index 6ca68a1..c78c153 100644 (file)
@@ -1,5 +1,6 @@
 [%  PROCESS "opac/parts/header.tt2";
     PROCESS "opac/parts/misc_util.tt2";
+    PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "circs"  %]
 <h3 class="sr-only">[% l('Current Items Checked Out') %]</h3>
@@ -10,7 +11,7 @@
             <a href="#">[% l("Current Items Checked Out") %]</a>
         </div>
         <div class="align">
-            <a href="[% mkurl('circ_history') %]">[% l("Check Out History") %]</a>
+            <a href="[% mkurl('circ_history',{},1) %]">[% l("Check Out History") %]</a>
         </div>
     </div>
 
                         onclick="var inputs=document.getElementsByTagName('input'); for (i = 0; i < inputs.length; i++) { if (inputs[i].name == 'circ' &amp;&amp; !inputs[i].disabled) inputs[i].checked = this.checked;}"
                     />
                 </th>
-                <th>[% l('Title / Author') %]</th>
-                <th>[% l('Renewals Left') %]</th>
-                <th>[% l('Due Date') %]</th>
-                <th>[% l('Barcode') %]</th>
-                <th>[% l('Call number') %]</th>
+                <th>[% sort_head("sort_title", l("Title")) %]</th>
+                <th>[% sort_head("author", l("Author")) %]</th>
+                <th>[% sort_head("renews", l("Renewals Left")) %]</th>
+                <th>[% sort_head("due", l("Due Date")) %]</th>
+                <th>[% sort_head("barcode", l("Barcode")) %]</th>
+                <th>[% sort_head("callnum", l("Call number")) %]</th>
             </tr>
             </thead>
-                <tbody>
-                    [% FOR circ IN ctx.circs;
-                        attrs = {marc_xml => circ.marc_xml};
-                        PROCESS get_marc_attrs args=attrs; %]
+            <tbody>
+                [%# Copy the ctx.circs into a local array, then add a SORT field
+                    that contains the value to sort on.  Since we need the item attrs,
+                    invoke it and save the result in ATTRS.
+               %]
+               [% 
+                circ_items = ctx.circs;  # Array assignment
+
+                sort_field = CGI.param('sort');  # unless changed below...
+
+                FOR circ IN circ_items;
+                    circ.ATTRS = {marc_xml => circ.marc_xml};
+                    PROCESS get_marc_attrs args=circ.ATTRS;
+               
+                    SWITCH sort_field;
+
+                       CASE "sort_title";
+                          circ.SORTING = circ.ATTRS.sort_title;
+
+                       CASE "author";
+                          circ.SORTING = circ.ATTRS.author;
+
+                       CASE "renews";
+                          circ.SORTING = circ.circ.renewal_remaining;
+                       
+                       CASE "due";
+                          circ.SORTING = circ.circ.due_date;
+                       
+                       CASE "barcode";
+                          circ.SORTING = circ.circ.target_copy.barcode;
+
+                       CASE "callnum";
+                          circ.SORTING = circ.circ.target_copy.call_number.label;
+                       
+                       CASE;
+                          sort_field = "";
+                    END; # SWITCH
+                END; #FOR circ
+
+                IF (sort_field != "sort_title");
+                   deemphasize_class = "";
+                ELSE;
+                   deemphasize_class = " class=\"sort_deemphasize\"";
+                END;
+
+                # Apply sorting to circ_items
+               IF (sort_field);
+                   circ_items = circ_items.sort("SORTING");
+                    IF (CGI.param("sort_type") == "desc");
+                        circ_items = circ_items.reverse;
+                    END;
+                END;
+
+               # circ_items list is now sorted.  Traverse and dump the information.
+
+                FOR circ IN circ_items; %]
                     <tr>
                         <td class="checkbox_column" valign="top">
                             <input type="checkbox" name="circ"
                                 [% IF circ.circ.renewal_remaining < 1 %] disabled="disabled" [% END %]
                                 value="[% circ.circ.id %]" />
                         </td>
-                        <td name="author">
+                        <td name="title">
                             [% IF circ.circ.target_copy.call_number.id == -1 %]
                                 [% circ.circ.target_copy.dummy_title | html %]
-                            [% ELSIF attrs.title %]
-                            <a href="[% mkurl(ctx.opac_root _ '/record/' _ 
-                                circ.circ.target_copy.call_number.record.id) %]"
-                                name="[% l('Catalog record') %]">[% attrs.title | html %]</a>
+                            [% ELSIF circ.ATTRS.title %]
+                                <a href="[% mkurl(ctx.opac_root _ '/record/' _ 
+                                    circ.circ.target_copy.call_number.record.id, {}, 1) %]"
+                                    name="[% l('Catalog record') %]"><span[%- deemphasize_class -%]>
+                                    [%- circ.ATTRS.title.substr(0,circ.ATTRS.nonfiling_characters) | html %]</span>
+                                    [%- circ.ATTRS.title.substr(circ.ATTRS.nonfiling_characters) | html %]</a>
                             [% END %]
-                            [% IF circ.circ.target_copy.call_number.id == -1 %] /
+                        </td>
+                        <td name="author">
+                            [% IF circ.circ.target_copy.call_number.id == -1 %]
                                 [% circ.circ.target_copy.dummy_author | html %]
-                            [% ELSIF attrs.author %] /
-                            <a href="[% mkurl(ctx.opac_root _ '/results',
-                                {qtype => 'author', query => attrs.author.replace('[,\.:;]', '')}
-                            ) %]">[% attrs.author | html %]</a>
+                            [% ELSIF circ.ATTRS.author %]
+                                <a href="[% mkurl(ctx.opac_root _ '/results',
+                                    {qtype => 'author', query => circ.ATTRS.author.replace('[,\.:;]', '')},
+                                    1
+                                ) %]">[% circ.ATTRS.author | html %]</a>
                             [% END %]
                         </td>
                         <td name="renewals">
                             </span>
                         </td>
                     </tr>
-                    [%  END;
+                    [%  END; # FOR
+
                     END %]
                 </tbody>
             </table>
index 6dbd4a2..68f9b4e 100644 (file)
@@ -1,6 +1,7 @@
 [%  PROCESS "opac/parts/header.tt2";
     PROCESS "opac/parts/misc_util.tt2";
     PROCESS "opac/parts/hold_status.tt2";
+    PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "holds";
     limit = (ctx.holds_limit.defined) ? ctx.holds_limit : 0;
@@ -15,7 +16,7 @@
             <a href='#'>[% l("Items on Hold") %]</a>
         </div>
         <div class="align">
-            <a href='[% mkurl('hold_history', {}, ['limit','offset','available']) %]'>[% l("Holds History") %]</a>
+            <a href='[% mkurl('hold_history', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("Holds History") %]</a>
         </div>
     </div>
 
@@ -47,9 +48,6 @@
                [% IF count <= limit + offset %] class='invisible' [% END %] >[% l('Next') %]<span class="nav_arrow_fix">&#9658;</span></a>
         </span>
 
-        <span style="float:right;">
-            <a class="hide_me" href="#">[% l('Export List') %]</a>
-        </span>
     </div>
     <div class="clear-both"></div>
     <div id='holds_main'>
                     <input type="checkbox" title="[% l('Select All Holds') %]"
                       onclick="var inputs=document.getElementsByTagName('input'); for (i = 0; i < inputs.length; i++) { if (inputs[i].name == 'hold_id' &amp;&amp; !inputs[i].disabled) inputs[i].checked = this.checked;}"/>
                 </th>
-                <th>[% l('Title') %]</th>
-                <th>[% l('Author') %]</th>
-                <th>[% l('Format') %]</th>
+                <th>[% sort_head("sort_title", l('Title')) %]</th>
+                <th>[% sort_head("author", l('Author')) %]</th>
+                <th>[% sort_head("format", l('Format')) %]</th>
                 <th>[% l('Pickup Location') %]</th>
                 <th>[% l('Activate') %]</th>
                 <th>[% l('Cancel if not filled by') %]</th>
             </tr>
             </thead>
             <tbody id="holds_temp_parent">
-                [% FOR hold IN ctx.holds;
-                    attrs = {marc_xml => hold.marc_xml};
-                    PROCESS get_marc_attrs args=attrs;
+
+                [%# Copy the ctx.holds into a local array, then add a SORT field
+                    that contains the value to sort on.  Since we need the item attrs,
+                    invoke it and save the result in ATTRS.
+               %]
+               [% 
+                hold_items = ctx.holds;
+
+                sort_field = CGI.param('sort');
+
+                FOR hold IN hold_items;
+                    hold.ATTRS = {marc_xml => hold.marc_xml};
+                    PROCESS get_marc_attrs args=hold.ATTRS;
+
+                    SWITCH sort_field;
+
+                       CASE "sort_title";
+                          hold.SORTING = hold.ATTRS.sort_title;
+
+                       CASE "author";
+                          hold.SORTING = hold.ATTRS.author;
+
+                       CASE "format";
+                          hold.SORTING = hold.ATTRS.format_label;
+                       
+                       CASE;
+                          sort_field = "";
+                    END; # SWITCH
+                END; #FOR hold
+
+                IF (sort_field != "sort_title");
+                   deemphasize_class = "";
+                ELSE;
+                   deemphasize_class = " class=\"sort_deemphasize\"";
+                END;
+
+                # Apply sorting to hold_items
+               IF (sort_field != "");
+                   hold_items = hold_items.sort("SORTING");
+                    IF (CGI.param("sort_type") == "desc");
+                        hold_items = hold_items.reverse;
+                    END;
+
+                    # Shorten the hold_items list per offset/limit/count 
+                    hi = offset + limit - 1;
+                    hi = hi > hold_items.max ? hold_items.max : hi;
+
+                    hold_items = hold_items.slice(offset, hi);
+                END;
+
+               # hold_items list is now sorted.  Traverse and dump the information.
+               
+                FOR hold IN hold_items;
                     ahr = hold.hold.hold %]
                 <tr name="acct_holds_temp"
                     class="acct_holds_temp[% ahr.frozen == 't' ? ' inactive-hold' : '' %]">
                     </td>
                     <td>
                         <div>
-                            [% 
-                                title = attrs.title;
-                                IF ahr.hold_type == 'P';
-                                    title = l('[_1] ([_2])', title, hold.hold.part.label);
-                                END;
-                            %]
-                            <a href="[% mkurl(ctx.opac_root _ '/record/' _ hold.hold.bre_id) %]">[% title | html %]</a>
+                            [% title = hold.ATTRS.title;
+                            IF ahr.hold_type == 'P';
+                               title = l('[_1] ([_2])', title, hold.hold.part.label);
+                            END; %]
+
+                            <a href="[% mkurl(ctx.opac_root _ '/record/' _ 
+                                hold.hold.bre_id, {}, 1) %]"
+                                name="[% l('Catalog record') %]"><span[%- deemphasize_class -%]>
+                                [%- title.substr(0,hold.ATTRS.nonfiling_characters) | html %]</span>
+                                [%- title.substr(hold.ATTRS.nonfiling_characters)   | html %]</a>
                         </div>
                     </td>
                     <td>
                         <div>
                             <a href="[% mkurl(ctx.opac_root _ '/results',
-                                {qtype => 'author', query => attrs.author.replace('[,\.:;]', '')}
-                            ) %]">[% attrs.author | html %]</a>
+                                {qtype => 'author', query => hold.ATTRS.author.replace('[,\.:;]', '')},
+                                1
+                            ) %]">[% hold.ATTRS.author | html %]</a>
                         </div>
                     </td>
                     <td>
                         <div class="format_icon">
                           [% 
-                            formats = attrs.all_formats;
+                            formats = hold.ATTRS.all_formats;
                             IF ahr.hold_type == 'M';
                               # only show selected formats for metarecords
                               formats = [];
index 8edeab5..6c2cb47 100644 (file)
         titresults = xml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b" or @code="n" or @code="p"]');
         titresults_content = [];
             FOR sub IN titresults; titresults_content.push(sub.textContent); END;
+
         args.title = titresults_content.join(" ");
         # Avoid ugly trailing syntax on brief titles
         args.title = args.title | replace('[:;/]$', '');
         END;
         args.title_extended = (args.titles.size) ? args.titles.0 : '';
 
+        # Create a version of the title designed for sorted displays.
+        args.sort_title = args.title | upper;
+
+        # If the title has a "non-filing chaaracters" 
+        # (to logically remove leading "The " for example)
+        # chop the title. Otherwise, chop until the first alphanumeric.
+        # BTW: Template Toolkit folds 1-element arrays to scalars!
+        title_node = xml.findnodes('//*[@tag="245"]');
+
+        args.nonfiling_characters = title_node.findvalue('@ind2');
+      
+        IF (args.nonfiling_characters > 0);
+             args.sort_title = args.sort_title.substr(args.nonfiling_characters); 
+        ELSE;
+             args.sort_title = args.sort_title.replace('^[^A-Z0-9]*','');
+        END;
+      
         args.pubplaces = [];
         pubplace_hunt = xml.findnodes('//*[@tag="260"]/*[@code="a"]') ||
             xml.findnodes('//*[@tag="264" and @ind2="1"]/*[@code="a"]');
index 74426df..828337b 100644 (file)
@@ -37,7 +37,7 @@
                     ELSE;
                         cls_which = "acct-tab-off";
                     END -%]
-                <a href="[% mkurl(ctx.opac_root _ '/myopac/' _ page.url, {}, ['bbid', 'offset', 'limit']) %]"
+                <a href="[% mkurl(ctx.opac_root _ '/myopac/' _ page.url, {}, ['bbid', 'offset', 'limit','sort','sort_type']) %]"
                     class="[% cls_which %]">[% page.name; %]</a>
                 [% END %]
             </div>
diff --git a/Open-ILS/src/templates/opac/parts/myopac/column_sort_support.tt2 b/Open-ILS/src/templates/opac/parts/myopac/column_sort_support.tt2
new file mode 100644 (file)
index 0000000..eb2bc49
--- /dev/null
@@ -0,0 +1,38 @@
+[%# Produce a URL for a given field that cycles for sorting from 
+    "nothing" to "ascending" to "descending" then back to "nothing".
+%]
+[% MACRO sort_url(field)
+        IF (CGI.param('sort') == field);
+                SWITCH CGI.param('sort_type');
+                CASE "asc";
+                   mkurl('',{sort=>field, sort_type=>'desc'},1);
+                CASE "desc";
+                   mkurl('',{},1);
+                CASE;
+                   mkurl('',{sort=>field, sort_type=>'asc'}, 1);
+                END;
+       ELSE;
+           mkurl('',{sort=>field, sort_type=>'asc'}, 1);
+       END;
+%]
+[%# SET click_sort = l("click to sort") %]
+[%# SET click_sort = "title=\"$click_sort\"" %]
+
+[%# Produce arrows to indicate the sorting status of the column %]
+[% MACRO sort_indicator(field)
+        IF (CGI.param('sort') == field);
+                SWITCH CGI.param('sort_type');
+                CASE "asc";
+"&nbsp;<span class=\"column_sort_arrow\">&#9650;</span>";
+                CASE "desc";
+"&nbsp;<span class=\"column_sort_arrow\">&#9660;</span>";
+                END;
+        END;
+%]
+
+[%# Column headers for sortable columns %]
+[% MACRO sort_head(field, field_label) 
+   BLOCK %]
+<a href="[% sort_url(field) %]" [% click_sort %]>[% l(field_label) %]</a>[%- sort_indicator(field) %]
+[% END
+%]
index aca62a8..719ffa1 100644 (file)
@@ -34,7 +34,7 @@
                         </div>
                     [% END %]
                 </div>
-                <a href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, ['single', 'message_id']) %]"
+                <a href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, ['single', 'message_id', 'sort','sort_type']) %]"
                     class="opac-button">[% l('My Account') %]</a>
                 <a href="[% mkurl(ctx.opac_root _ '/myopac/lists', {}, ['single', 'message_id']) %]"
                     class="opac-button">[% l('My Lists') %]</a>
             </div>
             <div id="dashboard">
                 <span class="dash-align">
-                    <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/circs', {}, ['limit','offset', 'single', 'message_id'])
+                    <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/circs', {}, 
+                        ['limit','offset', 'single', 'message_id', 'sort','sort_type'])
                         %]"><span id="dash_checked">[% ctx.user_stats.checkouts.total_out
                         %]</span> [% l("Checked Out") %]</a>
                 </span>
                 <span class="dash_divider">|</span>
                 <span class="dash-align">
-                    <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/holds', {}, ['available', 'single', 'message_id'])
+                    <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/holds', {}, 
+                        ['available', 'single', 'message_id', 'sort','sort_type'])
                         %]"><span id="dash_holds">[% ctx.user_stats.holds.total
                         %]</span> [% l("On Hold") %]</a>
                 </span>
                 <span class="dash_divider">|</span>
                 <span class="dash-align">
                     <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/holds',
-                        {available => 1}, ['single', 'message_id']) %]"><span id="dash_pickup">[%
+                        {available => 1}, ['single', 'message_id', 'sort','sort_type']) %]"><span id="dash_pickup">[%
                         ctx.user_stats.holds.ready %]</span> [% l("Ready for Pickup") %]</a>
                 </span>
                 <span class="dash_divider">|</span>
                 <span class="dash-align">
-                    <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, ['single', 'message_id'])
+                    <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, ['single', 'message_id', 'sort','sort_type'])
                         %]"><span id="dash_fines">[% money(ctx.user_stats.fines.balance_owed)
                         %]</span> [% l("Fines") %]</a>
                 </span>
diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/column_sort.txt b/docs/RELEASE_NOTES_NEXT/OPAC/column_sort.txt
new file mode 100644 (file)
index 0000000..52fe317
--- /dev/null
@@ -0,0 +1,21 @@
+Column sorting in circulation screens
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Sorting of selected columns is now available in the *Items Checked Out*, *Check Out History*,
+and *Holds* screen.
+
+* Clicking on the appropriate column heads now sorts the contents from
+``ascending'' to ``descending'' to ``no sort''.  (The ``no sort'' restores the
+original list as presented in the screen.)
+
+* The sort indicator (an up or down arrow) is placed to the right
+of the column head, as appropriate.
+
+* The combined *Title/Author* column in the *Items Checked Out* screen is now separated into two
+independently sortable columns (Title and Author).
+
+* Title sorting is done with the so-called `filing' characters (leading ``the'', ``a'',
+``an'', and other langugage equivalents) removed. The leading articles are rendered in
+a smaller font, so as to keep the main entry prominent.  In
+addition to the filing characters removed for the sort, leading
+non-alphanumeric characters are ignored in the sort.