JBAS-1377 New headings report UI / report view.
authorBill Erickson <berickxx@gmail.com>
Wed, 28 Sep 2016 20:27:59 +0000 (16:27 -0400)
committerBill Erickson <berickxx@gmail.com>
Thu, 21 Mar 2019 19:46:23 +0000 (15:46 -0400)
Adds a new reporter.cataloged_browse_entry DB view for finding browse
headings based on when their bibs were cataloged.

Adds a UI for searching and displayin such headings.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
KCLS/sql/schema/deploy/new-headings-report.sql [new file with mode: 0644]
KCLS/sql/schema/revert/new-headings-report.sql [new file with mode: 0644]
KCLS/sql/schema/sqitch.plan
KCLS/sql/schema/verify/new-headings-report.sql [new file with mode: 0644]
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/templates/cat/authority/new_headings.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/cat/authority/new_headings.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/admin/index.xhtml

diff --git a/KCLS/sql/schema/deploy/new-headings-report.sql b/KCLS/sql/schema/deploy/new-headings-report.sql
new file mode 100644 (file)
index 0000000..0c63321
--- /dev/null
@@ -0,0 +1,286 @@
+-- Deploy kcls-evergreen:new-headings-report to pg
+-- requires: po-print-li-count-and-date
+
+BEGIN;
+
+ALTER TABLE metabib.browse_author_entry
+    ADD COLUMN create_date TIMESTAMP WITH TIME ZONE
+    NOT NULL DEFAULT NOW();
+
+ALTER TABLE metabib.browse_subject_entry
+    ADD COLUMN create_date TIMESTAMP WITH TIME ZONE
+    NOT NULL DEFAULT NOW();
+
+ALTER TABLE metabib.browse_series_entry
+    ADD COLUMN create_date TIMESTAMP WITH TIME ZONE
+    NOT NULL DEFAULT NOW();
+
+ALTER TABLE metabib.browse_title_entry
+    ADD COLUMN create_date TIMESTAMP WITH TIME ZONE
+    NOT NULL DEFAULT NOW();
+
+ALTER TABLE metabib.browse_call_number_entry
+    ADD COLUMN create_date TIMESTAMP WITH TIME ZONE
+    NOT NULL DEFAULT NOW();
+
+CREATE INDEX by_cataloging_date_idx 
+    ON biblio.record_entry (cataloging_date) 
+    WHERE deleted IS FALSE or deleted = FALSE;
+
+-- Given a tag (or tag substring), create a single string
+-- concatenating the combined set of tags, indicators, subfields,
+-- and values into a single string.
+CREATE OR REPLACE FUNCTION evergreen.marc_tag_to_string
+    (tag TEXT, marcxml TEXT) RETURNS TEXT AS $FUNK$
+DECLARE
+    final TEXT DEFAULT '';
+    datafield XML;
+    subfield XML;
+BEGIN
+    FOR datafield IN SELECT UNNEST(XPATH(
+            '//*[starts-with(@tag,"'||tag||'")]', marcxml::XML)) LOOP
+
+        -- delineate marc tags
+        IF final <> '' THEN final := final || ' | '; END IF;
+
+        -- add the tag
+        final := final || (SELECT ARRAY_TO_STRING(
+            XPATH('//*/@tag[1]', datafield ), ''));
+
+        -- add indicator 1 and 2, defaulting to _
+        final := final || ' '
+            || (SELECT REGEXP_REPLACE(ARRAY_TO_STRING(
+                XPATH('//*/@ind1[1]', datafield), ''), '^\s?$', '_'))
+            || (SELECT REGEXP_REPLACE(ARRAY_TO_STRING(
+                XPATH('//*/@ind2[1]', datafield), ''), '^\s?$', '_'));
+
+        -- append each code and subfield value
+        FOR subfield IN SELECT UNNEST(XPATH(
+                '//*[@code]', datafield)) LOOP
+            final := final || ' $' || (
+                SELECT evergreen.xml_famous5_to_text( -- unescape XML
+                    ARRAY_TO_STRING(XPATH(
+                    '//*/@code[1]|//*/text()[1]', subfield), ' ')));
+        END LOOP;
+
+    END LOOP;
+    RETURN final;
+END;
+$FUNK$ LANGUAGE PLPGSQL;
+
+-- Returns a browse entry summary object of the selected type directly
+-- preceeding or following the provided browse entry sort value.
+-- If 'is_next' is true, return the next entry, otherwise the preceeding.
+CREATE OR REPLACE FUNCTION
+    reporter.metabib_browse_entry_window_part
+    (browse_type TEXT, pivot_sort_value TEXT, is_next BOOLEAN)
+    RETURNS TABLE (
+        entry BIGINT,
+        entry_value TEXT,
+        entry_sort_value TEXT,
+        entry_create_date TIMESTAMPTZ,
+        bib_record BIGINT,
+        bib_editor INTEGER,
+        bib_edit_date TIMESTAMPTZ,
+        bib_create_date TIMESTAMPTZ,
+        bib_cataloging_date TIMESTAMPTZ,
+        field_def INTEGER,
+        field_label TEXT,
+        auth_tag CHARACTER(3)
+    ) AS $FUNK$
+DECLARE
+    pivot_where TEXT;
+    pivot_sort TEXT;
+BEGIN
+
+    pivot_where := '<';
+    pivot_sort := 'DESC';
+
+    IF is_next THEN
+        pivot_where := '>';
+        pivot_sort := 'ASC';
+    END IF;
+
+    RETURN QUERY EXECUTE $$
+        SELECT 
+            entry.id, entry.value, entry.sort_value, entry.create_date,
+            bre.id, bre.editor, bre.edit_date, bre.create_date,
+            bre.cataloging_date, map.def, cmf.label, field.tag
+            FROM metabib.browse_$$ || browse_type || $$_entry entry
+                LEFT JOIN metabib.browse_$$ || 
+                    browse_type || $$_entry_def_map map
+                    ON (map.entry = entry.id)
+                LEFT JOIN biblio.record_entry bre
+                    ON (bre.id = map.source AND NOT bre.deleted)
+                LEFT JOIN config.metabib_field cmf ON (cmf.id = map.def)
+                LEFT JOIN metabib.browse_$$ || 
+                    browse_type || $$_entry_simple_heading_map hmap
+                    ON (map.source IS NULL AND hmap.entry = entry.id)
+                LEFT JOIN authority.simple_heading ash ON
+                    (ash.id = hmap.simple_heading)
+                LEFT JOIN authority.control_set_authority_field field
+                    ON (field.id = ash.atag)
+            WHERE
+                (map.source IS NOT NULL OR hmap.entry IS NOT NULL) AND
+                entry.sort_value $$ || pivot_where || ' ' 
+                    || QUOTE_LITERAL(pivot_sort_value) || $$
+            ORDER BY entry.sort_value $$ || pivot_sort || $$
+            LIMIT 1
+    $$;
+END;
+$FUNK$ LANGUAGE PLPGSQL;
+
+
+-- Returns a browse entry summary object of the selected type directly
+-- preceeding the provided browse entry sort value.
+CREATE OR REPLACE FUNCTION
+    reporter.metabib_browse_entry_window_prev
+    (browse_type TEXT, pivot_sort_value TEXT)
+    RETURNS TABLE (
+        prev_entry BIGINT,
+        prev_entry_value TEXT,
+        prev_entry_sort_value TEXT,
+        prev_entry_create_date TIMESTAMPTZ,
+        prev_bib_record BIGINT,
+        prev_bib_editor INTEGER,
+        prev_bib_edit_date TIMESTAMPTZ,
+        prev_bib_create_date TIMESTAMPTZ,
+        prev_bib_cataloging_date TIMESTAMPTZ,
+        prev_field_def INTEGER,
+        prev_field_label TEXT,
+        prev_auth_tag CHARACTER(3)
+    ) 
+AS $FUNK$
+    SELECT * FROM
+        reporter.metabib_browse_entry_window_part(
+            browse_type, pivot_sort_value, FALSE);
+$FUNK$ LANGUAGE SQL;
+
+-- Returns a browse entry summary object of the selected type directly
+-- preceeding the provided browse entry sort value.
+CREATE OR REPLACE FUNCTION
+    reporter.metabib_browse_entry_window_next
+    (browse_type TEXT, pivot_sort_value TEXT)
+    RETURNS TABLE (
+        next_entry BIGINT,
+        next_entry_value TEXT,
+        next_entry_sort_value TEXT,
+        next_entry_create_date TIMESTAMPTZ,
+        next_bib_record BIGINT,
+        next_bib_editor INTEGER,
+        next_bib_edit_date TIMESTAMPTZ,
+        next_bib_create_date TIMESTAMPTZ,
+        next_bib_cataloging_date TIMESTAMPTZ,
+        next_field_def INTEGER,
+        next_field_label TEXT,
+        next_auth_tag CHARACTER(3)
+    ) 
+AS $FUNK$
+    SELECT * FROM
+        reporter.metabib_browse_entry_window_part(
+            browse_type, pivot_sort_value, TRUE);
+$FUNK$ LANGUAGE SQL;
+
+-- Returns all browse entries that link to at least one non-deleted bib 
+-- record that has a value set for the cataloging date.  Includes details
+-- on the linked record.
+CREATE OR REPLACE FUNCTION 
+    reporter.cataloged_browse_entries(browse_type TEXT) 
+    RETURNS TABLE (
+        entry               BIGINT,
+        entry_value         TEXT,
+        entry_sort_value    TEXT,
+        entry_create_date   TIMESTAMPTZ,
+        bib_record          BIGINT,
+        bib_editor          INTEGER,
+        bib_edit_date       TIMESTAMPTZ,
+        bib_cataloging_date TIMESTAMPTZ,
+        bib_create_date     TIMESTAMPTZ,
+        field_def           INTEGER,
+        heading_date        TIMESTAMPTZ,
+        rank                BIGINT
+    ) AS $FUNK$
+BEGIN
+    RETURN QUERY EXECUTE $$
+        WITH cataloged_entries AS (
+            SELECT
+                entry.id, entry.value, entry.sort_value, entry.create_date,
+                map.source, bre.editor, bre.edit_date, bre.cataloging_date,
+                bre.create_date, map.def,
+                GREATEST(bre.cataloging_date, entry.create_date),
+                RANK() OVER (
+                    PARTITION BY entry.id
+                    ORDER BY
+                        bre.cataloging_date,
+                        map.id -- tie breaker
+                )
+            FROM metabib.browse_$$ || browse_type || $$_entry entry
+                JOIN metabib.browse_$$ || browse_type || $$_entry_def_map map 
+                    ON (map.entry = entry.id)
+                JOIN biblio.record_entry bre ON (bre.id = map.source)
+            WHERE
+                bre.deleted IS FALSE AND
+                bre.cataloging_date IS NOT NULL
+        ) 
+        SELECT all_entries.*
+        FROM cataloged_entries all_entries
+        -- we only want the most relevent linked bib record
+        WHERE all_entries.rank = 1
+    $$;
+END;
+$FUNK$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE VIEW reporter.cataloged_browse_entry_combined AS
+    SELECT 'subject'::TEXT AS browse_axis, * 
+        FROM reporter.cataloged_browse_entries('subject')
+    UNION ALL
+    SELECT 'author'::TEXT AS browse_axis, * 
+        FROM reporter.cataloged_browse_entries('author')
+    UNION ALL
+    SELECT 'series'::TEXT AS browse_axis, * 
+        FROM reporter.cataloged_browse_entries('series')
+    ;
+
+CREATE OR REPLACE VIEW reporter.cataloged_browse_entry AS
+SELECT entry.*,
+    cmf.label AS field_label,
+    usr.usrname AS bib_editor_usrname,
+    evergreen.marc_tag_to_string('1', bre.marc) AS bib_marc_1xx,
+    evergreen.marc_tag_to_string('245', bre.marc) AS bib_marc_245,
+    (reporter.metabib_browse_entry_window_prev(
+        entry.browse_axis, entry.entry_sort_value)).*,
+    (reporter.metabib_browse_entry_window_next(
+        entry.browse_axis, entry.entry_sort_value)).*
+FROM reporter.cataloged_browse_entry_combined entry
+    JOIN config.metabib_field cmf ON (cmf.id = entry.field_def)
+    JOIN biblio.record_entry bre ON (bre.id = entry.bib_record)
+    JOIN actor.usr usr ON (usr.id = bre.editor)
+ORDER BY 
+    CASE entry.browse_axis 
+        WHEN 'author' THEN 1 
+        WHEN 'subject' THEN 2 
+        ELSE 3 
+    END, 
+    entry.entry_sort_value
+;
+
+-- Local testing
+
+/*
+update metabib.browse_series_entry set create_date = now() - '2 days'::interval;
+update metabib.browse_series_entry set create_date = now() WHERE id between 1 and 1000;
+update metabib.browse_subject_entry set create_date = now() - '2 days'::interval;
+update metabib.browse_subject_entry set create_date = now() WHERE id between 1 and 1000;
+update metabib.browse_author_entry set create_date = now() - '2 days'::interval;
+update metabib.browse_author_entry set create_date = now() WHERE id between 1 and 1000;
+*/
+
+/*
+SELECT *
+FROM reporter.cataloged_browse_subject_entry
+WHERE DATE(heading_date) >= current_date ORDER BY entry_sort_value;
+*/
+
+-- /LOCAL TESTING
+
+COMMIT;
diff --git a/KCLS/sql/schema/revert/new-headings-report.sql b/KCLS/sql/schema/revert/new-headings-report.sql
new file mode 100644 (file)
index 0000000..a8530d1
--- /dev/null
@@ -0,0 +1,31 @@
+-- Revert kcls-evergreen:new-headings-report from pg
+
+BEGIN;
+
+/*
+DROP VIEW reporter.cataloged_browse_subject_entry;
+DROP VIEW reporter.cataloged_browse_author_entry;
+DROP VIEW reporter.cataloged_browse_series_entry;
+*/
+
+DROP VIEW reporter.cataloged_browse_entry;
+DROP VIEW reporter.cataloged_browse_entry_combined;
+
+DROP FUNCTION IF EXISTS 
+    reporter.cataloged_browse_entries(TEXT);
+DROP FUNCTION IF EXISTS 
+    reporter.metabib_browse_entry_window_prev(TEXT, TEXT);
+DROP FUNCTION IF EXISTS 
+    reporter.metabib_browse_entry_window_next(TEXT, TEXT);
+DROP FUNCTION IF EXISTS 
+    reporter.metabib_browse_entry_window_part (TEXT, TEXT, BOOLEAN);
+
+DROP FUNCTION evergreen.marc_tag_to_string(TEXT, TEXT);
+
+ALTER TABLE metabib.browse_author_entry DROP COLUMN create_date;
+ALTER TABLE metabib.browse_subject_entry DROP COLUMN create_date;
+ALTER TABLE metabib.browse_series_entry DROP COLUMN create_date;
+ALTER TABLE metabib.browse_title_entry DROP COLUMN create_date;
+ALTER TABLE metabib.browse_call_number_entry DROP COLUMN create_date;
+
+COMMIT;
index 52cad6b..8ba5423 100644 (file)
@@ -32,3 +32,4 @@ sip-act-type-freegalsip [payflow-hosted-org-settings] 2016-08-19T20:24:01Z Bill
 audit-table-maint [payflow-hosted-org-settings] 2016-07-22T14:49:08Z Bill Erickson <berickxx@gmail.com> # Audit table cleanup functions
 payflow-hosted-static-silent-post-url [payflow-hosted-org-settings] 2016-08-17T13:14:48Z Bill Erickson <berickxx@gmail.com> # Fall back to PP silent post URL
 teacher-group [student-groups] 2016-09-16T17:43:41Z Bill Erickson <berickxx@gmail.com> # Teach ecard group and policy configs
+new-headings-report [po-print-li-count-and-date] 2016-01-28T22:23:12Z Bill Erickson <berickxx@gmail.com> # Support for reporting on new browse headings
diff --git a/KCLS/sql/schema/verify/new-headings-report.sql b/KCLS/sql/schema/verify/new-headings-report.sql
new file mode 100644 (file)
index 0000000..1ee44e2
--- /dev/null
@@ -0,0 +1,7 @@
+-- Verify kcls-evergreen:new-headings-report on pg
+
+BEGIN;
+
+-- XXX Add verifications here.
+
+ROLLBACK;
index 854aa3e..f77f721 100644 (file)
@@ -4248,6 +4248,59 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <link field="simple_heading" reltype="has_a" key="id" map="" class="ash"/>
         </links>
     </class>
+    <class id="rcbe" 
+      controller="open-ils.cstore open-ils.pcrud" 
+      oils_obj:fieldmapper="reporter::cataloged_browse_entry" 
+      oils_persist:tablename="reporter.cataloged_browse_entry"
+      reporter:label="Cataloged Headings" 
+      reporter:core="true" oils_persist:readonly="true">
+      <fields>
+        <field name="browse_axis" reporter:datatype="text" reporter:label="Heading Type"/>
+        <field name="heading_date" reporter:datatype="timestamp" reporter:label="Heading Date"/>
+        <field name="prev_entry_value" reporter:datatype="text" reporter:label="Previous Heading"/>
+        <field name="prev_entry" reporter:datatype="link" reporter:label="Previous Browse Entry"/>
+        <field name="prev_bib_record" reporter:datatype="link" reporter:label="Previous Bib Record"/>
+        <field name="prev_field_label" reporter:datatype="text" reporter:label="Previous Field Label"/>
+        <field name="prev_field_def" reporter:datatype="link" reporter:label="Previous Field Definition"/>
+        <field name="prev_auth_tag" reporter:datatype="text" reporter:label="Previous Authority Tag"/>
+        <field name="entry_value" reporter:datatype="text" reporter:label="Heading"/>
+        <field name="entry" reporter:datatype="link" reporter:label="Browse Entry"/>
+        <field name="entry_create_date" reporter:datatype="timestamp" reporter:label="Browse Entry Create Date"/>
+        <field name="bib_record" reporter:datatype="link" reporter:label="Bib Record"/>
+        <field name="field_label" reporter:datatype="text" reporter:label="Field Label"/>
+        <field name="field_def" reporter:datatype="link" reporter:label="Field Definition"/>
+        <field name="bib_edit_date" reporter:datatype="timestamp" reporter:label="Bib Record Edit Date"/>
+        <field name="bib_create_date" reporter:datatype="timestamp" reporter:label="Bib Record Create Date"/>
+        <field name="bib_cataloging_date" reporter:datatype="timestamp" reporter:label="Bib Record Cataloging Date"/>
+        <field name="bib_marc_1xx" reporter:datatype="text" reporter:label="Bib Record MARC 1XX"/>
+        <field name="bib_marc_245" reporter:datatype="text" reporter:label="Bib Record MARC 245"/>
+        <field name="bib_editor" reporter:datatype="link" reporter:label="Bib Record Editor"/>
+        <field name="bib_editor_usrname" reporter:datatype="text" reporter:label="Bib Record Editor Username"/>
+        <field name="next_entry" reporter:datatype="link" reporter:label="Next Browse Entry"/>
+        <field name="next_entry_value" reporter:datatype="text" reporter:label="Next Heading"/>
+        <field name="next_bib_record" reporter:datatype="link" reporter:label="Next Bib Record"/>
+        <field name="next_field_label" reporter:datatype="text" reporter:label="Next Field Label"/>
+        <field name="next_field_def" reporter:datatype="link" reporter:label="Next Field Definition"/>
+        <field name="next_auth_tag" reporter:datatype="text" reporter:label="Next Authority Tag"/>
+      </fields>
+      <links>
+        <link field="prev_entry" reltype="has_a" key="id" map="" class="mbae"/>
+        <link field="prev_bib_record" reltype="has_a" key="id" map="" class="bre"/>
+        <link field="prev_field_def" reltype="has_a" key="id" map="" class="cmf"/>
+        <link field="entry" reltype="has_a" key="id" map="" class="mbae"/>
+        <link field="bib_record" reltype="has_a" key="id" map="" class="bre"/>
+        <link field="bib_editor" reltype="has_a" key="id" map="" class="au"/>
+        <link field="field_def" reltype="has_a" key="id" map="" class="cmf"/>
+        <link field="next_entry" reltype="has_a" key="id" map="" class="mbae"/>
+        <link field="next_bib_record" reltype="has_a" key="id" map="" class="bre"/>
+        <link field="next_field_def" reltype="has_a" key="id" map="" class="cmf"/>
+      </links>
+      <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+        <actions>
+          <retrieve permission="STAFF_LOGIN" global_required='true'/>
+        </actions>
+      </permacrud>
+    </class>
        <class id="mfe" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::field_entry" reporter:label="Combined Field Entry View" oils_persist:readonly="true">
                <oils_persist:source_definition><![CDATA[
                        SELECT * FROM metabib.author_field_entry
diff --git a/Open-ILS/src/templates/cat/authority/new_headings.tt2 b/Open-ILS/src/templates/cat/authority/new_headings.tt2
new file mode 100644 (file)
index 0000000..f708abd
--- /dev/null
@@ -0,0 +1,301 @@
+[% ctx.page_title = l('New Headings Report') %]
+[% WRAPPER base.tt2 %]
+
+<script type="text/javascript"
+  src='[% ctx.media_prefix %]/js/ui/default/cat/authority/new_headings.js'> </script>
+
+<style type="text/css">
+
+  .date-input { width: 6em; }
+
+  #headings-table th {
+    font-weight: bold;
+    font-size: 120%;
+    border-bottom: 2px solid grey;
+  }
+
+  .extended-field-label { 
+    white-space : nowrap; 
+    font-weight: bold;
+  }
+
+  .heading-row:nth-child(even) { 
+    /*background-color: #9ee7fa;*/
+    background-color: rgba(158,231,250, 0.3);
+  }
+
+  .new-heading { font-weight:bold; }
+  .extended-heading-value { 
+    color: red; font-weight: bold; 
+  }
+
+  .dialog-div { width: 700px; }
+  .dialog-div td { padding: 5px; }
+  #headings-row-loading td { padding: 10px; background-color: #AAA }
+
+  #navigation-div-container {
+    font-size: 120%;
+    position:fixed;
+    top: 0px;
+    height: 30px;
+    background-color: white;
+  }
+
+  #navigation-div {
+    background-color: #9ee7fa;
+    border-bottom: solid grey 1px;
+    height: 25px;
+  }
+
+  #navigation-div span { padding-left: 10px; padding-right: 10px }
+
+  #below-nav-div {margin-top: 35px}
+
+  .type-heading-row td {
+    font-size: 125%;
+    font-weight: bold;
+    padding-top: 7px;
+    padding-bottom: 7px;
+    text-align: center;
+    background-color: #8c65d3;
+    color: snow;
+  }
+
+  #loading-indicator, #zero-hits {
+    width: 100%;
+    margin-top: 50px;
+    font-size: 120%;
+    text-align: center;
+  }
+
+  .headings-entry-table {
+    border-bottom: 2px dashed #333;
+  }
+
+  .headings-entry-table td {
+    padding: 3px;
+  }
+
+</style>
+
+<div id='navigation-div-container'>
+  <div id='navigation-div'>
+    <div style='float:left'>
+      <span>Start Date: <input type='text' jsId='startDate'
+        class='date-input' dojoType='dijit.form.DateTextBox'/></span>
+      <span>End Date: <input type='text' jsId='endDate'
+        class='date-input' dojoType='dijit.form.DateTextBox'/></span>
+      <span><button id='apply-dates-btn'>Apply Dates</button></span>
+    </div>
+    <div style='padding-left: 20px; padding-right: 20px; float:left'>|</div>
+    <div style='float:left'>
+      <span><button id='prev-page-btn'>Previous Page</button></span>
+      <span>Page: <b id='page-number'></b></span>
+      <span><button id='next-page-btn'>Next Page</button></span>
+    </div>
+    <div style='clear:both'></div>
+  </div>
+  <div id='navigation-div-pad'></div>
+</div>
+
+<div id='below-nav-div'>
+
+  <div id='loading-indicator'>
+    Loading page <span id='ind-page'></span>...
+    <img src='/opac/images/progressbar_green.gif'/>
+  </div>
+
+  <div id='zero-hits'>
+    No new headings were found for the date range specified
+  </div>
+
+  <table id='headings-table'>
+    <tbody id='headings-tbody'>
+
+      <tr id='author-header-row' class='type-heading-row hidden'>
+        <td colspan='20'>
+          New Author Headings <!--(<span id='author-count'></span>)-->
+        </td>
+      </tr>
+      <tr id='subject-header-row' class='type-heading-row hidden'>
+        <td colspan='20'>
+          New Subject Headings <!--(<span id='subject-count'></span>)-->
+        </td>
+      </tr>
+      <tr id='series-header-row' class='type-heading-row hidden'>
+        <td colspan='20'>
+          New Series Headings <!--(<span id='series-count'></span>)-->
+        </td>
+      </tr>
+
+      <tr id='headings-row-template' class='heading-row'>
+        <td>
+          <table class='headings-entry-table'>
+            <tr>
+              <td>Previous:</td>
+              <td>
+                <span name='prev_entry_value-auth-bib-tag'></span>
+                <span name='prev_entry_value'></span>
+                <span name='prev-auth-tag-wrapper' class='hidden'>
+                  (From <span name='prev_auth_tag'></span>)
+                </span>
+              </td>
+            </tr>
+            <tr>
+              <td>New:</td>
+              <td>
+                <a href='javascript:;' name='heading-dialog-link'>
+                  <span name='entry_value' class='new-heading'></span>
+                </a>
+              </td>
+            </tr>
+            <tr>
+              <td>Next:</td>
+              <td>
+                <span name='next_entry_value-auth-bib-tag'></span>
+                <span name='next_entry_value'></span>
+                <span name='next-auth-tag-wrapper' class='hidden'> 
+                  (From <span name='next_auth_tag'></span>)
+                </span>
+              </td>
+            </tr>
+            <tr>
+              <td>From TCN:</td>
+              <td>
+                <a href='[% ctx.base_path %]/opac/record/'
+                  target='_blank' name='bib_record-catalog-link'>
+                  <span name='bib_record'></span>
+                </a>
+                <span> </span>
+                <span>Indexed As:</span>
+                <span><span name='field_label'></span></span>
+              </td>
+            </tr>
+            <tr>  
+              <td>Edited By:</td>
+              <td><span name='bib_editor_usrname'></span>
+                <span> </span>
+                <span>Heading Date:</span>
+                <span><span name='heading_date'></span></span>
+              </td>
+            </tr>
+            <tr>
+              <td>From 1XX:</td>
+              <td><span name='bib_marc_1xx'></span></td>
+            </tr>
+            <tr>
+              <td>From 245:</td>
+              <td><span name='bib_marc_245'></span></td>
+            </tr>
+          </table>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div class='hidden'>
+    <div dojoType='dijit.Dialog' jsId='detail_dialog' class='dialog-div'>
+      <table id='extended-info-table'>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Previous Heading</td>
+          <td><span name='extended-prev_entry_value' class='extended-heading-value'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Previous Indexed As</td>
+          <td><span name='extended-prev_field_label'></span></td>
+          <td class='extended-field-label'>Previous TCN</td>
+          <td><span name='extended-prev_bib_record'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Previous Authority Tag</td>
+          <td><span name='extended-prev_auth_tag'></span></td>
+          <td class='extended-field-label'>Previous Browse Entry ID</td>
+          <td><span name='extended-prev_entry'></span></td>
+        </tr>
+
+        <tr><td colspan='4'><hr/></td></tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>New Heading</td>
+          <td><span name='extended-entry_value' class='extended-heading-value'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Heading Date</td>
+          <td><span name='extended-heading_date'></span></td>
+          <td class='extended-field-label'>Indexed As</td>
+          <td><span name='extended-field_label'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>From TCN</td>
+          <td><span name='extended-bib_record'></span></td>
+          <td class='extended-field-label'>Browse Entry ID</td>
+          <td><span name='extended-entry'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Heading Create Date</td>
+          <td><span name='extended-entry_create_date'></span></td>
+          <td class='extended-field-label'>Bib Record Edit Date</td>
+          <td><span name='extended-bib_edit_date'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Bib Record Cataloging Date</td>
+          <td><span name='extended-bib_cataloging_date'></span></td>
+          <td class='extended-field-label'>Bib Record Create Date</td>
+          <td><span name='extended-bib_create_date'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Bib Record Editor Username</td>
+          <td><span name='extended-bib_editor_usrname'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>From 1XX</td>
+          <td colspan='3'><span name='extended-bib_marc_1xx'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>From 245</td>
+          <td colspan='3'><span name='extended-bib_marc_245'></span></td>
+        </tr>
+
+        <tr><td colspan='4'><hr/></td></tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Next Heading</td>
+          <td><span name='extended-next_entry_value' class='extended-heading-value'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Next Indexed As</td>
+          <td><span name='extended-next_field_label'></span></td>
+          <td class='extended-field-label'>Next TCN</td>
+          <td><span name='extended-next_bib_record'></span></td>
+        </tr>
+
+        <tr class='heading-field-row'>
+          <td class='extended-field-label'>Next Authority Tag</td>
+          <td><span name='extended-next_auth_tag'></span></td>
+          <td class='extended-field-label'>Next Browse Entry ID</td>
+          <td><span name='extended-next_entry'></span></td>
+        </tr>
+
+        <tr>
+          <td colspan='2'>
+            <div dojoType='dijit.form.Button' type='submit'
+              jsId='detail_dialog_closer'>[% l('Close') %]</div>
+          </td>
+        </tr>
+      </table>
+    </div>
+  </div>
+</div><!-- below-nav-div -->
+
+[% END %]
diff --git a/Open-ILS/web/js/ui/default/cat/authority/new_headings.js b/Open-ILS/web/js/ui/default/cat/authority/new_headings.js
new file mode 100644 (file)
index 0000000..bf213b6
--- /dev/null
@@ -0,0 +1,321 @@
+dojo.require('dijit.Dialog');
+dojo.require('dijit.form.DateTextBox');
+dojo.require('dijit.form.Button');
+dojo.require('dojo.date.locale');
+dojo.require("fieldmapper.Fieldmapper");
+dojo.require('openils.CGI');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.Util');
+dojo.require('openils.XUL');
+
+var cgi = new openils.CGI();
+var pcrud = new openils.PermaCrud();
+var page_size = 50; // TODO UI
+var page_offset = 0; // TODO UI
+var start_date;
+var end_date;
+// Max summary display field length before truncation
+var max_val_length = 80;
+var headings_tbody;
+var template_row;
+var cached_headings
+var all_fetched;
+
+var summary_fields = [
+    'prev_entry_value',
+    'entry_value',
+    'next_entry_value',
+    'field_label',
+    'bib_record',
+    'heading_date',
+    'bib_editor_usrname',
+    'prev_auth_tag',
+    'next_auth_tag',
+    'bib_marc_1xx',
+    'bib_marc_245'
+];
+
+var extended_fields = [
+    'prev_entry',
+    'prev_bib_record',
+    'prev_field_label',
+    'entry',
+    'entry_create_date',
+    'bib_edit_date',
+    'bib_create_date',
+    'bib_cataloging_date',
+    'next_entry',
+    'next_bib_record',
+    'next_field_label',
+];
+
+extended_fields = extended_fields.concat(summary_fields);
+
+function load() {
+
+    if (!headings_tbody) {
+        headings_tbody = dojo.byId('headings-tbody');
+        template_row = headings_tbody.removeChild(
+            dojo.byId('headings-row-template'));
+    } 
+
+    setup_paging();
+
+    dojo.byId('apply-dates-btn').onclick = function() {
+        page_offset = 0;
+        load_headings(true); // reset 
+    }
+
+    load_headings(true);
+}
+
+// Fetch headings.
+// Limiting each pcrud query by browse_axis significantly speeds up
+// the DB query, so fetch in order of display: author -> subject -> series.
+function load_headings(is_new, new_page) {
+
+    if (is_new) { // reset cache
+        cached_headings = [];
+        all_fetched = false;
+        new_page = true;
+        openils.Util.hide('zero-hits');
+    }
+
+    if (new_page) {
+        openils.Util.show('loading-indicator');
+        dojo.byId('ind-page').innerHTML = page_number();
+        openils.Util.hide('headings-table');
+        openils.Util.hide('author-header-row');
+        openils.Util.hide('subject-header-row');
+        openils.Util.hide('series-header-row');
+        dojo.query('[class=heading-row]').forEach(function(node) {
+            node.parentNode.removeChild(node)
+        });
+    }
+
+    // force the main dojo body div to scroll to the top
+    // during all navigation.
+    dojo.byId('oils-base-body-block').scrollTop = 0;
+
+    if (cached_headings.length >= page_offset + page_size || all_fetched) {
+        return draw();
+    }
+
+    /* cache 4 pages, plus 1 heading at a time */
+    var fetch_count = page_size * 4 + 1;
+
+    pcrud.search('rcbe', compile_date_filter(), {
+        offset : page_offset,
+        limit : fetch_count, 
+        async : true,
+        oncomplete : function(r) { 
+            var new_headings = openils.Util.readResponse(r);
+            cached_headings = cached_headings.concat(new_headings);
+
+            var fetched = new_headings.length;
+            console.log('Loaded ' + fetched + ' headings');
+
+            if (fetched < fetch_count) all_fetched = true;
+            draw();
+        }
+    });
+}
+
+var first_load = true;
+function compile_date_filter() {
+
+    if (first_load) {
+        first_load = false;
+        startDate.attr('value', new Date());
+        endDate.attr('value', new Date());
+    }
+
+    start_date = startDate.attr('value');
+    end_date = endDate.attr('value');
+
+    if (start_date) {
+        start_date = openils.Util.getYMD(start_date);
+    }
+
+    if (end_date) {
+        // the end date has to be extended by one day, since the between
+        // query cuts off at midnight (0 hour) on the end date, which 
+        // would miss all headings for that date created after hour 0.
+        // note: setDate() will rollover to the next month when needed.
+        end_date.setDate(end_date.getDate() + 1);
+        end_date = openils.Util.getYMD(end_date);
+    }
+
+    var date_filter = {heading_date : {}};
+
+    if (start_date && end_date) {
+        date_filter.heading_date = {between : [start_date, end_date]}
+    } else if (start_date) {
+        date_filter.heading_date = {'>=' : start_date};
+    } else {
+        date_filter.heading_date = {'<=' : end_date};
+    }
+
+    return date_filter;
+}
+
+function page_number() {
+    return (page_offset / page_size) + 1
+}
+
+function setup_paging() {
+
+    dojo.byId('page-number').innerHTML = page_number();
+
+    dojo.byId('prev-page-btn').onclick = function() {
+        if (page_offset <= 0) return;
+        page_offset -= page_size;
+        load_headings(false, true);
+        dojo.byId('page-number').innerHTML = page_number();
+    }
+
+    dojo.byId('next-page-btn').onclick = function() {
+        page_offset += page_size;
+        load_headings(false, true);
+        dojo.byId('page-number').innerHTML = page_number();
+    }
+}
+
+function draw() {
+
+    headings = cached_headings.slice(page_offset, page_offset + page_size);
+    openils.Util.hide('loading-indicator');
+
+    if (headings.length == 0) {
+        openils.Util.show('zero-hits');
+        return;
+    }
+
+    openils.Util.show('headings-table', 'table');
+
+    if (page_offset == 0) {
+        dojo.byId('prev-page-btn').setAttribute('disabled', true);
+    } else {
+        dojo.byId('prev-page-btn').removeAttribute('disabled');
+    }
+
+    console.log('total = ' + cached_headings.length);
+    console.log('page size = ' + page_size);
+    console.log('page offset = ' + page_offset);
+
+    if (cached_headings.length > page_size + page_offset) {
+        dojo.byId('next-page-btn').removeAttribute('disabled'); 
+    } else {
+        dojo.byId('next-page-btn').setAttribute('disabled', true); 
+    }
+
+    dojo.forEach(headings, function(heading, idx) {
+
+        var row = template_row.cloneNode(true);
+        row.id = 'headings-row-' + idx;
+
+        if (heading.browse_axis() == 'author') {
+            headings_tbody.insertBefore(row, dojo.byId('subject-header-row')); 
+            openils.Util.show('author-header-row', 'table-row');
+        } else if (heading.browse_axis() == 'subject') {
+            openils.Util.show('subject-header-row', 'table-row');
+            headings_tbody.insertBefore(row, dojo.byId('series-header-row')); 
+        } else {
+            openils.Util.show('series-header-row', 'table-row');
+            headings_tbody.appendChild(row);
+        }
+
+        if (!heading.prev_bib_record()) {
+            openils.Util.show(
+              openils.Util.getNodeByName('prev-auth-tag-wrapper', row),
+              'inline'
+            );
+        }
+
+        if (!heading.next_bib_record()) {
+            openils.Util.show(
+              openils.Util.getNodeByName('next-auth-tag-wrapper', row),
+              'inline'
+            );
+        }
+            
+        dojo.forEach(summary_fields, function(heading_field) {
+            var value = heading[heading_field]();
+
+            if (heading_field.match(/date/)) {
+                value = dojo.date.locale.format(
+                    new Date(Date.parse(value)), {formatLength:'short'});
+            } else {
+                // prevent values from being crazy long
+                if (value && value.length > max_val_length) 
+                    value = value.substr(0, max_val_length) + '...';
+            }
+
+            openils.Util.getNodeByName(heading_field, row).innerHTML = value;
+
+            if (heading_field == 'entry_value') {
+                var link = openils.Util.getNodeByName(
+                  'heading-dialog-link', row);
+
+                link.onclick = function() {
+                    flesh_details_dialog(heading);
+                }
+            } else if (heading_field.match('entry_value')) {
+                var node = openils.Util.getNodeByName(
+                   heading_field + '-auth-bib-tag', row);
+                if (heading_field == 'prev_entry_value') {
+                    node.innerHTML = heading.prev_bib_record() ? 'B: ' : 'A: ';
+                } else {
+                    node.innerHTML = heading.next_bib_record() ? 'B: ' : 'A: ';
+                }
+            }
+
+            if (heading_field == 'bib_record') {
+                var link = openils.Util.getNodeByName(
+                  'bib_record-catalog-link', row);
+
+                // append the bib record ID to the in-template catalog link.
+                link.setAttribute('href', link.getAttribute('href') + value);
+
+                if (openils.XUL.isXUL()) {
+                    // open a new staff client tab to view the bib record
+                    var target_url = oilsBasePath + '/opac/record/' + value;
+
+                    link.onclick = function() {
+                        openils.XUL.newTabEasy(
+                            xulG.url_prefix(
+                                'chrome://open_ils_staff_client/content/cat/opac.xul'),
+                            null, {'opac_url' : target_url}
+                        );
+                        return false;
+                    }
+                }
+            }
+        });
+    });
+}
+
+function flesh_details_dialog(heading) {
+
+    detail_dialog.show();
+
+    dojo.forEach(extended_fields, function(heading_field) {
+        var value = heading[heading_field]();
+
+        if (heading_field.match(/date/)) {
+            value = dojo.date.locale.format(
+                new Date(Date.parse(value)), {formatLength:'short'});
+        }
+        
+        try {
+            openils.Util.getNodeByName('extended-' + heading_field, 
+                detail_dialog.domNode).innerHTML = value;
+        } catch (E) {
+            console.error('Error setting value for field ' + heading_field);
+            throw E;
+        }
+    });
+}
+
+dojo.addOnLoad(load);
+
index ea4a3f1..739fbf4 100644 (file)
@@ -93,6 +93,9 @@
                             <div style='padding: 8px;'>
                                 <a href='javascript:window.xulG.new_tab("/xul/server/admin/transit_list.xul",{"tab_name":"&staff.server.admin.index.transits;"},{});'>&staff.server.admin.index.transit_list;</a>
                             </div>
+                            <div style='padding: 8px;'>
+                                <a href='/eg/cat/authority/new_headings'>New Headings Report</a>
+                            </div>
                         </td>
                     </tr>
                 </tbody>